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
This commit is contained in:
pulse-triage[bot]
2026-09-13 13:03:02 +01:00
parent 1c289e746a
commit f9569426f5
3 changed files with 33 additions and 1 deletions
+3 -1
View File
@@ -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:
@@ -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.
@@ -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",