From 1534a1a91987830f567ba67d1a5348891c35b9a0 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:24:42 +0000 Subject: [PATCH] Bind Server native alias promotion to the exact tagged dispatch policy --- qualification/README.md | 8 +++ qualification/policy.json | 21 +++++++ scripts/qualification_policy.py | 13 +++- tests/test_qualification_policy.py | 99 ++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) diff --git a/qualification/README.md b/qualification/README.md index 5ca1076..7c844c5 100644 --- a/qualification/README.md +++ b/qualification/README.md @@ -45,6 +45,14 @@ unconditional, unprivileged plan job and carries the exact condition, so a release runs from its own tag ref and a wrong ref fails in the plan job. The workflow may have no other trigger. +An explicitly optional binding can be absent; every present job still satisfies +its full trust binding. `optional_jobs` allows only named, registered jobs to be +absent. These presence exceptions support the Server alias-promotion rollout: +accepted Server retains its legacy release alias job while the successor moves +that job into a separate protected workflow. Tighten the bindings after Server +accepts the successor. Neither option admits an unregistered privileged job or +relaxes ref, environment, plan verifier or failed-plan checks. + Product repositories own their tests, documentation deployment, publication, and deployment workflows. Editorial wording is outside this policy. diff --git a/qualification/policy.json b/qualification/policy.json index 3f75e12..63398ca 100644 --- a/qualification/policy.json +++ b/qualification/policy.json @@ -164,6 +164,27 @@ "run": "scripts/ci/verify-native-release-dispatch.sh" }, "tag_input": "tag", + "tag_ref_condition": "github.ref == format('refs/tags/{0}', inputs.tag)", + "optional_jobs": [ + "aliases" + ] + }, + "server/native-alias-promotion.yml": { + "environment": "dockerhub-native", + "jobs": [ + "promote" + ], + "optional": true, + "plan_job": "plan", + "plan_verifier": { + "env": { + "GH_TOKEN": "${{ github.token }}", + "PROMOTION_EVIDENCE": "${{ inputs.evidence }}", + "PROMOTION_TAG": "${{ inputs.tag }}" + }, + "run": "python3 scripts/ci/native_alias_promote.py plan" + }, + "tag_input": "tag", "tag_ref_condition": "github.ref == format('refs/tags/{0}', inputs.tag)" } }, diff --git a/scripts/qualification_policy.py b/scripts/qualification_policy.py index 1ffe13b..8e6df97 100644 --- a/scripts/qualification_policy.py +++ b/scripts/qualification_policy.py @@ -1723,7 +1723,8 @@ def _validate_tag_dispatch_bindings(policy: dict[str, Any], bindings: object) -> raise PolicyError(f"invalid privileged tag dispatch workflow path {path!r}") if path.split("/", 1)[0] not in policy.get("targets", {}): raise PolicyError(f"{path} names an unknown workflow trust target") - if not isinstance(binding, dict) or set(binding) != TAG_DISPATCH_BINDING_FIELDS: + if (not isinstance(binding, dict) or not TAG_DISPATCH_BINDING_FIELDS <= set(binding) + or set(binding) - TAG_DISPATCH_BINDING_FIELDS - {"optional", "optional_jobs"}): raise PolicyError(f"{path} must declare the complete tag dispatch trust binding") jobs = binding["jobs"] if ( @@ -1733,6 +1734,13 @@ def _validate_tag_dispatch_bindings(policy: dict[str, Any], bindings: object) -> or jobs != sorted(set(jobs)) ): raise PolicyError(f"{path} tag dispatch jobs must be a sorted list of unique job names") + if type(binding.get("optional", False)) is not bool: + raise PolicyError(f"{path} optional workflow presence must be boolean") + optional_jobs = binding.get("optional_jobs", []) + if (not isinstance(optional_jobs, list) + or not all(isinstance(job, str) and job in jobs for job in optional_jobs) + or optional_jobs != sorted(set(optional_jobs))): + raise PolicyError(f"{path} optional jobs must be a sorted unique subset of bound jobs") plan_job = binding["plan_job"] if not isinstance(plan_job, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", plan_job) or plan_job in jobs: raise PolicyError(f"{path} tag dispatch plan job must be a separate job name") @@ -1856,7 +1864,7 @@ def _check_tag_dispatch_workflow( job = jobs.get(job_name) if isinstance(job, dict) and "continue-on-error" in job: raise PolicyError(f"{label} tag dispatch job {job_name!r} must not continue on error") - missing = sorted(set(binding["jobs"]) - set(jobs)) + missing = sorted(set(binding["jobs"]) - set(binding.get("optional_jobs", [])) - set(jobs)) if missing: raise PolicyError(f"{label} is missing policy-bound tag dispatch jobs {missing}") @@ -2133,6 +2141,7 @@ def scan_workflow_sources( key.rsplit("/", 1)[1] for key in policy["workflow_trust"]["privileged_tag_dispatch_workflows"] if key.startswith(f"{target_name}/") + and not policy["workflow_trust"]["privileged_tag_dispatch_workflows"][key].get("optional", False) } missing_tag_dispatch = expected_tag_dispatch - present if missing_tag_dispatch: diff --git a/tests/test_qualification_policy.py b/tests/test_qualification_policy.py index afd0fff..703bdce 100644 --- a/tests/test_qualification_policy.py +++ b/tests/test_qualification_policy.py @@ -877,5 +877,104 @@ def test_policy_refuses_a_loosened_or_incomplete_binding(self) -> None: validate_policy(policy) + +class NativePromotionTransitionTest(unittest.TestCase): + """Presence exceptions cannot relax any present workflow's trust binding.""" + + def setUp(self) -> None: + self.policy = policy_fixture() + self.path = ".github/workflows/native-alias-promotion.yml" + binding = self.policy["workflow_trust"]["privileged_tag_dispatch_workflows"]["server/native-alias-promotion.yml"] + env = "\n".join(f" {name}: {value}" for name, value in binding["plan_verifier"]["env"].items()) + self.promotion = f"""name: Native alias promotion +on: + workflow_dispatch: + inputs: + tag: + required: true + type: string + evidence: + required: true + type: string +permissions: {{}} +jobs: + plan: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: +{CHECKOUT_STEP} - name: Validate dispatch + env: +{env} + run: python3 scripts/ci/native_alias_promote.py plan + promote: + needs: plan + if: {TAG_GATE} + runs-on: ubuntu-latest + timeout-minutes: 5 + environment: dockerhub-native + permissions: + packages: write + steps: + - run: echo synthetic-promotion +""" + + def sources(self, promotion: str | None, legacy_aliases: bool = True) -> dict[str, str]: + release = tag_dispatch_workflow() + if not legacy_aliases: + release = release.split(" aliases:\n", 1)[0] + sources = {".github/workflows/native-release.yml": release} + if promotion is not None: + sources[self.path] = promotion + return sources + + def test_accepted_legacy_and_successor_topologies_share_one_policy(self) -> None: + for promotion, legacy in ((None, True), (self.promotion, True), (self.promotion, False)): + with self.subTest(promotion=promotion is not None, legacy=legacy): + evidence = scan_workflow_sources(self.policy, "server", self.sources(promotion, legacy)) + if promotion is not None: + self.assertEqual(evidence[self.path]["privileged_jobs"], ["promote"]) + + def test_optional_workflow_still_requires_exact_plan_ref_environment_and_jobs_when_present(self) -> None: + for before, after in ( + (f"if: {TAG_GATE}", "if: github.ref == 'refs/heads/main' || true"), + ("needs: plan", "needs: []"), + ("environment: dockerhub-native", "environment: other"), + ("native_alias_promote.py plan", "native_alias_promote.py promote"), + (" promote:\n", " unregistered:\n"), + (" plan:\n", " plan:\n if: false\n"), + ): + source = self.promotion.replace(before, after) + with self.subTest(after=after), self.assertRaises(PolicyError): + scan_workflow_sources(self.policy, "server", self.sources(source, False)) + + def test_optional_legacy_alias_job_is_fully_checked_if_present(self) -> None: + sources = self.sources(None) + path = ".github/workflows/native-release.yml" + sources[path] = sources[path].replace(" aliases:\n", " aliases:\n continue-on-error: true\n") + with self.assertRaisesRegex(PolicyError, "must not continue on error"): + scan_workflow_sources(self.policy, "server", sources) + + def test_presence_exception_cannot_name_unregistered_jobs_or_nonboolean_flags(self) -> None: + for field, value in (("optional", "true"), ("optional", 1), ("optional_jobs", ["unknown"]), + ("optional_jobs", ["aliases", "aliases"]), ("optional_jobs", "aliases")): + policy = copy.deepcopy(self.policy) + policy["workflow_trust"]["privileged_tag_dispatch_workflows"]["server/native-release.yml"][field] = value + with self.subTest(field=field, value=value), self.assertRaises(PolicyError): + validate_policy(policy) + + def test_tightened_post_transition_policy_requires_promotion_and_refuses_legacy_aliases(self) -> None: + policy = copy.deepcopy(self.policy) + bindings = policy["workflow_trust"]["privileged_tag_dispatch_workflows"] + del bindings["server/native-alias-promotion.yml"]["optional"] + del bindings["server/native-release.yml"]["optional_jobs"] + bindings["server/native-release.yml"]["jobs"].remove("aliases") + scan_workflow_sources(policy, "server", self.sources(self.promotion, False)) + for promotion, legacy in ((None, False), (self.promotion, True)): + with self.subTest(promotion=promotion is not None, legacy=legacy), self.assertRaises(PolicyError): + scan_workflow_sources(policy, "server", self.sources(promotion, legacy)) + + if __name__ == "__main__": unittest.main()