diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 8d2d72b97..6d221cada 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1932,6 +1932,14 @@ artifact-selection behaviour. workflow-run details from GitHub and poll the exact returned run ID; it must never infer its child from the newest matching workflow/branch/timestamp, because version-scoped release concurrency and manual dispatches can overlap. + Before it creates the unpublished draft, `create-release.yml` must prove the + private payload can resolve its source: the `prepare` job must read + `docs/release-source-pairs/.json` from + `rcourtman/pulse-enterprise` and fail unless that file exists and declares + `pulse_sha` equal to the frozen public commit. A missing or mismatched + declaration must stop the run before any draft release object exists, so a + private build that cannot resolve its pair never orphans another draft. The + check is a fact check only; the release steward still selects the pair. Only after public release asset validation, staged install smoke, exact public Docker publication, exact Helm OCI publication, durable convergence dispatch, and the publicly readable activation-commit marker may the diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 09df57eba..0558f5b9a 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -2895,6 +2895,26 @@ class CandidatePublicationBoundaryTest(unittest.TestCase): def setUp(self) -> None: self.jobs = yaml.load(read(".github/workflows/create-release.yml"), Loader=UniqueKeyLoader)["jobs"] + def test_private_pro_pair_is_verified_before_any_draft_exists(self) -> None: + prepare_steps = self.jobs["prepare"]["steps"] + step = next( + (candidate for candidate in prepare_steps + if candidate.get("name") == "Verify the private Pro source pair is declared"), + None, + ) + self.assertIsNotNone(step, "prepare must verify the private Pro source pair before dispatch") + condition = step["if"] + self.assertIn("startsWith(github.event.inputs.version, '6.')", condition) + self.assertIn("historical_asset_backfill_only != 'true'", condition) + self.assertIn("draft_only != 'true'", condition) + self.assertEqual(step["env"]["PRIVATE_PAIR_REPOSITORY"], "rcourtman/pulse-enterprise") + script = step["run"] + self.assertIn("docs/release-source-pairs/${EXPECTED_SOURCE_SHA}.json", script) + self.assertIn("pulse_sha", script) + self.assertIn("exit 1", script) + # The draft is created only after the prepare gate can refuse the run. + self.assertIn("prepare", self.jobs["create_release"]["needs"]) + def condition(self, job: str, results: dict[str, str], *, draft: bool = False, cancelled: bool = False) -> bool: # Execute the workflow's Boolean condition with explicit job outcomes. # This intentionally fails if the workflow adds an unsupported expression.