-
Notifications
You must be signed in to change notification settings - Fork 0
ci(ga): GA evidence producer for VER-001 + SUPPLY-001 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| name: ga evidence | ||
|
|
||
| # Producer for the WAVE GA readiness gate — VER-001 and SUPPLY-001 — computed against what the | ||
| # PUBLIC PyPI registry and GitHub actually serve, never the checkout under test. See | ||
| # scripts/ga/ga_evidence.py for what each criterion verifies and what it leaves `unknown`. | ||
| # | ||
| # wave-av/sdks is the only other repo in the WAVE org that ships a GA-evidence producer today | ||
| # (its `registry clean-room acceptance` workflow). This mirrors that repo's fail-loud posture: | ||
| # every trigger reports its true state, no `|| true`, no continue-on-error, and the final | ||
| # Enforce step turns a non-zero producer exit into a red job. | ||
| # | ||
| # `pull_request` legitimately sees VER-001 as `unknown` on a release PR whose tag/version is | ||
| # ahead of what PyPI has published — the producer reports that as `unknown`, not `fail`; see the | ||
| # HEAD-ahead-of-published branch in scripts/ga/check_ver_001.py. | ||
| # | ||
| # PR CONTRACT: on `pull_request`, exit 1 (a live criterion failed) is a `::warning`, not a job | ||
| # failure — that is a property of the live registry, not of the PR's diff. Exit 2 (the producer | ||
| # could not run) always fails the job, and on schedule/workflow_dispatch/push exit 1 fails it too. | ||
|
|
||
| on: | ||
| pull_request: | ||
| workflow_dispatch: | ||
| inputs: | ||
| expect_version: | ||
| description: 'Assert PyPI now serves exactly this version (e.g. a release job verifying its own publish)' | ||
| type: string | ||
| required: false | ||
| schedule: | ||
| # 09:43 UTC — offset from a round hour so a registry rate-limit window shared across the org's | ||
| # scheduled jobs does not land on this one every day. | ||
| - cron: "43 9 * * *" | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| ga-evidence: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Run GA evidence producer against the public registries | ||
| id: evidence | ||
| env: | ||
| GA_EXPECT_VERSION: ${{ inputs.expect_version }} | ||
| run: | | ||
| set -uo pipefail | ||
| args=(--out-dir "$GITHUB_WORKSPACE/ga-out") | ||
| [ -n "${GA_EXPECT_VERSION:-}" ] && args+=(--expect-version "$GA_EXPECT_VERSION") | ||
| set +e | ||
| python3 scripts/ga/ga_evidence.py "${args[@]}" 2>&1 | tee "$RUNNER_TEMP/ga-evidence.log" | ||
| code=${PIPESTATUS[0]} | ||
| set -e | ||
| echo "exit_code=$code" >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "## GA evidence — VER-001 / SUPPLY-001" | ||
| echo | ||
| echo "Exit code \`$code\` (0 = pass/unknown, 1 = a criterion failed, 2 = the producer could not run)." | ||
| echo | ||
| echo '```' | ||
| cat "$RUNNER_TEMP/ga-evidence.log" | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit 0 | ||
|
|
||
| - name: Upload GA evidence | ||
| if: always() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | ||
| with: | ||
| name: ga-evidence-sdk-python | ||
| path: ga-out/ | ||
| if-no-files-found: warn | ||
| retention-days: 90 | ||
|
|
||
| - name: Enforce | ||
| # A gate that cannot fail is not a gate (wave-av/sdks#79 is the org's own cautionary | ||
| # tale — see registry-cleanroom.yml). This step fails loud on every trigger except one: | ||
| # see the PR CONTRACT note in the header — a live-criterion failure (exit 1) on | ||
| # `pull_request` logs a `::warning` and exits 0 instead of failing the job, because that | ||
| # failure is a property of the live registry, not of this PR's diff. Whether the job is a | ||
| # *required* branch-protection check is a separate branch-ruleset decision. | ||
| env: | ||
| CODE: ${{ steps.evidence.outputs.exit_code }} | ||
| EVENT: ${{ github.event_name }} | ||
| run: | | ||
| if [ "$CODE" = "0" ]; then | ||
| echo "ga-evidence: no criterion failed (pass or unknown only) — see the job summary for detail" | ||
| exit 0 | ||
| fi | ||
| if [ "$CODE" = "1" ] && [ "$EVENT" = "pull_request" ]; then | ||
| echo "::warning title=ga-evidence::sdk-python GA evidence producer reports a failing live criterion (exit 1); evidence is in the job summary and artifact; this does not fail the PR because the criterion is a property of the live surface, not of this change" | ||
| exit 0 | ||
| fi | ||
| if [ "$CODE" = "1" ]; then | ||
| echo "::error title=ga-evidence::a GA criterion failed verification (exit 1) — see the job summary" | ||
| exit 1 | ||
| fi | ||
| echo "::error title=ga-evidence::the producer could not run (exit $CODE) — never read as a pass" | ||
| exit 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # GA evidence producer output — regenerated by scripts/ga/ga_evidence.py, never committed. | ||
| ga-out/ | ||
|
|
||
| # Python | ||
| __pycache__/ | ||
| *.pyc | ||
| .venv/ | ||
| dist/ | ||
| build/ | ||
| *.egg-info/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| #!/usr/bin/env bash | ||
| # SUPPLY-001 — release artifacts built by approved CI from an immutable source revision, | ||
| # provenance verifiable, SBOM attached. | ||
| # | ||
| # Thin wrapper around ga_evidence.py (see there for what is and is not machine-verified: the | ||
| # provenance clause only — SBOM attachment and known-vuln resolution are named as unverified, | ||
| # never assumed). This script filters the shared run down to the SUPPLY-001 line so it can also | ||
| # be invoked standalone. | ||
| # | ||
| # Prints one `PASS|FAIL|UNKNOWN SUPPLY-001: <detail>` line. | ||
| # Exit 0 = pass, 1 = fail, 2 = could not run (never read as a pass). | ||
| set -uo pipefail | ||
|
|
||
| HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| OUT_DIR="${GA_OUT_DIR:-$HERE/../../ga-out}" | ||
| REPO="${GA_REPO:-wave-av/sdk-python}" | ||
| PACKAGE="${GA_PACKAGE:-wave-sdk}" | ||
|
|
||
| OUTPUT="$(python3 "$HERE/ga_evidence.py" --out-dir "$OUT_DIR" --repo "$REPO" --package "$PACKAGE" 2>&1)" | ||
| CODE=$? | ||
|
|
||
| echo "$OUTPUT" | grep -E '^(PASS|FAIL|UNKNOWN) SUPPLY-001:' | ||
| if [ "$CODE" -eq 2 ]; then | ||
| echo "$OUTPUT" 1>&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| echo "$OUTPUT" | grep -q '^FAIL SUPPLY-001:' && exit 1 | ||
| exit 0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #!/usr/bin/env bash | ||
| # VER-001 — every shipped component resolves to one source revision and version; no newer | ||
| # source is represented as deployed. | ||
| # | ||
| # Thin wrapper around ga_evidence.py, which computes both criteria in one registry-fetch pass | ||
| # (VER-001 and SUPPLY-001 share the same PyPI `info.version` lookup). This script filters the | ||
| # shared run down to the VER-001 line so it can also be invoked standalone. | ||
| # | ||
| # Prints one `PASS|FAIL|UNKNOWN VER-001: <detail>` line. | ||
| # Exit 0 = pass, 1 = fail, 2 = could not run (never read as a pass). | ||
| set -uo pipefail | ||
|
|
||
| HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| OUT_DIR="${GA_OUT_DIR:-$HERE/../../ga-out}" | ||
| REPO="${GA_REPO:-wave-av/sdk-python}" | ||
| PACKAGE="${GA_PACKAGE:-wave-sdk}" | ||
|
|
||
| ARGS=(--out-dir "$OUT_DIR" --repo "$REPO" --package "$PACKAGE") | ||
| # GA_EXPECT_VERSION: optional pin asserting PyPI now serves exactly this version (e.g. a release | ||
| # job verifying its own publish). Also the deliberate-break lever for the drill this producer's | ||
| # PR must prove: pin a wrong version and this check flips PASS/UNKNOWN -> FAIL, exit 1. | ||
| [ -n "${GA_EXPECT_VERSION:-}" ] && ARGS+=(--expect-version "$GA_EXPECT_VERSION") | ||
|
|
||
| OUTPUT="$(python3 "$HERE/ga_evidence.py" "${ARGS[@]}" 2>&1)" | ||
| CODE=$? | ||
|
|
||
| echo "$OUTPUT" | grep -E '^(PASS|FAIL|UNKNOWN) VER-001:' | ||
| if [ "$CODE" -eq 2 ]; then | ||
| echo "$OUTPUT" 1>&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| echo "$OUTPUT" | grep -q '^FAIL VER-001:' && exit 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Custom agent: Flag AI Slop and Fabricated Changes Unexpected producer failures can make this wrapper exit 0. When Prompt for AI agents |
||
| exit 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """SUPPLY-001 — release artifacts built by approved CI from an immutable source revision, | ||
| provenance verifiable, SBOM attached. | ||
|
|
||
| This producer machine-verifies the provenance clause ONLY: the PyPI Integrity API is queried for | ||
| every published artifact (wheel + sdist), and the attestation's claimed source repository must be | ||
| `github.com/<repo>`. SBOM attachment and critical-vulnerability resolution are NOT machine-verified | ||
| here, so a fully-verified provenance still yields `unknown` (never `pass`) with those two gaps | ||
| named explicitly in `failing_checks`. Absent or mismatched provenance is `fail`. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
|
|
||
| from ga_common import CheckResult, CriterionResult, fetch_json, fetch_json_allow_404, pypi_url | ||
|
|
||
|
|
||
| def run(repo: str, package: str) -> CriterionResult: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Pragmatic Test Coverage The new SUPPLY-001 gate has no tests for its main success and failure paths. Cover empty artifact lists, missing/404 provenance, wrong repository claims, and the fully-provenanced Prompt for AI agents |
||
| command = f"python3 scripts/ga/ga_evidence.py --repo {repo} --package {package}" | ||
| checks: list[CheckResult] = [] | ||
|
|
||
| meta = fetch_json(pypi_url(package)) | ||
| version = meta["info"]["version"] | ||
| urls = meta.get("urls", []) | ||
| targets = [f"{package}@{version}"] | ||
|
|
||
| if not urls: | ||
| checks.append(CheckResult("pypi-artifacts-present", False, f"PyPI serves no files for {package}=={version}")) | ||
| return CriterionResult("SUPPLY-001", "fail", command, checks, targets) | ||
|
|
||
| all_have_provenance = True | ||
| wrong_repo_claims: list[str] = [] | ||
| for u in urls: | ||
| filename = u["filename"] | ||
| prov_url = ( | ||
| f"https://pypi.org/integrity/{package}/{version}/{filename}/provenance" | ||
| ) | ||
| status, body = fetch_json_allow_404(prov_url) | ||
| if status == 404 or body is None or "attestation_bundles" not in body: | ||
| all_have_provenance = False | ||
| checks.append(CheckResult( | ||
| f"provenance-present:{filename}", False, | ||
| "no provenance available from PyPI Integrity API", | ||
| )) | ||
| continue | ||
| bundles = body.get("attestation_bundles", []) | ||
| raw = json.dumps(body) | ||
| repo_claim_ok = f"github.com/{repo}" in raw or repo in raw | ||
|
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if repo_claim_ok: | ||
| checks.append(CheckResult( | ||
| f"provenance-present:{filename}", True, | ||
| f"PyPI Integrity API returned {len(bundles)} attestation bundle(s) referencing {repo}", | ||
| )) | ||
| else: | ||
| wrong_repo_claims.append(filename) | ||
| checks.append(CheckResult( | ||
| f"provenance-present:{filename}", False, | ||
| f"attestation present but does not reference {repo}", | ||
| )) | ||
|
|
||
| if wrong_repo_claims or not all_have_provenance: | ||
| status = "fail" | ||
| else: | ||
| # Provenance verifies for every artifact, but SBOM attachment and known-vuln resolution | ||
| # stay out of scope for this producer — the criterion cannot be a full pass. | ||
| status = "unknown" | ||
| checks.append(CheckResult("sbom-attached", None, "SBOM attachment not verified by this producer")) | ||
| checks.append(CheckResult("known-vuln-resolution", None, "critical-vuln resolution not verified by this producer")) | ||
|
|
||
| return CriterionResult("SUPPLY-001", status, command, checks, targets) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This workflow triggers the producer on every
pull_requestand on a dailyschedule, andcheck_ver_001.pycalls the GitHub API without a token (https://api.github.com/repos/{repo}/tagsand.../releases/tags/...). Unauthenticated GitHub API is limited to 60 req/hr per egress IP, and GitHub-hosted runner IPs are shared across many concurrent jobs, so a 403 rate-limit response is plausible — especially during the org's shared rate-limit window that the daily cron deliberately tries to avoid, but PR-triggered runs are not offset at all. A 403 (non-404 HTTP error) makesfetch_json_allow_404/fetch_jsonraiseRegistryError, whichga_evidence.pyconverts to exit code 2, and theEnforcestep then turns the entire PR job red for every open PR even though no criterion actually failed. Consider supplying the rate-limited data via a token (e.g. using the built-ingithub.token, which the runner is authorized for and which is not subject to the unauth limit) or accepting 429/403 transiently, so intermittent registry throttling does not flake the PR gate red.Prompt for AI agents