From beb470740162e86832372623f2e474cff767b3b9 Mon Sep 17 00:00:00 2001 From: Joseph Magly <1159087+jmagly@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:28:30 -0400 Subject: [PATCH] ci: release tested source snapshots only --- .aiwg/aiwg.config | 10 +- .aiwg/bt6-maintainer.yaml | 6 +- .../config/repository-profile.schema.json | 6 +- .../skills/bt6-release-readiness/SKILL.md | 10 +- .../skills/bt6-release-validation/SKILL.md | 11 +- .../templates/bt6-release-readiness-report.md | 2 +- .../bt6-release-validation-report.md | 2 +- .../templates/bt6-repository-profile.yaml | 3 +- .github/workflows/ci.yml | 150 ++++-------------- WORKSPACE.md | 22 +-- docs/SUPPLY_CHAIN_POLICY.md | 24 +-- scripts/check_supply_chain_policy.py | 14 +- tests/test_aiwg_workspace_contracts.py | 3 +- tests/test_ci_policy.py | 10 +- tests/test_supply_chain_policy.py | 12 +- 15 files changed, 104 insertions(+), 181 deletions(-) diff --git a/.aiwg/aiwg.config b/.aiwg/aiwg.config index 74aa68c..8948c07 100644 --- a/.aiwg/aiwg.config +++ b/.aiwg/aiwg.config @@ -49,7 +49,7 @@ "bt6-maintainer": { "version": "0.3.0", "source": "project-local", - "installedAt": "2026-08-22T14:56:47.190Z", + "installedAt": "2026-08-22T15:28:11.760Z", "deployedTo": { "codex": { "agents": 5, @@ -74,8 +74,8 @@ "skills/bt6-pr-audit/SKILL.md": "38be15ba6c6c12f958c7ced0bf6152654dc6df53ddc17156fda84083ae990ea9", "skills/bt6-provider-review/SKILL.md": "a95ac6d2be20847626bb94e5038358148d4a19c25e00ade6049526f8a16d0c15", "skills/bt6-queue-audit/SKILL.md": "aaa77f2e14b46a2f8329025923ed54745152cea6896c947580f7a1babff81312", - "skills/bt6-release-readiness/SKILL.md": "b51881662f27604c518c67e009187b3b0bad1d0755116efae1b0d1505dbb3dfe", - "skills/bt6-release-validation/SKILL.md": "3ecf342ad12cd1eed2f38ed8eb5b7fb4e396a4e25c79b54e5f649eef0c10c866" + "skills/bt6-release-readiness/SKILL.md": "26c65c1f64ce29f947be8a90987489a499814c360f4789ed80ef35f711c3d4e0", + "skills/bt6-release-validation/SKILL.md": "9c0dfe72690bdcae273624be730751951b42bab36b538989a8708c0fb48f76d1" }, "deployedArtifactHashes": { "codex": { @@ -90,8 +90,8 @@ "skills/bt6-pr-audit/SKILL.md": "9a1e55bfd6972f3168f884d54cd6ede3673b541940b54352120ed4c0a4c49f2c", "skills/bt6-provider-review/SKILL.md": "2de8b37f546dda852bee88d023caf82877baab988aebc219fd83a784c2bf58a3", "skills/bt6-queue-audit/SKILL.md": "e0bdb36c8f8f85a2e9dff0180de2f502972bb8af9b7b13c4661be065354ef893", - "skills/bt6-release-readiness/SKILL.md": "e609a2f2197fd8c65239f563a0f8cc594ad0d420e4d18da5ba8362f26574ff52", - "skills/bt6-release-validation/SKILL.md": "942cd70729c5c13c384532214ebfff1763fd9374a85abda18925932b6dc79167" + "skills/bt6-release-readiness/SKILL.md": "db317a00f8f4198e93e0666a00af29ad645d4c3d50de2e42141d84d6e7c16eb1", + "skills/bt6-release-validation/SKILL.md": "e94720ea230a8206d7acf5b3c514c6bdf1b7f81865580b7b2c6df11472bdb384" } } } diff --git a/.aiwg/bt6-maintainer.yaml b/.aiwg/bt6-maintainer.yaml index 8e4b8a6..8fc3c73 100644 --- a/.aiwg/bt6-maintainer.yaml +++ b/.aiwg/bt6-maintainer.yaml @@ -28,7 +28,6 @@ validation: - "mkdir -p test-results && python -m pytest -m 'not slow and not gpu and not mps and not mlx and not network and not download and not remote and not operator_ui' --cov-branch --cov-fail-under=0 --cov-report=json:test-results/coverage-release.json" - "python scripts/check_coverage_thresholds.py test-results/coverage-release.json --min-line 75 --min-branch 60 --min-file obliteratus/device.py=70 --min-file obliteratus/models/loader.py=70 --min-file obliteratus/architecture_profiles.py=70 --min-file obliteratus/cli.py=70 --min-file obliteratus/mlx_backend.py=70 --min-file obliteratus/evaluation/metrics.py=70 --min-file obliteratus/evaluation/advanced_metrics.py=70 --min-file obliteratus/reporting/report.py=70 --min-file obliteratus/community.py=70 --min-file obliteratus/telemetry.py=70" - "python scripts/check_quality_policy.py --policy ci/test-quality-policy.json --coverage test-results/coverage-release.json && python scripts/check_conditional_policy.py && python scripts/check_test_risk_map.py" - - "python -m build --sdist --wheel" - "python -c 'import obliteratus; print(obliteratus.__version__)'" - "python -m obliteratus --help" qualityPolicy: @@ -40,12 +39,13 @@ validation: researchIntegrity: - "python -m pytest tests/test_advanced_metrics.py tests/test_breakthrough_modules.py tests/test_community.py" releaseEvidence: + artifactType: "source-zip" hashAlgorithm: "sha256" provenanceFormat: "slsa-v1" attestationFormat: "in-toto" signingMode: "sigstore-keyless" sbomFormat: "cyclonedx" - buildOnce: true + snapshotOnce: true verifyBeforePromotion: true riskSurfaces: - id: "model-loading" @@ -67,7 +67,7 @@ riskSurfaces: - id: "ci-supply-chain" paths: [".github/workflows/**", "ci/**", "docs/SUPPLY_CHAIN_POLICY.md", "pyproject.toml", "scripts/check_supply_chain_policy.py", "uv.lock"] concerns: ["workflow-permissions", "dependency-pinning", "untrusted-pull-request-code"] - requiredChecks: ["uv lock --check", "python -m pytest tests/test_ci_policy.py tests/test_supply_chain_policy.py", "python -m build --sdist --wheel"] + requiredChecks: ["uv lock --check", "python -m pytest tests/test_ci_policy.py tests/test_supply_chain_policy.py"] research: corpusPaths: ["obliteratus/prompts.py", "community_results/**"] evidencePaths: ["paper/**", "docs/**", "community_results/**"] diff --git a/.aiwg/plugins/bt6-maintainer/payload/config/repository-profile.schema.json b/.aiwg/plugins/bt6-maintainer/payload/config/repository-profile.schema.json index 6bcad15..0017673 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/config/repository-profile.schema.json +++ b/.aiwg/plugins/bt6-maintainer/payload/config/repository-profile.schema.json @@ -102,21 +102,23 @@ "type": "object", "additionalProperties": false, "required": [ + "artifactType", "hashAlgorithm", "provenanceFormat", "attestationFormat", "signingMode", "sbomFormat", - "buildOnce", + "snapshotOnce", "verifyBeforePromotion" ], "properties": { + "artifactType": { "const": "source-zip" }, "hashAlgorithm": { "const": "sha256" }, "provenanceFormat": { "const": "slsa-v1" }, "attestationFormat": { "const": "in-toto" }, "signingMode": { "enum": ["sigstore-keyless", "project-defined"] }, "sbomFormat": { "enum": ["cyclonedx", "spdx"] }, - "buildOnce": { "const": true }, + "snapshotOnce": { "const": true }, "verifyBeforePromotion": { "const": true } } }, diff --git a/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-readiness/SKILL.md b/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-readiness/SKILL.md index 838218b..5161bd1 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-readiness/SKILL.md +++ b/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-readiness/SKILL.md @@ -52,10 +52,12 @@ promote a tag. 5. Re-run focused checks after each repair, then re-run the complete applicable pre-tag gate. Stop when all locally executable hard gates pass or a required external/platform gate remains unresolved. -6. When `releaseEvidence` is configured, require one canonical SHA-256 manifest - for the build-once release set, a bound SBOM, SLSA/in-toto provenance, and - the configured signature mechanism. Downstream gates must consume the same - artifact bytes rather than rebuilding them. +6. When `releaseEvidence` is configured, require one deterministic source ZIP + of the tested commit, a canonical SHA-256 manifest, a bound source SBOM, + SLSA/in-toto provenance, and the configured signature mechanism. Do not + compile or publish installable packages unless a repository explicitly + overrides the source-snapshot policy. Downstream gates must consume the same + ZIP bytes rather than recreating them. ## Decision diff --git a/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-validation/SKILL.md b/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-validation/SKILL.md index fefebff..40ed919 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-validation/SKILL.md +++ b/.aiwg/plugins/bt6-maintainer/payload/skills/bt6-release-validation/SKILL.md @@ -45,9 +45,9 @@ and does not publish a release without separate explicit authorization. 1. Run every repository-profile `validation.full` command at the exact tagged commit. 2. Run applicable `documentation`, `researchIntegrity`, and risk-surface checks. -3. Run repository-defined packaging, artifact-integrity, compatibility, and - supported-platform checks. Verify generated artifacts come from the tagged - source rather than an unrelated checkout. +3. Run repository-defined source-snapshot integrity, compatibility, and + supported-platform checks. Verify the release ZIP comes from the tagged + source rather than an unrelated checkout; do not require a compiled package. 4. Compare local evidence with CI for the same tag and commit. Record missing or stale evidence as incomplete, not passing. 5. Treat warnings, flakes, skips, coverage changes, and conditional-gate gaps @@ -55,8 +55,9 @@ and does not publish a release without separate explicit authorization. turnaround budget as a release exemption. 6. When `releaseEvidence` is configured, verify every artifact against the canonical checksum manifest, authenticate its SLSA/in-toto attestation, - validate the SBOM binding, and prove promotion uses the tested build-once - bytes. A checksum without authenticated provenance is incomplete evidence. + validate the source-SBOM binding, and prove promotion uses the tested + snapshot-once ZIP bytes. A checksum without authenticated provenance is + incomplete evidence. ## Decision diff --git a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-readiness-report.md b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-readiness-report.md index cc3b56c..71672df 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-readiness-report.md +++ b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-readiness-report.md @@ -26,7 +26,7 @@ description: Pre-tag audit and hardening report for moving a candidate from the | Full validation | `` | `` | `` | | Coverage and regression | `` | `` | `` | | Quality depth | `` | `` | `` | -| Packaging and supply chain | `` | `` | `` | +| Source snapshot and supply chain | `` | `` | `` | | Conditional/platform checks | `` | `` | `` | ## Repairs diff --git a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-validation-report.md b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-validation-report.md index bc6c4ad..a467ac3 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-validation-report.md +++ b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-release-validation-report.md @@ -33,7 +33,7 @@ Profile: `` | Documentation | `` | `
` | | Research and provenance | `` | `
` | | Risk-surface checks | `` | `
` | -| Packaging and artifact integrity | `` | `
` | +| Source ZIP and artifact integrity | `` | `
` | | Supported platforms | `` | `
` | ## Decision diff --git a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-repository-profile.yaml b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-repository-profile.yaml index 2f309c5..2ea6f38 100644 --- a/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-repository-profile.yaml +++ b/.aiwg/plugins/bt6-maintainer/payload/templates/bt6-repository-profile.yaml @@ -35,12 +35,13 @@ validation: researchIntegrity: - "" releaseEvidence: + artifactType: "source-zip" hashAlgorithm: "sha256" provenanceFormat: "slsa-v1" attestationFormat: "in-toto" signingMode: "sigstore-keyless" sbomFormat: "cyclonedx" - buildOnce: true + snapshotOnce: true verifyBeforePromotion: true riskSurfaces: - id: "ingestion" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec1ebc6..228a166 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,54 +49,24 @@ jobs: pyproject.toml uv.lock - - name: Install build tooling + - name: Install release evidence tooling run: | python -m pip install "uv==${UV_VERSION}" UV_PROJECT_ENVIRONMENT="$BUILD_TOOLS" uv sync --locked --only-group ci - - name: Build source and wheel distributions - run: >- - "$BUILD_TOOLS/bin/python" -m build --sdist --wheel - - - name: Verify wheel contents and entry point + - name: Create deterministic tested-source snapshot run: | - python - <<'PY' - from pathlib import Path - from zipfile import ZipFile + mkdir -p release package-evidence + snapshot="release/OBLITERATUS-${GITHUB_SHA}.zip" + git archive \ + --format=zip \ + --prefix="OBLITERATUS-${GITHUB_SHA}/" \ + --output="$snapshot" \ + "$GITHUB_SHA" + test "$(find release -maxdepth 1 -type f -name '*.zip' | wc -l)" -eq 1 + unzip -t "$snapshot" | tee package-evidence/source-zip-check.txt - wheels = list(Path("dist").glob("*.whl")) - if len(wheels) != 1: - raise SystemExit(f"expected one wheel, found: {wheels}") - - with ZipFile(wheels[0]) as archive: - names = set(archive.namelist()) - required = { - "app.py", - "obliteratus/__init__.py", - "obliteratus/local_ui.py", - } - missing = sorted(required - names) - if missing: - raise SystemExit(f"wheel is missing required modules: {missing}") - - entry_points = [ - name for name in names if name.endswith(".dist-info/entry_points.txt") - ] - if len(entry_points) != 1: - raise SystemExit(f"expected one entry_points.txt, found: {entry_points}") - contents = archive.read(entry_points[0]).decode("utf-8") - if "obliteratus = obliteratus.cli:main" not in contents: - raise SystemExit("wheel is missing the obliteratus console entry point") - - print(f"verified wheel contents: {wheels[0]}") - PY - - - name: Validate distribution metadata - run: | - mkdir -p package-evidence - "$BUILD_TOOLS/bin/python" -m twine check dist/* | tee package-evidence/twine-check.txt - - - name: Generate and bind CycloneDX SBOM + - name: Generate and bind source CycloneDX SBOM run: | uv --preview-features sbom-export export \ --locked \ @@ -105,95 +75,33 @@ jobs: --no-dev \ --no-editable \ --output-file package-evidence/obliteratus.cdx.unbound.json - mapfile -t wheels < <(find dist -maxdepth 1 -type f -name '*.whl' -print) - if [ "${#wheels[@]}" -ne 1 ]; then - echo "expected exactly one wheel, found ${#wheels[@]}" + mapfile -t snapshots < <(find release -maxdepth 1 -type f -name '*.zip' -print) + if [ "${#snapshots[@]}" -ne 1 ]; then + echo "expected exactly one source snapshot, found ${#snapshots[@]}" exit 1 fi python scripts/check_supply_chain_policy.py sbom \ --input package-evidence/obliteratus.cdx.unbound.json \ - --wheel "${wheels[0]}" \ + --artifact "${snapshots[0]}" \ --output package-evidence/obliteratus.cdx.json rm package-evidence/obliteratus.cdx.unbound.json - name: Generate canonical release checksums run: >- - sha256sum dist/* package-evidence/obliteratus.cdx.json | + sha256sum release/*.zip package-evidence/obliteratus.cdx.json | tee package-evidence/SHA256SUMS - - name: Verify installed wheel contract - run: | - mapfile -t wheels < <(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f -name '*.whl' -print) - if [ "${#wheels[@]}" -ne 1 ]; then - echo "expected exactly one wheel, found ${#wheels[@]}" - exit 1 - fi - - wheel_env="$RUNNER_TEMP/obliteratus-wheel-env" - wheel_cwd="$RUNNER_TEMP/obliteratus-wheel-cwd" - python -m venv "$wheel_env" - mkdir -p "$wheel_cwd" - "$wheel_env/bin/python" -m pip install --no-cache-dir "rich==15.0.0" - "$wheel_env/bin/python" -m pip install --no-cache-dir --no-deps "${wheels[0]}" - - cd "$wheel_cwd" - "$wheel_env/bin/python" -I - <<'PY' | tee "$GITHUB_WORKSPACE/package-evidence/wheel-import.txt" - import importlib.metadata - from pathlib import Path - - import obliteratus - - origin = Path(obliteratus.__file__).resolve() - assert "site-packages" in origin.parts, origin - assert obliteratus.__version__ == importlib.metadata.version("obliteratus") - print(f"installed wheel import: {origin}") - print(f"version: {obliteratus.__version__}") - PY - "$wheel_env/bin/python" -I -m obliteratus --help > "$GITHUB_WORKSPACE/package-evidence/wheel-module-help.txt" - "$wheel_env/bin/obliteratus" --help > "$GITHUB_WORKSPACE/package-evidence/wheel-console-help.txt" - - - name: Verify installed sdist contract - run: | - mapfile -t sdists < <(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f -name '*.tar.gz' -print) - if [ "${#sdists[@]}" -ne 1 ]; then - echo "expected exactly one sdist, found ${#sdists[@]}" - exit 1 - fi - - sdist_env="$RUNNER_TEMP/obliteratus-sdist-env" - sdist_cwd="$RUNNER_TEMP/obliteratus-sdist-cwd" - python -m venv "$sdist_env" - mkdir -p "$sdist_cwd" - "$sdist_env/bin/python" -m pip install --no-cache-dir "rich==15.0.0" - "$sdist_env/bin/python" -m pip install --no-cache-dir --no-deps "${sdists[0]}" - - cd "$sdist_cwd" - "$sdist_env/bin/python" -I - <<'PY' | tee "$GITHUB_WORKSPACE/package-evidence/sdist-import.txt" - import importlib.metadata - from pathlib import Path - - import obliteratus - - origin = Path(obliteratus.__file__).resolve() - assert "site-packages" in origin.parts, origin - assert obliteratus.__version__ == importlib.metadata.version("obliteratus") - print(f"installed sdist import: {origin}") - print(f"version: {obliteratus.__version__}") - PY - "$sdist_env/bin/python" -I -m obliteratus --help > "$GITHUB_WORKSPACE/package-evidence/sdist-module-help.txt" - "$sdist_env/bin/obliteratus" --help > "$GITHUB_WORKSPACE/package-evidence/sdist-console-help.txt" - - name: Attest release-set SLSA provenance id: provenance uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-checksums: package-evidence/SHA256SUMS - - name: Attest wheel SBOM + - name: Attest source snapshot SBOM id: sbom-attestation uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-path: dist/*.whl + subject-path: release/*.zip sbom-path: package-evidence/obliteratus.cdx.json - name: Retain Sigstore attestation bundles @@ -204,13 +112,13 @@ jobs: package-evidence/sbom-attestation.sigstore.json sha256sum -c package-evidence/SHA256SUMS - - name: Upload distributions and package evidence + - name: Upload tested source snapshot and release evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: distributions-py3.12 + name: tested-source-snapshot-py3.12 path: | - dist/ + release/ package-evidence/ if-no-files-found: error retention-days: 14 @@ -770,10 +678,10 @@ jobs: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Download attested release set + - name: Download attested tested-source snapshot uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: distributions-py3.12 + name: tested-source-snapshot-py3.12 path: /tmp/supply-chain-evidence/release - name: Set up Python @@ -841,18 +749,18 @@ jobs: echo "$status" > "$EVIDENCE/uv-audit-py${version}.status" done - - name: Verify attested release set and bound CycloneDX SBOM + - name: Verify attested source snapshot and bound CycloneDX SBOM run: | cd "$EVIDENCE/release" sha256sum -c package-evidence/SHA256SUMS - mapfile -t wheels < <(find dist -maxdepth 1 -type f -name '*.whl' -print) - if [ "${#wheels[@]}" -ne 1 ]; then - echo "expected exactly one wheel, found ${#wheels[@]}" + mapfile -t snapshots < <(find release -maxdepth 1 -type f -name '*.zip' -print) + if [ "${#snapshots[@]}" -ne 1 ]; then + echo "expected exactly one source snapshot, found ${#snapshots[@]}" exit 1 fi python "$GITHUB_WORKSPACE/scripts/check_supply_chain_policy.py" sbom \ --input package-evidence/obliteratus.cdx.json \ - --wheel "${wheels[0]}" \ + --artifact "${snapshots[0]}" \ --output "$EVIDENCE/verified-obliteratus.cdx.json" - name: Collect packaged-dependency license inventory diff --git a/WORKSPACE.md b/WORKSPACE.md index 845e5e3..964ffa2 100644 --- a/WORKSPACE.md +++ b/WORKSPACE.md @@ -70,16 +70,16 @@ profile permits merge integration, not GitHub rebase integration, because rebase can recreate an audited signed head as an unsigned canonical commit. Verify the actual `main` commit after every merge; never rewrite `main` to repair history. -Release CI additionally validates wheel and sdist metadata, installs each distribution -in an independent environment outside the checkout, exercises both CLI entry -paths, and retains the distributions plus evidence. Immutable CI action/tool -pins are recorded in [ci/digests.txt](ci/digests.txt). +Release CI creates one deterministic ZIP of the exact tested repository commit. +The ZIP is a source snapshot and release-quality marker, not an installable or +compiled distribution. Immutable CI action/tool pins are recorded in +[ci/digests.txt](ci/digests.txt). -The package job is the sole release-artifact producer. It binds wheel, sdist, -and CycloneDX SBOM digests into `SHA256SUMS`, creates keyless Sigstore-backed -SLSA and SBOM attestations, and retains their bundles. Downstream supply-chain -and publication jobs must verify and reuse those exact bytes rather than -rebuilding equivalent-looking artifacts. +The package job is the sole source-snapshot producer. It binds the ZIP and +CycloneDX source SBOM digests into `SHA256SUMS`, creates keyless +Sigstore-backed SLSA and SBOM attestations, and retains their bundles. +Downstream supply-chain and publication jobs must verify and reuse that exact +ZIP rather than recreating an equivalent-looking archive. The source-to-test ownership graph is versioned in [ci/test-risk-map.json](ci/test-risk-map.json). Coverage, JUnit, repeat, and @@ -91,8 +91,8 @@ flaking twice in 30 days requires an active owner/issue-linked quarantine. Python CI resolution is locked by `uv.lock`, including the official CPU-only PyTorch source for Linux and Windows. The required Supply chain job scans all supported Python versions for known vulnerabilities, scans the checkout for -secrets with fully redacted evidence, enforces the packaged-dependency license -allow list, and binds a CycloneDX SBOM to the built wheel. Exception and update +secrets with fully redacted evidence, enforces the dependency license allow +list, and binds a CycloneDX source SBOM to the repository ZIP. Exception and update rules are documented in [docs/SUPPLY_CHAIN_POLICY.md](docs/SUPPLY_CHAIN_POLICY.md). GPU, MPS, MLX, model-download, external-evaluation, network, operator-UI, and diff --git a/docs/SUPPLY_CHAIN_POLICY.md b/docs/SUPPLY_CHAIN_POLICY.md index b1229b0..37c7bdd 100644 --- a/docs/SUPPLY_CHAIN_POLICY.md +++ b/docs/SUPPLY_CHAIN_POLICY.md @@ -11,21 +11,21 @@ license-tool transitive dependencies are covered by `uv.lock`. ## Digest-bound release evidence -Release artifacts use a build-once evidence chain. The release package job is -the sole producer of the wheel and source distribution. It also emits a bound -CycloneDX SBOM and a canonical `SHA256SUMS` manifest covering both distributions -and the SBOM. +The release artifact is a deterministic ZIP snapshot of the tested repository +commit. No wheel, source distribution, executable, or other compiled package is +published. The release job creates the snapshot once with `git archive`, then +emits a bound CycloneDX source SBOM and a canonical `SHA256SUMS` manifest. GitHub's keyless Sigstore-backed attestation service signs two in-toto statements for that release set: - SLSA build provenance for every subject named by `SHA256SUMS`; -- an SBOM attestation binding the CycloneDX document to the wheel digest. +- an SBOM attestation binding the CycloneDX document to the source ZIP digest. The supply-chain job downloads the package job's retained artifact, verifies -`SHA256SUMS`, and revalidates the SBOM binding. It must not rebuild a substitute -wheel. Publication must use these same tested and attested bytes. A commit, -dependency lock, build instruction, artifact, checksum manifest, SBOM, or +`SHA256SUMS`, and revalidates the SBOM binding. It must not create a substitute +archive. Publication must use this same tested and attested ZIP. A commit, +dependency lock, snapshot instruction, artifact, checksum manifest, SBOM, or release-policy change invalidates the evidence. Consumers can perform the portable integrity check with: @@ -34,7 +34,7 @@ Consumers can perform the portable integrity check with: sha256sum --check SHA256SUMS ``` -For authenticity and provenance, verify each distribution against the canonical +For authenticity and provenance, verify the source ZIP against the canonical repository with GitHub's attestation verifier. A checksum without its signed provenance proves byte integrity only; it does not prove who built the artifact or which source and build instructions produced it. @@ -45,9 +45,9 @@ The Supply chain job retains these artifacts for 14 days: - one redacted Gitleaks JSON report for the checkout; - OSV audit JSON and scanner status for Python 3.10, 3.11, and 3.12 on Linux; -- a JSON license inventory for all packaged extras; -- a CycloneDX 1.5 SBOM bound to the built wheel by SHA-256; -- the policy decisions and built wheel used by those checks. +- a JSON license inventory for all supported dependency extras; +- a CycloneDX 1.5 source SBOM bound to the repository ZIP by SHA-256; +- the policy decisions and exact tested source ZIP used by those checks. Every secret finding and every OSV vulnerability is blocking by default. OSV does not provide a normalized severity for every advisory, so the policy treats diff --git a/scripts/check_supply_chain_policy.py b/scripts/check_supply_chain_policy.py index 6ccdbad..d7f9b8e 100644 --- a/scripts/check_supply_chain_policy.py +++ b/scripts/check_supply_chain_policy.py @@ -291,8 +291,8 @@ def evaluate_secrets( } -def bind_sbom(sbom: object, wheel: Path) -> dict[str, Any]: - """Bind a CycloneDX SBOM to the exact built wheel by SHA-256.""" +def bind_sbom(sbom: object, artifact: Path) -> dict[str, Any]: + """Bind a CycloneDX SBOM to the exact promoted source artifact by SHA-256.""" if not isinstance(sbom, dict) or sbom.get("bomFormat") != "CycloneDX": raise ValueError("SBOM must be a CycloneDX JSON object") if sbom.get("specVersion") != "1.5": @@ -303,12 +303,12 @@ def bind_sbom(sbom: object, wheel: Path) -> dict[str, Any]: component = metadata.get("component") if not isinstance(component, dict) or component.get("name") != "obliteratus": raise ValueError("SBOM metadata must describe obliteratus") - digest = hashlib.sha256(wheel.read_bytes()).hexdigest() + digest = hashlib.sha256(artifact.read_bytes()).hexdigest() component["hashes"] = [{"alg": "SHA-256", "content": digest}] properties = component.setdefault("properties", []) if not isinstance(properties, list): raise ValueError("SBOM component properties must be a list") - properties.append({"name": "obliteratus:distribution-file", "value": wheel.name}) + properties.append({"name": "obliteratus:release-artifact", "value": artifact.name}) return sbom @@ -343,7 +343,7 @@ def _parser() -> argparse.ArgumentParser: licenses.add_argument("--decision", type=Path, required=True) sbom = commands.add_parser("sbom") sbom.add_argument("--input", type=Path, required=True) - sbom.add_argument("--wheel", type=Path, required=True) + sbom.add_argument("--artifact", type=Path, required=True) sbom.add_argument("--output", type=Path, required=True) return parser @@ -354,9 +354,9 @@ def main() -> int: failures = validate_policy(_read_json(args.policy)) return _write_decision(Path("/dev/null"), {"passed": not failures, "failures": failures}) if args.command == "sbom": - bound = bind_sbom(_read_json(args.input), args.wheel) + bound = bind_sbom(_read_json(args.input), args.artifact) args.output.write_text(json.dumps(bound, indent=2) + "\n", encoding="utf-8") - print(f"bound SBOM to {args.wheel.name}") + print(f"bound SBOM to {args.artifact.name}") return 0 policy = _read_json(args.policy) diff --git a/tests/test_aiwg_workspace_contracts.py b/tests/test_aiwg_workspace_contracts.py index efa7fdc..b3a7b19 100644 --- a/tests/test_aiwg_workspace_contracts.py +++ b/tests/test_aiwg_workspace_contracts.py @@ -139,6 +139,7 @@ def test_release_profile_enforces_cpu_coverage_without_collecting_hardware_gates "attestationFormat": "in-toto", "signingMode": "sigstore-keyless", "sbomFormat": "cyclonedx", - "buildOnce": True, + "artifactType": "source-zip", + "snapshotOnce": True, "verifyBeforePromotion": True, } diff --git a/tests/test_ci_policy.py b/tests/test_ci_policy.py index a819c11..1034eb4 100644 --- a/tests/test_ci_policy.py +++ b/tests/test_ci_policy.py @@ -41,7 +41,7 @@ def test_every_external_action_is_sha_pinned_and_manifested(): assert entries[("action", name)] == (pin, version) -def test_release_artifacts_are_built_once_attested_and_reused(): +def test_tested_source_snapshot_is_created_once_attested_and_reused(): workflow = WORKFLOW.read_text(encoding="utf-8") package = workflow.split(" package:\n", maxsplit=1)[1].split( " lint:\n", maxsplit=1, @@ -53,6 +53,12 @@ def test_release_artifacts_are_built_once_attested_and_reused(): assert "id-token: write" in package assert "package-evidence/SHA256SUMS" in package assert "package-evidence/obliteratus.cdx.json" in package + assert 'git archive \\' in package + assert '--format=zip' in package + assert 'subject-path: release/*.zip' in package + assert "python -m build" not in package + assert "*.whl" not in package + assert "*.tar.gz" not in package assert package.count("uses: actions/attest@") == 2 assert "subject-checksums: package-evidence/SHA256SUMS" in package assert "sbom-path: package-evidence/obliteratus.cdx.json" in package @@ -60,6 +66,8 @@ def test_release_artifacts_are_built_once_attested_and_reused(): assert "needs: package" in supply_chain assert "uses: actions/download-artifact@" in supply_chain assert "sha256sum -c package-evidence/SHA256SUMS" in supply_chain + assert "tested-source-snapshot-py3.12" in supply_chain + assert "find release -maxdepth 1 -type f -name '*.zip'" in supply_chain assert "-m build" not in supply_chain diff --git a/tests/test_supply_chain_policy.py b/tests/test_supply_chain_policy.py index 046c7e7..5917503 100644 --- a/tests/test_supply_chain_policy.py +++ b/tests/test_supply_chain_policy.py @@ -177,24 +177,24 @@ def test_secret_gate_requires_redaction_even_for_suppressed_finding(): assert any("not fully redacted" in failure for failure in decision["failures"]) -def test_bind_sbom_adds_exact_wheel_hash(tmp_path): - wheel = tmp_path / "obliteratus-0.1.2-py3-none-any.whl" - wheel.write_bytes(b"wheel") +def test_bind_sbom_adds_exact_release_artifact_hash(tmp_path): + artifact = tmp_path / "OBLITERATUS-deadbeef.zip" + artifact.write_bytes(b"source snapshot") sbom = { "bomFormat": "CycloneDX", "specVersion": "1.5", "metadata": {"component": {"name": "obliteratus"}}, } - bound = MODULE.bind_sbom(sbom, wheel) + bound = MODULE.bind_sbom(sbom, artifact) component = bound["metadata"]["component"] assert component["hashes"] == [ { "alg": "SHA-256", - "content": "ba59926159d2aa256eb8739b8da7e2b574b960e1202c6d624cbe981cef996c91", + "content": "41637c7875947b50775c0ab2bb30a28b292be748376a5713eb8f527d9953433c", }, ] assert component["properties"] == [ - {"name": "obliteratus:distribution-file", "value": wheel.name}, + {"name": "obliteratus:release-artifact", "value": artifact.name}, ]