Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions qualification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
21 changes: 21 additions & 0 deletions qualification/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
},
Expand Down
13 changes: 11 additions & 2 deletions scripts/qualification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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")
Expand Down Expand Up @@ -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}")

Expand Down Expand Up @@ -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:
Expand Down
99 changes: 99 additions & 0 deletions tests/test_qualification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,5 +877,104 @@
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):

Check warning on line 949 in tests/test_qualification_policy.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_.github&issues=AaGrdZJ5oanhakbbM3Ql&open=AaGrdZJ5oanhakbbM3Ql&pullRequest=294
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):

Check warning on line 975 in tests/test_qualification_policy.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_.github&issues=AaGrdZJ5oanhakbbM3Qm&open=AaGrdZJ5oanhakbbM3Qm&pullRequest=294
scan_workflow_sources(policy, "server", self.sources(promotion, legacy))


if __name__ == "__main__":
unittest.main()
Loading