This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new 59ece79  fix(generate-cve-json): forward-state labels keep 
CNA_private.state at REVIEW (#373)
59ece79 is described below

commit 59ece7919ee53dc119f731d4025829aa773a627c
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri May 29 19:23:40 2026 +0200

    fix(generate-cve-json): forward-state labels keep CNA_private.state at 
REVIEW (#373)
    
    When release-vote gating is enabled, the generator computed
    release_vote_in_progress = (RC_VOTING_LABEL in issue_labels). The
    sync skill's pr-merged-to-fix-released transition removes the
    rc-voting label and adds fix-released, so the two events combined
    made the generator compute release_vote_in_progress=False and
    walked the embedded CNA_private.state back from REVIEW to DRAFT —
    the wrong direction for a record about to be published.
    
    Add a FORWARD_STATE_LABELS set (configurable via
    [workflow].forward_state_labels, default fix-released /
    announced-emails-sent / announced / vendor-advisory-ready) and OR
    it into the gate check. Any forward-state label on the tracker
    means the release has shipped — the vote, if there was one,
    passed — so the rc-voting gate is moot.
    
    The bug was caught while syncing airflow-s#259 and airflow-s#377
    after Airflow 3.2.2 shipped. The sync skill workaround was to
    pass --review on every regen, but that loses the auto-gate
    behaviour the config switch is supposed to provide.
    
    Tests:
    - test_forward_state_labels_keep_state_at_review_when_rc_voting_removed
      verifies every default forward-state label keeps REVIEW.
    - test_custom_forward_state_labels_from_config verifies adopters
      can extend or replace the default set.
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../src/generate_cve_json/cve_json.py              | 35 ++++++++++++-
 .../vulnogram/generate-cve-json/tests/test_cli.py  | 57 ++++++++++++++++++++++
 2 files changed, 90 insertions(+), 2 deletions(-)

diff --git 
a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py 
b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
index 4e109c2..5387288 100644
--- a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
+++ b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
@@ -183,7 +183,7 @@ def _populate_constants() -> None:
     global TOP_LEVEL_NAME, TOP_LEVEL_PRODUCT, PROJECT_PRODUCT_TEMPLATE
     global CNA_PRIVATE_PROJECT_URL, CNA_PRIVATE_OWNER, CNA_PRIVATE_USERS_LIST
     global TITLE_STRIP_RE, TRACKER_FILTER_TOKEN
-    global RELEASE_VOTE_GATING, RC_VOTING_LABEL
+    global RELEASE_VOTE_GATING, RC_VOTING_LABEL, FORWARD_STATE_LABELS
 
     DEFAULT_REPO = cfg["meta"]["tracker_repo"]
     DEFAULT_VENDOR = cfg["product"]["vendor"]
@@ -226,6 +226,21 @@ def _populate_constants() -> None:
     workflow_cfg = cfg.get("workflow", {})
     RELEASE_VOTE_GATING = bool(workflow_cfg.get("release_vote_gating", False))
     RC_VOTING_LABEL = workflow_cfg.get("rc_voting_label", "rc voting")
+    # Forward-state labels — when the tracker carries any of these the
+    # release has already shipped (the vote, if there was one, passed) and
+    # the rc-voting gate is moot. Treat them as "vote completed", which
+    # means a ready CNA stays at REVIEW (or advances to PUBLIC on
+    # vendor-advisory) regardless of whether the `rc voting` label is
+    # still on the tracker. Without this, removing `rc voting` at the
+    # `pr merged → fix released` transition (per the sync skill) walks
+    # the state back to DRAFT — the wrong direction for a record that's
+    # about to be published.
+    FORWARD_STATE_LABELS = set(
+        workflow_cfg.get(
+            "forward_state_labels",
+            ["fix released", "announced - emails sent", "announced", 
"vendor-advisory ready"],
+        )
+    )
 
     # Title-strip regex for `resolve_title` — built from the configured
     # top-level product so the *"<vendor>: <product>:"* prefix is
@@ -269,6 +284,7 @@ TITLE_STRIP_RE: re.Pattern[str] = re.compile("")
 TRACKER_FILTER_TOKEN: str = ""
 RELEASE_VOTE_GATING: bool = False
 RC_VOTING_LABEL: str = ""
+FORWARD_STATE_LABELS: set[str] = set()
 
 # CVE 5.x convention values that are not project-specific.
 DEFAULT_CREDIT_TYPE = "finder"
@@ -1992,7 +2008,22 @@ def main(argv: list[str] | None = None) -> int:
     elif args.draft:
         release_vote_in_progress = False
     elif RELEASE_VOTE_GATING:
-        release_vote_in_progress = RC_VOTING_LABEL in issue_labels
+        # `rc voting` is the *forward* signal that a vote is happening.
+        # Once the release has shipped, the vote-gating is moot — the
+        # release is out, the advisory is going to be written next. Any
+        # forward-state label on the tracker (`fix released`,
+        # `announced - emails sent`, `announced`, `vendor-advisory ready`,
+        # plus any project-specific extensions in
+        # `[workflow].forward_state_labels`) is the "vote completed"
+        # signal. Without this OR, removing `rc voting` at the
+        # `pr merged → fix released` transition (per the sync skill's
+        # convention) would compute `release_vote_in_progress=False` and
+        # walk the embedded `CNA_private.state` back from REVIEW to
+        # DRAFT — the wrong direction for a record that's about to
+        # publish.
+        release_vote_in_progress = RC_VOTING_LABEL in issue_labels or bool(
+            FORWARD_STATE_LABELS & set(issue_labels)
+        )
     else:
         release_vote_in_progress = None  # legacy: ready ⇒ REVIEW
 
diff --git a/tools/vulnogram/generate-cve-json/tests/test_cli.py 
b/tools/vulnogram/generate-cve-json/tests/test_cli.py
index 3d656f9..2622754 100644
--- a/tools/vulnogram/generate-cve-json/tests/test_cli.py
+++ b/tools/vulnogram/generate-cve-json/tests/test_cli.py
@@ -644,6 +644,63 @@ class TestReleaseVoteGating:
         record = json.loads(capsys.readouterr().out)
         assert record["CNA_private"]["state"] == "DRAFT"
 
+    def 
test_forward_state_labels_keep_state_at_review_when_rc_voting_removed(self, 
capsys, tmp_path):
+        # After the `pr merged → fix released` transition (per the sync
+        # skill's convention), the `rc voting` label is removed and
+        # `fix released` is added. Without the forward-state-label
+        # check, the state would walk back from REVIEW to DRAFT —
+        # exactly the wrong direction for a record that is about to
+        # be published. Verify every default forward-state label
+        # individually keeps the state at REVIEW.
+        cfg = self._write_gating_on_config(tmp_path)
+        for forward_label in [
+            "fix released",
+            "announced - emails sent",
+            "announced",
+            "vendor-advisory ready",
+        ]:
+            with patch(
+                "generate_cve_json.cve_json.fetch_issue",
+                return_value=("Issue title", _issue_body(), ["airflow", 
forward_label]),
+            ):
+                rc = cve_json.main(["123", "--config", cfg])
+            assert rc == 0, f"non-zero exit on label={forward_label!r}"
+            record = json.loads(capsys.readouterr().out)
+            assert record["CNA_private"]["state"] == "REVIEW", (
+                f"forward-state label {forward_label!r} should keep state at 
REVIEW, "
+                f"got {record['CNA_private']['state']!r}"
+            )
+
+    def test_custom_forward_state_labels_from_config(self, capsys, tmp_path):
+        # Adopters can extend / replace the default forward-state-label
+        # set via [workflow].forward_state_labels in the TOML config.
+        fixture = (Path(__file__).resolve().parent / "fixtures" / 
"cve-json-config.toml").read_text()
+        patched = fixture.replace(
+            "release_vote_gating = false",
+            'release_vote_gating = true\nforward_state_labels = ["shipped", 
"advisory-published"]',
+        )
+        cfg_path = tmp_path / "cve-json-config-custom-forward.toml"
+        cfg_path.write_text(patched)
+        # The default `fix released` is no longer in the set, so a
+        # ready CNA with only `fix released` walks back to DRAFT.
+        with patch(
+            "generate_cve_json.cve_json.fetch_issue",
+            return_value=("Issue title", _issue_body(), ["airflow", "fix 
released"]),
+        ):
+            rc = cve_json.main(["123", "--config", str(cfg_path)])
+        assert rc == 0
+        record = json.loads(capsys.readouterr().out)
+        assert record["CNA_private"]["state"] == "DRAFT"
+        # `shipped` is in the configured set → REVIEW.
+        with patch(
+            "generate_cve_json.cve_json.fetch_issue",
+            return_value=("Issue title", _issue_body(), ["airflow", 
"shipped"]),
+        ):
+            rc = cve_json.main(["123", "--config", str(cfg_path)])
+        assert rc == 0
+        record = json.loads(capsys.readouterr().out)
+        assert record["CNA_private"]["state"] == "REVIEW"
+
     def test_review_and_draft_are_mutually_exclusive(self):
         # argparse raises SystemExit on mutually-exclusive group conflict;
         # the exit code is 2.

Reply via email to