From 6b71e52360b50f0a92c757c8e87d95ddd32609cf Mon Sep 17 00:00:00 2001 From: Patrick Ryan Date: Wed, 30 Sep 2026 19:37:29 -0500 Subject: [PATCH 1/2] docs(policy): add legacy-2026-09 descriptive policy Historical description of what releases under the 2026-09-sha-pinned-v1 label actually did. Descriptive only; not an enforcement contract. Authoritative copy is the policy-legacy-2026-09 release asset. --- policy/POLICY.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ policy/policy.json | 71 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 policy/POLICY.md create mode 100644 policy/policy.json diff --git a/policy/POLICY.md b/policy/POLICY.md new file mode 100644 index 0000000..99ed798 --- /dev/null +++ b/policy/POLICY.md @@ -0,0 +1,72 @@ +# Policy legacy-2026-09 + + + +- Enforcement mode: **descriptive** +- Legacy aliases: `2026-09-sha-pinned-v1` +- Hash: sha256 of the exact `policy.json` bytes, published as `policy.json.sha256` and as the release asset digest. This file cannot contain its own hash. + +Describes what releases built under the legacy label 2026-09-sha-pinned-v1 actually did. Historical and descriptive only: no verifier loaded this file or failed a release against it, builds do not consume it, and it is not an enforcement contract or evidence that any policy was enforced. Written after those releases; they reference the label, not this file's hash. The first enforcing policy is v1. + +## Rules + +### SRC-1 - Pinned upstream commit, clean tree + +- Status: `performed-at-build` +- Implemented by: checkout_verify.py (musllinux); inline pwsh steps (win-arm64) + +The upstream git tag is resolved to a commit, checked out detached at that exact commit, and the working tree is verified clean before building. The build fails if the check fails. The result is recorded as upstream_commit and tree_clean in the signed upstream-source attestation. + +### SRC-2 - Source snapshot archive + +- Status: `performed-at-build` +- Implemented by: checkout_verify.py (musllinux); inline pwsh steps (win-arm64) + +A git archive of the pinned commit is created, its sha256 is recorded in the upstream-source attestation as snapshot_archive_sha256, and the archive is attached to the release. + +### POL-1 - Policy label recorded + +- Status: `performed-at-build` +- Implemented by: POLICY_VERSION env in both build workflows; checkout_verify.py (musllinux); inline pwsh (win-arm64) + +The build writes the label 2026-09-sha-pinned-v1 into the upstream-source attestation as policy_version. It is a label only: no hash and no rule list were attached to it. + +### ATT-1 - Two attestations per wheel, verified with gh before publish + +- Status: `verified-at-release` +- Implemented by: release_manager.py (musllinux); inline release job (win-arm64) + +Each wheel has a build-provenance attestation and an upstream-source attestation. Before publishing, each was verified with GitHub's attestation verification against the expected repository and calling workflow. The historical release path did not additionally constrain the attestation's source ref, workflow commit, runner type, or predicate contents. + +### ATT-2 - Attestation bundles attached + +- Status: `verified-at-release` +- Implemented by: release_manager.py (musllinux); inline release job (win-arm64) + +Each wheel's attestation bundle is attached to the release as .attestations.jsonl, and the bundles are checked to be distinct per wheel. + +### REL-1 - Immutable release and exact verified asset set + +- Status: `verified-at-release` +- Implemented by: release_manager.py release_transaction, choose_tag (musllinux); inline release job (win-arm64); GitHub immutable releases + +The release job publishes only the verified asset set and does not overwrite an existing release with a different asset set (musllinux releases a changed wheel set under a new .postN tag; win-arm64 refuses the rerun). Published releases are immutable. + +### ACT-1 - Actions pinned to commit SHAs + +- Status: `declared` +- Implemented by: workflow authoring + +When this policy was written, every GitHub Action `uses:` in both build workflows and the shared release workflow was pinned to a full commit SHA. This is a convention: no automated check enforced it, and it was not re-verified for the workflow commit that built each earlier release. + +## Not guaranteed + +- No policy file was loaded or enforced. The label 2026-09-sha-pinned-v1 carries no hash and no rule list; this v1 text was written afterwards to describe the behaviour above. +- The release job did not read attestation predicate contents: upstream_commit, tree_clean and policy_version were not compared to release inputs. +- Attestations were verified by repository and signer workflow only. Source ref, workflow commit and runner type were not constrained. +- The source archive's sha256 was not compared to the digest in the attestation at release time. +- Release tags were not required to point at the exact build commit. win-arm64 created its tag without a target, so it lands on the default-branch tip at publish time; dbt-oss-v2.0.5.post1's tag and build commits differ. +- Release tags are lightweight and unsigned. A verified-signature badge on a tagged commit can be GitHub's own signature for a commit created on GitHub.com; it does not sign the tag and does not identify the author. +- Dependencies of a wheel are not verified; only the exact wheel is covered. +- Wheels are not claimed to be bit-for-bit reproducible. +- Commit signing and signer-identity checks protect promotion into main and are not part of this policy. \ No newline at end of file diff --git a/policy/policy.json b/policy/policy.json new file mode 100644 index 0000000..e653d5c --- /dev/null +++ b/policy/policy.json @@ -0,0 +1,71 @@ +{ + "enforcement_mode": "descriptive", + "legacy_aliases": [ + "2026-09-sha-pinned-v1" + ], + "not_guaranteed": [ + "No policy file was loaded or enforced. The label 2026-09-sha-pinned-v1 carries no hash and no rule list; this v1 text was written afterwards to describe the behaviour above.", + "The release job did not read attestation predicate contents: upstream_commit, tree_clean and policy_version were not compared to release inputs.", + "Attestations were verified by repository and signer workflow only. Source ref, workflow commit and runner type were not constrained.", + "The source archive's sha256 was not compared to the digest in the attestation at release time.", + "Release tags were not required to point at the exact build commit. win-arm64 created its tag without a target, so it lands on the default-branch tip at publish time; dbt-oss-v2.0.5.post1's tag and build commits differ.", + "Release tags are lightweight and unsigned. A verified-signature badge on a tagged commit can be GitHub's own signature for a commit created on GitHub.com; it does not sign the tag and does not identify the author.", + "Dependencies of a wheel are not verified; only the exact wheel is covered.", + "Wheels are not claimed to be bit-for-bit reproducible.", + "Commit signing and signer-identity checks protect promotion into main and are not part of this policy." + ], + "policy_version": "legacy-2026-09", + "rules": [ + { + "id": "SRC-1", + "implemented_by": "checkout_verify.py (musllinux); inline pwsh steps (win-arm64)", + "statement": "The upstream git tag is resolved to a commit, checked out detached at that exact commit, and the working tree is verified clean before building. The build fails if the check fails. The result is recorded as upstream_commit and tree_clean in the signed upstream-source attestation.", + "status": "performed-at-build", + "title": "Pinned upstream commit, clean tree" + }, + { + "id": "SRC-2", + "implemented_by": "checkout_verify.py (musllinux); inline pwsh steps (win-arm64)", + "statement": "A git archive of the pinned commit is created, its sha256 is recorded in the upstream-source attestation as snapshot_archive_sha256, and the archive is attached to the release.", + "status": "performed-at-build", + "title": "Source snapshot archive" + }, + { + "id": "POL-1", + "implemented_by": "POLICY_VERSION env in both build workflows; checkout_verify.py (musllinux); inline pwsh (win-arm64)", + "statement": "The build writes the label 2026-09-sha-pinned-v1 into the upstream-source attestation as policy_version. It is a label only: no hash and no rule list were attached to it.", + "status": "performed-at-build", + "title": "Policy label recorded" + }, + { + "id": "ATT-1", + "implemented_by": "release_manager.py (musllinux); inline release job (win-arm64)", + "statement": "Each wheel has a build-provenance attestation and an upstream-source attestation. Before publishing, each was verified with GitHub's attestation verification against the expected repository and calling workflow. The historical release path did not additionally constrain the attestation's source ref, workflow commit, runner type, or predicate contents.", + "status": "verified-at-release", + "title": "Two attestations per wheel, verified with gh before publish" + }, + { + "id": "ATT-2", + "implemented_by": "release_manager.py (musllinux); inline release job (win-arm64)", + "statement": "Each wheel's attestation bundle is attached to the release as .attestations.jsonl, and the bundles are checked to be distinct per wheel.", + "status": "verified-at-release", + "title": "Attestation bundles attached" + }, + { + "id": "REL-1", + "implemented_by": "release_manager.py release_transaction, choose_tag (musllinux); inline release job (win-arm64); GitHub immutable releases", + "statement": "The release job publishes only the verified asset set and does not overwrite an existing release with a different asset set (musllinux releases a changed wheel set under a new .postN tag; win-arm64 refuses the rerun). Published releases are immutable.", + "status": "verified-at-release", + "title": "Immutable release and exact verified asset set" + }, + { + "id": "ACT-1", + "implemented_by": "workflow authoring", + "statement": "When this policy was written, every GitHub Action `uses:` in both build workflows and the shared release workflow was pinned to a full commit SHA. This is a convention: no automated check enforced it, and it was not re-verified for the workflow commit that built each earlier release.", + "status": "declared", + "title": "Actions pinned to commit SHAs" + } + ], + "schema": 1, + "summary": "Describes what releases built under the legacy label 2026-09-sha-pinned-v1 actually did. Historical and descriptive only: no verifier loaded this file or failed a release against it, builds do not consume it, and it is not an enforcement contract or evidence that any policy was enforced. Written after those releases; they reference the label, not this file's hash. The first enforcing policy is v1." +} \ No newline at end of file From 8a6ab2a7b42054c3adcf444f058f19082664c731 Mon Sep 17 00:00:00 2001 From: Patrick Ryan Date: Wed, 30 Sep 2026 19:41:33 -0500 Subject: [PATCH 2/2] fix(policy): final legacy-2026-09 policy, tool and workflow --- .github/scripts/policy_tool.py | 24 ++++++++++++----- .github/workflows/release-policy.yml | 40 +++++++++++++++++++--------- policy/POLICY.md | 2 +- policy/policy.json | 2 +- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/.github/scripts/policy_tool.py b/.github/scripts/policy_tool.py index 01d043a..a6a4e6b 100644 --- a/.github/scripts/policy_tool.py +++ b/.github/scripts/policy_tool.py @@ -9,7 +9,7 @@ POLICY.md is generated from policy.json; `check-md` fails on drift. -Subcommands: lint | fmt | render-md | check-md | hash | version | stage +Subcommands: lint | fmt | render-md | check-md | hash | version | mode | stage Pure stdlib. Prints ::error:: lines so failures show up in Actions logs. """ import argparse @@ -20,7 +20,8 @@ import sys from pathlib import Path -VERSION_RE = re.compile(r"^v[1-9][0-9]*$") +ENFORCED_VERSION_RE = re.compile(r"^v[1-9][0-9]*$") +LEGACY_VERSION_RE = re.compile(r"^legacy-[0-9]{4}-(0[1-9]|1[0-2])$") RULE_ID_RE = re.compile(r"^[A-Z]{3,4}-[0-9]+$") MODES = ("descriptive", "enforced") STATUSES = ("performed-at-build", "verified-at-release", "declared", "enforced") @@ -64,10 +65,15 @@ def validate(obj) -> list[str]: return errs if obj["schema"] != 1: errs.append("schema must be 1") - if not (isinstance(obj["policy_version"], str) and VERSION_RE.match(obj["policy_version"])): - errs.append("policy_version must look like v1, v2, ...") if obj["enforcement_mode"] not in MODES: errs.append(f"enforcement_mode must be one of {MODES}") + ver = obj["policy_version"] + if not isinstance(ver, str): + errs.append("policy_version must be a string") + elif obj["enforcement_mode"] == "enforced" and not ENFORCED_VERSION_RE.match(ver): + errs.append("enforced policies must be versioned v1, v2, ...") + elif obj["enforcement_mode"] == "descriptive" and not LEGACY_VERSION_RE.match(ver): + errs.append("descriptive policies must be versioned legacy-YYYY-MM (v1, v2, ... are reserved for enforced policies)") if not (isinstance(obj["legacy_aliases"], list) and all(isinstance(a, str) for a in obj["legacy_aliases"])): errs.append("legacy_aliases must be a list of strings") if not (isinstance(obj["summary"], str) and obj["summary"].strip()): @@ -170,6 +176,11 @@ def cmd_version(a) -> None: print(obj["policy_version"]) +def cmd_mode(a) -> None: + _, obj = load(a.policy) + print(obj["enforcement_mode"]) + + def cmd_stage(a) -> None: cmd_lint(a) cmd_check_md(a) @@ -187,7 +198,8 @@ def main() -> None: p.add_argument("--md", type=Path, default=Path("policy/POLICY.md")) sub = p.add_subparsers(dest="cmd", required=True) for name, fn in (("lint", cmd_lint), ("fmt", cmd_fmt), ("render-md", cmd_render_md), - ("check-md", cmd_check_md), ("hash", cmd_hash), ("version", cmd_version)): + ("check-md", cmd_check_md), ("hash", cmd_hash), ("version", cmd_version), + ("mode", cmd_mode)): sub.add_parser(name).set_defaults(fn=fn) s = sub.add_parser("stage") s.add_argument("outdir", type=Path) @@ -197,4 +209,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/.github/workflows/release-policy.yml b/.github/workflows/release-policy.yml index 2fa4c46..7bf92c8 100644 --- a/.github/workflows/release-policy.yml +++ b/.github/workflows/release-policy.yml @@ -4,15 +4,17 @@ name: release-policy # Manual only, from main, behind the `policy-release` environment (create it # in repo Settings -> Environments with required reviewers). Gitsign on the # commit says who wrote the policy; this release says it is the frozen rule -# set. Nothing here publishes automatically. +# set. Nothing here publishes automatically. Descriptive (legacy-*) policies +# are published without a provenance attestation so they cannot be mistaken +# for an enforcement contract; enforced (vN) policies are attested. on: workflow_dispatch: inputs: policy_version: - description: "Policy version to freeze; must equal policy_version in policy/policy.json (e.g. v1)" + description: "Policy version to freeze; must equal policy_version in policy/policy.json (legacy-2026-09, or v1 for the first enforcing policy)" required: true - default: "v1" + default: "legacy-2026-09" permissions: contents: read @@ -72,14 +74,19 @@ jobs: python-version: "3.11" - name: Stage release assets - run: python3 .github/scripts/policy_tool.py stage policy-release + run: | + set -euo pipefail + python3 .github/scripts/policy_tool.py stage policy-release + echo "MODE=$(python3 .github/scripts/policy_tool.py mode)" >> "$GITHUB_ENV" - name: Attest build provenance (policy.json) + if: env.MODE == 'enforced' uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3 with: subject-path: policy-release/policy.json - name: Verify attestation and attach bundle + if: env.MODE == 'enforced' run: | set -euo pipefail signer="${GITHUB_REPOSITORY}/.github/workflows/release-policy.yml" @@ -100,14 +107,19 @@ jobs: run: | set -euo pipefail tag="policy-${WANT}" + files=(policy-release/policy.json policy-release/POLICY.md policy-release/policy.json.sha256) + want="POLICY.md,policy.json,policy.json.sha256" + if [ "$MODE" = "enforced" ]; then + files+=(policy-release/policy.json.attestations.jsonl) + want="POLICY.md,policy.json,policy.json.attestations.jsonl,policy.json.sha256" + notes="Frozen enforcing policy ${WANT} (sha256 in policy.json.sha256). Verify: gh attestation verify policy.json --repo ${GITHUB_REPOSITORY} --signer-workflow ${GITHUB_REPOSITORY}/.github/workflows/release-policy.yml" + else + notes="Historical description ${WANT} (sha256 in policy.json.sha256). Describes what earlier releases did. Not consumed by builds, not an enforcement contract, and not evidence that any policy was enforced." + fi gh release create "$tag" --repo "$GITHUB_REPOSITORY" --draft --target "$GITHUB_SHA" \ - --title "Policy ${WANT}" \ - --notes "Frozen policy ${WANT} (sha256 in policy.json.sha256). Verify: gh attestation verify policy.json --repo ${GITHUB_REPOSITORY} --signer-workflow ${GITHUB_REPOSITORY}/.github/workflows/release-policy.yml" - gh release upload "$tag" --repo "$GITHUB_REPOSITORY" \ - policy-release/policy.json policy-release/POLICY.md \ - policy-release/policy.json.sha256 policy-release/policy.json.attestations.jsonl + --title "Policy ${WANT}" --notes "$notes" + gh release upload "$tag" --repo "$GITHUB_REPOSITORY" "${files[@]}" got="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '[.assets[].name] | sort | join(",")')" - want="POLICY.md,policy.json,policy.json.attestations.jsonl,policy.json.sha256" if [ "$got" != "$want" ]; then echo "::error::asset set mismatch: got '$got', want '$want' - not publishing" exit 1 @@ -128,6 +140,8 @@ jobs: cmp published/policy.json policy-release/policy.json cmp published/POLICY.md policy-release/POLICY.md (cd published && sha256sum -c policy.json.sha256) - gh attestation verify published/policy.json --repo "$GITHUB_REPOSITORY" \ - --signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release-policy.yml" - echo "Published $tag: $(sha256sum published/policy.json | cut -d' ' -f1)" \ No newline at end of file + if [ "$MODE" = "enforced" ]; then + gh attestation verify published/policy.json --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release-policy.yml" + fi + echo "Published $tag: $(sha256sum published/policy.json | cut -d' ' -f1)" diff --git a/policy/POLICY.md b/policy/POLICY.md index 99ed798..613522d 100644 --- a/policy/POLICY.md +++ b/policy/POLICY.md @@ -69,4 +69,4 @@ When this policy was written, every GitHub Action `uses:` in both build workflow - Release tags are lightweight and unsigned. A verified-signature badge on a tagged commit can be GitHub's own signature for a commit created on GitHub.com; it does not sign the tag and does not identify the author. - Dependencies of a wheel are not verified; only the exact wheel is covered. - Wheels are not claimed to be bit-for-bit reproducible. -- Commit signing and signer-identity checks protect promotion into main and are not part of this policy. \ No newline at end of file +- Commit signing and signer-identity checks protect promotion into main and are not part of this policy. diff --git a/policy/policy.json b/policy/policy.json index e653d5c..2c6a90c 100644 --- a/policy/policy.json +++ b/policy/policy.json @@ -68,4 +68,4 @@ ], "schema": 1, "summary": "Describes what releases built under the legacy label 2026-09-sha-pinned-v1 actually did. Historical and descriptive only: no verifier loaded this file or failed a release against it, builds do not consume it, and it is not an enforcement contract or evidence that any policy was enforced. Written after those releases; they reference the label, not this file's hash. The first enforcing policy is v1." -} \ No newline at end of file +}