Skip to content
Merged

Dev #13

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
24 changes: 18 additions & 6 deletions .github/scripts/policy_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir>
Subcommands: lint | fmt | render-md | check-md | hash | version | mode | stage <dir>
Pure stdlib. Prints ::error:: lines so failures show up in Actions logs.
"""
import argparse
Expand All @@ -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")
Expand Down Expand Up @@ -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()):
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -197,4 +209,4 @@ def main() -> None:


if __name__ == "__main__":
main()
main()
40 changes: 27 additions & 13 deletions .github/workflows/release-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)"
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)"
72 changes: 72 additions & 0 deletions policy/POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Policy legacy-2026-09

<!-- Generated from policy/policy.json by policy_tool.py render-md. Do not edit. -->

- 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 <wheel>.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.
71 changes: 71 additions & 0 deletions policy/policy.json
Original file line number Diff line number Diff line change
@@ -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 <wheel>.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."
}
Loading