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.