From f9569426f509867764a7e9b0df61ba5148ad660e Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:03:02 +0100 Subject: [PATCH] fix(release): publish qualified tags after optional skipped checks Use an explicit status guard while requiring successful preparation and qualification. Model the beta skipped-ancestor path and adverse direct prerequisites; retain immutable identity and all release gates. Change-source: pulse-maintainer --- .github/workflows/create-release.yml | 4 +++- .../subsystems/deployment-installability.md | 15 +++++++++++++++ .../release_promotion_policy_test.py | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 5373052731..bbc826eb1e 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1416,7 +1416,9 @@ jobs: needs: - prepare - candidate_qualification - if: ${{ needs.candidate_qualification.result == 'success' }} + # Beta qualification deliberately permits skipped integration ancestors. + # Override implicit success(), but never bypass either direct prerequisite. + if: ${{ always() && needs.prepare.result == 'success' && needs.candidate_qualification.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 54f50653c0..fcb4c09008 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -5464,3 +5464,18 @@ draft shell with recording fake APIs. It must reject changed/unknown/branch historical targets before any API mutation, admit same-identity recovery and private replacement, and preserve activated/published refusals. This is local workflow proof, not publication or installed acceptance. + +### Qualified publication after optional skipped checks + +The public tag writer must use an explicit status function and require both +preparation and candidate qualification to succeed. Beta policy can intentionally +skip integration tests upstream of qualification; GitHub's implicit `success()` +must not suppress tag publication after the qualification join accepts that +skip. Failure, cancellation or skipping of either direct prerequisite must still +block publication. This changes no qualification requirement or immutable identity. + +Regression: `CandidatePublicationBoundaryTest.test_qualified_beta_tag_survives_intentionally_skipped_ancestor` +in `scripts/release_control/release_promotion_policy_test.py` models the skipped +ancestor and all adverse direct-prerequisite outcomes. Existing writer tests retain +the candidate-failure and draft barriers. Source validation is not hosted recovery +or evidence that a previously frozen workflow has changed. diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 418e9db2f9..1f7f5ecbcf 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -2907,6 +2907,21 @@ class CandidatePublicationBoundaryTest(unittest.TestCase): expression = re.sub(r"!(?!=)", "not ", expression) return bool(eval(expression, {"__builtins__": {}, "startsWith": lambda value, prefix: value.startswith(prefix)})) + def test_qualified_beta_tag_survives_intentionally_skipped_ancestor(self) -> None: + outcomes = dict.fromkeys(self.jobs, "success") + outcomes["integration_tests"] = "skipped" + self.assertTrue(self.condition("candidate_qualification", outcomes)) + # Actions applies implicit success() when no status function is present. + # A skipped ancestor therefore prevents the writer despite the explicit + # qualification result. Model that status gate as well as its expression. + writer = self.jobs["publish_release_tag"] + has_status = bool(re.search(r"\b(always|success|failure|cancelled)\(", writer["if"])) + self.assertTrue(has_status and self.condition("publish_release_tag", outcomes)) + for dependency in writer["needs"]: + for state in ("failure", "cancelled", "skipped"): + with self.subTest(dependency=dependency, state=state): + self.assertFalse(self.condition("publish_release_tag", outcomes | {dependency: state})) + def test_failed_candidate_cannot_reach_any_public_version_writer(self) -> None: required = { "prepare", "publication_trust_preflight", "build_release_candidate",