From f329e068afc3ab99560abfeaa25ee4e74be8672c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 26 Sep 2026 21:33:24 +0900 Subject: [PATCH 1/3] ci: require exact distribution set before dependency gate --- .../release-dependency-license-strix-gate.yml | 70 ++++++++++--------- ...lease_dependency_gate_workflow_contract.py | 28 ++++++-- tests/test_verify_release_distribution_set.py | 42 +++++++++++ 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/.github/workflows/release-dependency-license-strix-gate.yml b/.github/workflows/release-dependency-license-strix-gate.yml index 67a20e8189..d343699e77 100644 --- a/.github/workflows/release-dependency-license-strix-gate.yml +++ b/.github/workflows/release-dependency-license-strix-gate.yml @@ -46,16 +46,12 @@ on: required: false type: string default: "" - build_artifact_name: - description: Caller artifact holding the exact wheel and sdist to publish. + distribution_set_artifact_id: + description: Immutable same-run reproducibility record artifact ID containing the complete distribution set manifest. required: true type: string - build_artifact_id: - description: Immutable upload-artifact ID from this caller run. - required: true - type: string - build_artifact_digest: - description: Expected sha256-prefixed artifact digest from the producer upload. + distribution_set_artifact_digest: + description: Expected sha256-prefixed reproducibility record artifact digest from the producer upload. required: true type: string wheel_filename: @@ -188,7 +184,7 @@ jobs: with: repository: ContextualWisdomLab/.github # Reviewed helper revision; intentionally distinct from workflow revision. - ref: 00c6551183cca101cfc97c43656a17cc2491c1b4 + ref: 7cb4be4c5cfef406fae065eb4697018fe956b18c path: trusted-gate persist-credentials: false # The whole scripts/ci tree, not an enumerated file list: the trusted @@ -208,17 +204,18 @@ jobs: CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | - expected=00c6551183cca101cfc97c43656a17cc2491c1b4 + expected=7cb4be4c5cfef406fae065eb4697018fe956b18c test "$(git -C "$HELPER_ROOT" rev-parse HEAD)" = "$expected" origin="$(git -C "$HELPER_ROOT" remote get-url origin)" case "$origin" in https://github.com/ContextualWisdomLab/.github|https://github.com/ContextualWisdomLab/.github.git) ;; *) echo "Foreign helper repository" >&2; exit 1 ;; esac - test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = bf26d3eefdb71fe79b855d941ffb46eb432b2f76 + test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = 30720a6a86fc0ee7f836bd7905fc62e7122e0b6d test "$(git -C "$HELPER_ROOT" rev-parse HEAD:requirements-strix-ci-hashes.txt)" = 9e705850b5ce53c7fe836bc3df3a18771151e3f6 git -C "$HELPER_ROOT" diff --exit-code HEAD -- scripts/ci requirements-strix-ci-hashes.txt test -f "$HELPER_ROOT/scripts/ci/release_dependency_gate.py" + test -f "$HELPER_ROOT/scripts/ci/verify_release_distribution_set.py" test -f "$HELPER_ROOT/scripts/ci/verify_exact_artifact_sbom_handoff.py" test -f "$HELPER_ROOT/requirements-strix-ci-hashes.txt" printf 'helper_repository=ContextualWisdomLab/.github helper_sha=%s caller_workflow_sha=%s\n' "$expected" "$CALLER_WORKFLOW_SHA" @@ -257,34 +254,43 @@ jobs: with: python-version: "3.13" - - name: Verify immutable same-run build artifact metadata + - name: List the current run and attempt artifacts env: GH_TOKEN: ${{ github.token }} SOURCE_REPOSITORY: ${{ inputs.source_repository }} - ARTIFACT_ID: ${{ inputs.build_artifact_id }} - ARTIFACT_NAME: ${{ inputs.build_artifact_name }} - ARTIFACT_DIGEST: ${{ inputs.build_artifact_digest }} shell: bash --noprofile --norc -e -o pipefail {0} run: | - # Reuse the exact-artifact attestation's same-run metadata boundary. test "$SOURCE_REPOSITORY" = "$GITHUB_REPOSITORY" - [[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] - [[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] - artifact_json="$(gh api "/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" - jq -e \ - --arg id "$ARTIFACT_ID" \ - --arg name "$ARTIFACT_NAME" \ - --arg digest "$ARTIFACT_DIGEST" \ - --argjson run_id "$GITHUB_RUN_ID" \ - '(.id | tostring) == $id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false' \ - <<<"$artifact_json" >/dev/null + gh api --paginate "repos/${SOURCE_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \ + --jq '.artifacts[]' > "${RUNNER_TEMP}/release-artifacts.jsonl" + gh api "repos/${SOURCE_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}" \ + > "${RUNNER_TEMP}/release-attempt.json" - - name: Download the exact distributions the caller intends to publish - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ inputs.build_artifact_id }} - digest-mismatch: error - path: release-distributions + - name: Verify every immutable distribution before dependency capture + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + CONTROL_SHA: ${{ github.sha }} + RECORD_ID: ${{ inputs.distribution_set_artifact_id }} + RECORD_DIGEST: ${{ inputs.distribution_set_artifact_digest }} + WHEEL_FILENAME: ${{ inputs.wheel_filename }} + SDIST_FILENAME: ${{ inputs.sdist_filename }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python3 -I trusted-gate/scripts/ci/verify_release_distribution_set.py \ + --repository "$SOURCE_REPOSITORY" \ + --source-sha "$SOURCE_SHA" \ + --control-sha "$CONTROL_SHA" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" \ + --record-artifact-id "$RECORD_ID" \ + --record-artifact-digest "$RECORD_DIGEST" \ + --wheel-filename "$WHEEL_FILENAME" \ + --sdist-filename "$SDIST_FILENAME" \ + --metadata "${RUNNER_TEMP}/release-artifacts.jsonl" \ + --attempt "${RUNNER_TEMP}/release-attempt.json" \ + --output release-distributions - name: Collect the release closure without installing or executing it env: diff --git a/tests/test_release_dependency_gate_workflow_contract.py b/tests/test_release_dependency_gate_workflow_contract.py index 3513d5deb7..4d3d4449e4 100644 --- a/tests/test_release_dependency_gate_workflow_contract.py +++ b/tests/test_release_dependency_gate_workflow_contract.py @@ -98,14 +98,14 @@ def test_every_action_is_pinned_to_the_same_commits_as_attestation() -> None: """The gate and the attestation it feeds materialize identical trusted actions.""" workflow = _workflow_text() attestation = _ATTESTATION.read_text(encoding="utf-8") - for pin in (_HARDEN_RUNNER_PIN, _CHECKOUT_PIN, _UPLOAD_ARTIFACT_PIN, _DOWNLOAD_ARTIFACT_PIN): + for pin in (_HARDEN_RUNNER_PIN, _CHECKOUT_PIN, _UPLOAD_ARTIFACT_PIN): assert pin in workflow assert pin in attestation + assert _DOWNLOAD_ARTIFACT_PIN in attestation assert _SETUP_PYTHON_PIN in workflow references = re.findall(r"(?m)^ +uses: (.+)$", workflow) - # Eight: harden-runner, two checkouts, setup-python, download-artifact, and - # three uploads (sealed evidence, the licence report, the gate report). - assert len(references) == 8 + # Seven: harden-runner, two checkouts, setup-python, and three uploads. + assert len(references) == 7 for reference in references: assert re.match(r"^[^@]+@[0-9a-f]{40} # ", reference), reference @@ -124,7 +124,7 @@ def test_trusted_gate_is_materialized_from_this_repository_at_its_pinned_sha() - """The decision code is the base repository's, never the caller's tree.""" workflow = _workflow_text() assert "repository: ContextualWisdomLab/.github" in workflow - assert "ref: 00c6551183cca101cfc97c43656a17cc2491c1b4" in workflow + assert "ref: 7cb4be4c5cfef406fae065eb4697018fe956b18c" in workflow assert "path: trusted-gate" in workflow assert "persist-credentials: false" in workflow # The whole scripts/ci tree, because the trusted Strix gate, the @@ -167,6 +167,7 @@ def test_step_order_captures_then_strixes_then_gates_then_seals() -> None: "Materialize immutable trusted gate", "Validate the exact release identity before anything else runs", "Check out the exact release head", + "Verify every immutable distribution before dependency capture", "Collect the release closure without installing or executing it", "Assemble per-dependency evidence and isolated synthetic fixtures", "Refuse a denied or unverifiable licence before any credential exists", @@ -183,6 +184,23 @@ def test_step_order_captures_then_strixes_then_gates_then_seals() -> None: assert positions == sorted(positions), "gate steps are out of order" +def test_complete_distribution_set_is_required_and_verified_before_strix() -> None: + workflow = _workflow_text() + inputs = workflow.split(" inputs:\n", 1)[1].split(" secrets:\n", 1)[0] + for name in ("distribution_set_artifact_id", "distribution_set_artifact_digest"): + assert re.search(rf"(?m)^ {name}:\n(?: .*\n)*? required: true$", inputs) + assert "build_artifact_id" not in inputs + verifier = "python3 -I trusted-gate/scripts/ci/verify_release_distribution_set.py" + assert verifier in workflow + assert workflow.index(verifier) < workflow.index("release_dependency_gate.py prescreen") + assert workflow.index(verifier) < workflow.index("secrets.BYTEZ_API_KEY") + assert "--record-artifact-id \"$RECORD_ID\"" in workflow + assert "--record-artifact-digest \"$RECORD_DIGEST\"" in workflow + assert "--run-attempt \"$GITHUB_RUN_ATTEMPT\"" in workflow + assert "--wheel-filename \"$WHEEL_FILENAME\"" in workflow + assert "--sdist-filename \"$SDIST_FILENAME\"" in workflow + + def test_the_licence_decision_precedes_every_credential_and_model_step() -> None: """A denied licence is refused before a provider secret is read at all. diff --git a/tests/test_verify_release_distribution_set.py b/tests/test_verify_release_distribution_set.py index c312df8b61..068a86a776 100644 --- a/tests/test_verify_release_distribution_set.py +++ b/tests/test_verify_release_distribution_set.py @@ -4,6 +4,9 @@ import hashlib import io import json +import os +import subprocess +import sys import zipfile from pathlib import Path @@ -109,6 +112,45 @@ def test_verifies_all_thirteen_immutable_artifact_archives(tmp_path: Path) -> No assert _sha((tmp_path / "dist" / row["file"]).read_bytes()) == row["sha256"] +def test_cli_downloads_the_exact_ids_before_exposing_files(tmp_path: Path) -> None: + case = _case() + archives = tmp_path / "archives" + archives.mkdir() + for artifact_id, data in case["archives"].items(): + (archives / f"{artifact_id}.zip").write_bytes(data) + metadata = tmp_path / "metadata.jsonl" + metadata.write_text("".join(json.dumps(item) + "\n" for item in case["metadata"])) + attempt = tmp_path / "attempt.json" + attempt.write_text(json.dumps(case["attempt"])) + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + gh = bin_dir / "gh" + gh.write_text( + "#!/usr/bin/env python3\n" + "import os, pathlib, sys\n" + "path = sys.argv[2]\n" + "sys.stdout.buffer.write((pathlib.Path(os.environ['FAKE_ARCHIVES']) / (path.split('/')[-2] + '.zip')).read_bytes())\n" + ) + gh.chmod(0o755) + script = Path(__file__).resolve().parents[1] / "scripts/ci/verify_release_distribution_set.py" + record_digest = next(item["digest"] for item in case["metadata"] if item["name"] == "reproducibility-record") + result = subprocess.run( + [sys.executable, "-I", str(script), "--repository", "owner/repo", + "--source-sha", SOURCE, "--control-sha", CONTROL, + "--run-id", str(RUN), "--run-attempt", str(ATTEMPT), + "--record-artifact-id", "14", "--record-artifact-digest", record_digest, + "--wheel-filename", "pkg-1.2.3-1.whl", "--sdist-filename", "pkg-1.2.3.tar.gz", + "--metadata", str(metadata), "--attempt", str(attempt), + "--output", str(tmp_path / "dist")], + env={**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}", + "FAKE_ARCHIVES": str(archives)}, + capture_output=True, text=True, + ) + assert result.returncode == 0, result.stderr + assert len(json.loads(result.stdout)["verified_distributions"]) == 13 + assert len(list((tmp_path / "dist").iterdir())) == 13 + + def test_refuses_forged_missing_stale_and_tampered_sets(tmp_path: Path) -> None: def missing(case): case["manifest"]["distributions"].pop() From 3846ce3875e2659386c12130ed1f78cf9fcbb996 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 26 Sep 2026 22:29:37 +0900 Subject: [PATCH 2/3] test(release): bind helper identity fixtures per workflow --- tests/test_release_fixed_helper_identity.py | 41 ++++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/tests/test_release_fixed_helper_identity.py b/tests/test_release_fixed_helper_identity.py index 5dd5dae194..00fc240ff4 100644 --- a/tests/test_release_fixed_helper_identity.py +++ b/tests/test_release_fixed_helper_identity.py @@ -7,10 +7,16 @@ import pytest ROOT = Path(__file__).resolve().parents[1] -PIN = "00c6551183cca101cfc97c43656a17cc2491c1b4" -CASES = [("release-dependency-license-strix-gate.yml", "trusted-gate"), - ("exact-artifact-sbom-attestation.yml", "trusted-intake"), - ("exact-artifact-sbom-attestation.yml", "trusted-signer")] +LEGACY_PIN = "00c6551183cca101cfc97c43656a17cc2491c1b4" +EXACT_SET_PIN = "7cb4be4c5cfef406fae065eb4697018fe956b18c" +CASES = [ + ("release-dependency-license-strix-gate.yml", "trusted-gate", + EXACT_SET_PIN, "30720a6a86fc0ee7f836bd7905fc62e7122e0b6d"), + ("exact-artifact-sbom-attestation.yml", "trusted-intake", + LEGACY_PIN, "bf26d3eefdb71fe79b855d941ffb46eb432b2f76"), + ("exact-artifact-sbom-attestation.yml", "trusted-signer", + LEGACY_PIN, "bf26d3eefdb71fe79b855d941ffb46eb432b2f76"), +] def _parts(filename, destination): @@ -22,34 +28,40 @@ def _parts(filename, destination): return checkout, "\n".join(line[10:] for line in script.splitlines()) -@pytest.mark.parametrize("filename,destination", CASES) -def test_literal_source_pin_and_sibling_scope(filename, destination): +@pytest.mark.parametrize("filename,destination,pin,tree", CASES) +def test_literal_source_pin_and_sibling_scope(filename, destination, pin, tree): checkout, script = _parts(filename, destination) - assert f"ref: {PIN}" in checkout + assert f"ref: {pin}" in checkout assert "repository: ContextualWisdomLab/.github" in checkout assert "${{" not in checkout - assert f"expected={PIN}" in script + assert f"expected={pin}" in script + assert f"rev-parse HEAD:scripts/ci)\" = {tree}" in script text = (ROOT / ".github/workflows" / filename).read_text() following = text.split(f" path: {destination}\n", 1)[1] scope = following.split(" - name: Verify fixed helper checkout identity", 1)[0] assert "scripts/ci/" in scope and "requirements-strix-ci-hashes.txt" in scope -@pytest.mark.parametrize("filename,destination", CASES) +@pytest.mark.parametrize("filename,destination,pin,tree", CASES) @pytest.mark.parametrize("case", ["ok", "caller_changed", "foreign", "missing", "pin", "tree", "dirty", "file"]) -def test_actual_guard_rejects_bad_source(tmp_path, filename, destination, case): +def test_actual_guard_rejects_bad_source(tmp_path, filename, destination, pin, tree, case): _, script = _parts(filename, destination) helper = tmp_path / destination (helper / "scripts/ci").mkdir(parents=True) - for name in ("scripts/ci/release_dependency_gate.py", "scripts/ci/verify_exact_artifact_sbom_handoff.py", "requirements-strix-ci-hashes.txt"): + for name in ( + "scripts/ci/release_dependency_gate.py", + "scripts/ci/verify_release_distribution_set.py", + "scripts/ci/verify_exact_artifact_sbom_handoff.py", + "requirements-strix-ci-hashes.txt", + ): if not (case == "file" and name.endswith("release_dependency_gate.py")): (helper / name).write_text("inert fixture") fake = '''git() { if [ "$CASE" = missing ]; then return 128; fi case "$*" in - *"rev-parse HEAD:scripts/ci") [ "$CASE" = tree ] && echo bad || echo bf26d3eefdb71fe79b855d941ffb46eb432b2f76 ;; + *"rev-parse HEAD:scripts/ci") [ "$CASE" = tree ] && echo bad || echo "$EXPECTED_TREE" ;; *"rev-parse HEAD:requirements-strix-ci-hashes.txt") echo 9e705850b5ce53c7fe836bc3df3a18771151e3f6 ;; - *"rev-parse HEAD") [ "$CASE" = pin ] && echo bad || echo 00c6551183cca101cfc97c43656a17cc2491c1b4 ;; + *"rev-parse HEAD") [ "$CASE" = pin ] && echo bad || echo "$EXPECTED_PIN" ;; *"remote get-url origin") [ "$CASE" = foreign ] && echo https://github.com/caller/repo || echo https://github.com/ContextualWisdomLab/.github ;; *"diff --exit-code"*) [ "$CASE" != dirty ] ;; *) return 99 ;; @@ -58,8 +70,9 @@ def test_actual_guard_rejects_bad_source(tmp_path, filename, destination, case): ''' result = subprocess.run(["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", fake + script], env={**os.environ, "HELPER_ROOT": str(helper), "CASE": case, + "EXPECTED_PIN": pin, "EXPECTED_TREE": tree, "CALLER_WORKFLOW_SHA": ("b" if case == "caller_changed" else "a") * 40}, capture_output=True, text=True) assert (result.returncode == 0) is (case in ("ok", "caller_changed")), result.stderr if result.returncode == 0: - assert f"helper_sha={PIN}" in result.stdout + assert f"helper_sha={pin}" in result.stdout From 03675f3bf07dc0b1588c0be848c15fd0ccf1f0a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 26 Sep 2026 22:35:16 +0900 Subject: [PATCH 3/3] test(release): remove superseded pair-artifact oracle --- tests/test_release_build_artifact_identity.py | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 tests/test_release_build_artifact_identity.py diff --git a/tests/test_release_build_artifact_identity.py b/tests/test_release_build_artifact_identity.py deleted file mode 100644 index 68d6ab70bb..0000000000 --- a/tests/test_release_build_artifact_identity.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Execute the shipped artifact-metadata boundary with inert API fixtures.""" - -import json -import os -import re -from pathlib import Path -import subprocess -import textwrap - -import pytest - -WORKFLOW = Path('.github/workflows/release-dependency-license-strix-gate.yml') - - -@pytest.mark.parametrize('case', ['valid', 'wrong-id', 'wrong-run', 'missing-digest', - 'wrong-digest', 'expired', 'missing-id', 'bad-input-digest', - 'wrong-repository', 'api-failure']) -def test_build_artifact_boundary_runs_before_download(tmp_path, case): - workflow = WORKFLOW.read_text() - step_name = 'Verify immutable same-run build artifact metadata' - step = workflow.split('- name: ' + step_name + '\n', 1)[1].split('\n - ', 1)[0] - script = textwrap.dedent(step.split(' run: |\n', 1)[1]) - download = workflow.split('- name: Download the exact distributions', 1)[1].split('\n - ', 1)[0] - assert 'artifact-ids: ${{ inputs.build_artifact_id }}' in download - assert 'digest-mismatch: error' in download - assert 'name: ${{ inputs.build_artifact_name }}' not in download - assert workflow.index(step_name) < workflow.index('Download the exact distributions') - assert ' actions: read' in workflow - for key in ('build_artifact_id', 'build_artifact_digest'): - declaration = re.split(r'\n \S', workflow.split(' ' + key + ':\n', 1)[1], maxsplit=1)[0] - assert 'required: true' in declaration - digest = 'sha256:' + 'a' * 64 - metadata = dict(id=123, name='dist-pair', digest=digest, workflow_run=dict(id=456), expired=False) - if case == 'wrong-id': metadata['id'] = 124 - if case == 'wrong-run': metadata['workflow_run']['id'] = 457 - if case == 'missing-digest': metadata.pop('digest') - if case == 'wrong-digest': metadata['digest'] = 'sha256:' + 'b' * 64 - if case == 'expired': metadata['expired'] = True - fake = tmp_path / 'gh' - fake.write_text('#!/bin/sh\n[ "$1" = api ] || exit 91\n' - '[ "$2" = /repos/owner/repo/actions/artifacts/123 ] || exit 92\n' - '[ "$API_FAILURE" != yes ] || exit 93\n' - 'printf "%s" "$API_METADATA"\n') - fake.chmod(0o755) - marker = tmp_path / 'download-reached' - env = {**os.environ, 'PATH': str(tmp_path) + os.pathsep + os.environ['PATH'], - 'GH_TOKEN': '', 'SOURCE_REPOSITORY': 'other/repo' if case == 'wrong-repository' else 'owner/repo', - 'GITHUB_REPOSITORY': 'owner/repo', 'GITHUB_RUN_ID': '456', - 'ARTIFACT_ID': '' if case == 'missing-id' else '123', 'ARTIFACT_NAME': 'dist-pair', - 'ARTIFACT_DIGEST': '' if case == 'bad-input-digest' else digest, - 'API_METADATA': json.dumps(metadata), 'API_FAILURE': 'yes' if case == 'api-failure' else 'no', - 'MARKER': str(marker)} - result = subprocess.run(['bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', - script + '\nprintf reached > "$MARKER"\n'], - env=env, capture_output=True, text=True) - assert (result.returncode == 0) == (case == 'valid'), result.stderr - assert marker.exists() == (case == 'valid')