From 0d1c0e9c595fd19640b1ca9948ab3792444b99a4 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 12:43:18 +1000 Subject: [PATCH 01/10] fix(ci): fail loudly on attestation lookup failure; decide the codecov pair (LAB-2528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the three LAB-2528 fail-open findings. Finding 1 (the Atheris job that fuzzed nothing) is fixed under LAB-1140 in #269 and deliberately not touched here, so the two PRs cannot conflict in security-deep.yml. Finding 2 — attestation-check.yml swallowed a failed release lookup into a green skip. `2>/dev/null || echo ""` made "the gh API failed" and "this repo has no releases" the same fact: the run exited 0, wrote skip=true, and logged "No releases found, skipping" — so a week with no attestation verification at all was indistinguishable from a healthy one. The lookup now fails the step with an ::error:: annotation and only a genuinely empty release list skips, with distinct log lines for the two. github.repository moves into env: REPO, which also removes a template-expansion-into-shell site. Finding 3 — the codecov fail_ci_if_error pair, recorded as a decision rather than left at its default. coverage.xml is flipped to true: it is the only input to the project/patch statuses codecov.yml declares, and with carryforward: true on every flag a silently-dropped upload does not remove the patch status, it answers "is this PR's new code 80% covered?" with an earlier run's numbers — a green status that measured none of the diff, the same manufactured-evidence class as the two findings above. junit.xml stays false on purpose: it feeds Test Analytics only, nothing gates on it, and a Codecov outage there would redden passing CI while hiding nothing. Both rationales live in ci.yml at the point of enforcement. No fork-PR exposure — fork PRs cannot mint the OIDC token these uploads use. Evidence: the shipped step body extracted from the YAML and run under bash -e against a stubbed gh, pre-fix vs post-fix. Pre-fix on API failure: exit 0, skip=true, "No releases found, skipping". Post-fix: exit 1 with the annotation; empty list still exits 0 with skip=true; healthy path yields tag=v0.17.1, skip=false. gh's --jq null rendering checked against the real binary on repos with and without releases. actionlint passes. --- .github/workflows/attestation-check.yml | 16 ++++++++++++++-- .github/workflows/ci.yml | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/attestation-check.yml b/.github/workflows/attestation-check.yml index 19a49633..93adf30e 100644 --- a/.github/workflows/attestation-check.yml +++ b/.github/workflows/attestation-check.yml @@ -16,16 +16,28 @@ jobs: - name: Get latest release tag id: release run: | - TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || echo "") + # "the lookup failed" and "there are no releases" are different facts and + # must not share an exit path. The old `2>/dev/null || echo ""` collapsed + # an API hiccup or a bad token into the same green "skipping" as a repo + # with no releases at all, so a week of attestation verification could go + # un-run and still report success — indistinguishable from healthy. + # `// empty` keeps the genuinely-empty list an empty TAG without relying + # on how gh renders a null jq result. + if ! TAG=$(gh release list --repo "$REPO" --limit 1 --json tagName --jq '.[0].tagName // empty'); then + echo "::error::gh release list failed for $REPO — cannot determine the latest release; refusing to report a green skip" + exit 1 + fi if [ -z "$TAG" ]; then - echo "No releases found, skipping" + echo "$REPO has no published releases — nothing to attest, skipping" echo "skip=true" >> "$GITHUB_OUTPUT" else + echo "Latest release: $TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT" fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} - name: Set up Python if: steps.release.outputs.skip != 'true' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 927d2eca..881ef92a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,15 +153,31 @@ jobs: REDIS_URL: redis://localhost:6379 # autouse redis-isolation fixture uses external Redis when set (else spawns a binary the runner lacks) run: uv run pytest tests/performance/ -m "performance and slow" -q + # fail_ci_if_error is deliberately split between these two uploads, not + # defaulted (LAB-2528 finding 3). coverage.xml is the ONLY input to the + # project/patch statuses codecov.yml declares, and every flag there sets + # `carryforward: true` — so a silently-dropped upload does not remove the + # status, it answers "is this PR's new code 80% covered?" with a previous + # run's numbers. A green patch status that measured none of the diff is the + # same manufactured-evidence class this ticket exists to remove, so an + # upload failure has to be red and visible. Fork PRs cannot mint the OIDC + # token this uses (`id-token: write` is never granted to them) and never + # reach the self-hosted `cachekit` runner unapproved, so this cannot redden + # an outside contribution. - name: Upload coverage to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 with: files: ./coverage.xml use_oidc: true - fail_ci_if_error: false + fail_ci_if_error: true flags: ${{ github.event_name == 'push' && 'full' || 'critical' }}-python-${{ matrix.python-version }} + # Stays false, deliberately: junit.xml feeds Codecov Test Analytics + # (flaky-test history) only. Nothing gates on it and no status is computed + # from it, so a Codecov-side outage here would redden otherwise-passing CI + # while hiding nothing. Losing a run of flake history is not a trust bug the + # way stale coverage is. - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 From 228e904f7b6ca811da6ea646a2631acf09bc9915 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 12:56:17 +1000 Subject: [PATCH 02/10] =?UTF-8?q?fix(ci):=20apply=20expert-panel=20finding?= =?UTF-8?q?s=20=E2=80=94=20draft/prerelease=20lookup,=20tag=20injection,?= =?UTF-8?q?=20fork-scoped=20codecov=20gate=20(LAB-2528)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four-agent panel at high stakes. Surviving findings, all applied: CRIT (introduced by the previous commit, caught by two agents independently): the in-file comment justifying `fail_ci_if_error: true` claimed fork PRs "cannot mint the OIDC token" and therefore could not be reddened. Read at the pinned SHA, the action does the opposite: `Get OIDC token` is guarded `CC_USE_OIDC == 'true' && CC_FORK != 'true'`, so on a fork it never attempts OIDC, CC_TOKEN stays empty, `Override branch for forks` sets TOKENLESS, and CC_FAIL_ON_ERROR still applies — a Codecov rate-limit would redden an outside contribution. On a repo with no branch protection that trains maintainers to merge over red CI, i.e. it degrades the gate it was meant to harden. The flag is now scoped to same-repo events, where OIDC actually authenticates, and the comment records the mechanism rather than the false premise. A comment asserting behaviour the code does not exhibit is a trust bug in its own right. MAJ (introduced): the new `exit 1` on a failed lookup fell into the `if: failure()` issue-creation step, filing a public bug issue titled "Attestation verification failed for " — empty tag, blaming attestation verification for an API outage that never reached the verify step, weekly and undeduped. Gated on `steps.release.outputs.skip == 'false'`; for a lookup failure the red run is the signal. CRIT (in scope — this diff rewrote the lookup): `gh release list --limit 1` is unfiltered. `--exclude-drafts` / `--exclude-pre-releases` are opt-in, so a draft or prerelease can win `.[0]` — verifying an RC green while the stable wheel users install goes unchecked, or failing on a wheel PyPI never got. Now selects on `isLatest` (GitHub's own newest-non-draft-non-prerelease marker), and "releases exist but none is latest" is a hard failure rather than a green skip: that was the LAB-984 shape reproduced one level down. CRIT (pre-existing, in-family so fixed here): `VER="${{ ... outputs.tag }}"` template-interpolated a release tag into the shell body. `git check-ref-format` accepts `v1.0.0$(id)` and backticked tags, and whoever can name a tag is the adversary this tripwire exists to catch — that is code execution in a job holding GH_TOKEN and issues: write, from where a `gh` shim makes the verify two lines later exit 0. TAG and REPO now arrive via env in both remaining steps; the previous commit had moved only `github.repository`, leaving the one value that is actually externally set interpolated. Two rhetorical comment sentences cut (both agents flagged them as restating the preceding line). REJECTED, with reason recorded in-file: `handle_no_reports_found: true`. It would also swallow "the report was never written" — the silent degradation finding 3 exists to remove. A second red step on an already-red job is noise; a green job that uploaded nothing is a trust bug. Evidence. The shipped lookup body is extracted from the YAML with yaml.safe_load and run under `bash -e` against a stubbed gh, five cases, all asserted: lookup failure -> exit 1; releases-but-none-latest -> exit 1; zero releases -> exit 0 skip=true; healthy -> exit 0 tag=v0.17.1; prerelease newer than stable -> picks the stable one. `isLatest`/`isDraft`/`isPrerelease` confirmed as real `--json` fields and `--exclude-*` confirmed opt-in against the installed gh. The codecov flip is backed by the step LOG (not the step conclusion, which proves nothing while the flag is false) on the last three main runs: `Get OIDC token` succeeded and "Your upload is now queued for processing" on every interpreter. actionlint passes — it caught a literal template marker inside a comment being parsed as an empty expression. Out of scope, filed as observations rather than silently widened: the verify call pins neither `--signer-workflow` nor `--source-ref` and emits no `--format json` evidence (needs checking against a real 0.17.1 attestation); only 1 of the 21 attested artifacts per release is verified; junit-unit.xml is generated and never uploaded; and the vendored codecov action proceeds after its own CLI signature check prints "Could not verify signature". --- .github/workflows/attestation-check.yml | 53 ++++++++++++++++++------- .github/workflows/ci.yml | 27 ++++++++----- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/.github/workflows/attestation-check.yml b/.github/workflows/attestation-check.yml index 93adf30e..fc586dab 100644 --- a/.github/workflows/attestation-check.yml +++ b/.github/workflows/attestation-check.yml @@ -16,18 +16,29 @@ jobs: - name: Get latest release tag id: release run: | - # "the lookup failed" and "there are no releases" are different facts and - # must not share an exit path. The old `2>/dev/null || echo ""` collapsed - # an API hiccup or a bad token into the same green "skipping" as a repo - # with no releases at all, so a week of attestation verification could go - # un-run and still report success — indistinguishable from healthy. - # `// empty` keeps the genuinely-empty list an empty TAG without relying - # on how gh renders a null jq result. - if ! TAG=$(gh release list --repo "$REPO" --limit 1 --json tagName --jq '.[0].tagName // empty'); then + # Three outcomes, three exit paths. The old `2>/dev/null || echo ""` + # collapsed the first into the third, so a week with no attestation + # verification at all exited 0 and read as healthy: + # 1. lookup failed -> hard fail; we know nothing either way. + # 2. releases exist but none is GitHub's "latest" -> hard fail. On a repo + # that publishes releases that is a tamper or misconfiguration signal, + # and skipping it is the exact shape LAB-984 fell into. + # 3. zero releases at all -> skip; nothing has been published yet. + # + # `isLatest` is GitHub's own newest-non-draft-non-prerelease marker, so it + # pins the artifact users actually install. An unfiltered `--limit 1` can + # return a draft or prerelease, which would either verify an RC green while + # the stable wheel goes unchecked, or fail on a wheel PyPI never got. + if ! RELEASES=$(gh release list --repo "$REPO" --limit 30 --json tagName,isLatest); then echo "::error::gh release list failed for $REPO — cannot determine the latest release; refusing to report a green skip" exit 1 fi + TAG=$(jq -r 'map(select(.isLatest))[0].tagName // empty' <<<"$RELEASES") if [ -z "$TAG" ]; then + if [ "$(jq -r 'length' <<<"$RELEASES")" -ne 0 ]; then + echo "::error::$REPO has releases but none is marked latest (all drafts/prereleases?) — refusing to skip a release that may need attesting" + exit 1 + fi echo "$REPO has no published releases — nothing to attest, skipping" echo "skip=true" >> "$GITHUB_OUTPUT" else @@ -51,8 +62,13 @@ jobs: # `gh attestation` has no `list` subcommand — the old query errored in # ~8s every run (that was the real cause of the weekly red, not missing # attestations). Verify the actual published artifact instead. - VER="${{ steps.release.outputs.tag }}" - VER="${VER#v}" + # + # TAG/REPO arrive via env, never template-interpolated into this body: a + # git tag may legally contain `$(...)` or backticks, and whoever can name + # a tag is exactly the adversary this tripwire exists to catch — + # interpolating it would hand them code execution in a job holding + # GH_TOKEN and issues: write. + VER="${TAG#v}" echo "Verifying attestations for cachekit ${VER}" mkdir -p attest-check if ! pip download "cachekit==${VER}" --no-deps --only-binary :all: -d attest-check; then @@ -67,21 +83,30 @@ jobs: fi for f in "${files[@]}"; do echo "Verifying attestation for $f" - gh attestation verify "$f" --repo ${{ github.repository }} || { + gh attestation verify "$f" --repo "$REPO" || { echo "::error::Attestation verification failed for $f" exit 1 } done env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ steps.release.outputs.tag }} + # Only a real verification failure opens an issue. Without the skip guard a + # failed *lookup* also landed here, filing a public "Attestation verification + # failed for " issue — empty tag, blaming attestations for an API outage that + # never reached the verify step. A guard that misreports is the same trust bug + # as the silence it replaced; for a lookup failure the red run is the signal. - name: Open issue on failure - if: failure() + if: failure() && steps.release.outputs.skip == 'false' run: | gh issue create \ - --repo ${{ github.repository }} \ - --title "Attestation verification failed for ${{ steps.release.outputs.tag }}" \ + --repo "$REPO" \ + --title "Attestation verification failed for $TAG" \ --body "Weekly attestation health check failed. Verify that the release workflow produced valid attestations." \ --label "bug" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ steps.release.outputs.tag }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 881ef92a..a6bb5e05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,26 +158,35 @@ jobs: # project/patch statuses codecov.yml declares, and every flag there sets # `carryforward: true` — so a silently-dropped upload does not remove the # status, it answers "is this PR's new code 80% covered?" with a previous - # run's numbers. A green patch status that measured none of the diff is the - # same manufactured-evidence class this ticket exists to remove, so an - # upload failure has to be red and visible. Fork PRs cannot mint the OIDC - # token this uses (`id-token: write` is never granted to them) and never - # reach the self-hosted `cachekit` runner unapproved, so this cannot redden - # an outside contribution. + # run's numbers. + # + # Scoped to same-repo events rather than a bare `true`, because of what the + # action actually does on a fork (read at the pinned SHA, not assumed): its + # `Get OIDC token` step is guarded `CC_USE_OIDC == 'true' && CC_FORK != 'true'`, + # so on a fork it never attempts OIDC, `CC_TOKEN` stays empty, and the upload + # proceeds TOKENLESS — Codecov's rate-limited path. A bare `true` would let a + # 429 nobody controls redden an outside contribution, which on a repo with no + # branch protection trains maintainers to merge over red CI: the opposite of + # what this ticket is hardening. The flag therefore applies exactly where OIDC + # really authenticates. - name: Upload coverage to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 with: files: ./coverage.xml use_oidc: true - fail_ci_if_error: true + fail_ci_if_error: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} flags: ${{ github.event_name == 'push' && 'full' || 'critical' }}-python-${{ matrix.python-version }} # Stays false, deliberately: junit.xml feeds Codecov Test Analytics # (flaky-test history) only. Nothing gates on it and no status is computed # from it, so a Codecov-side outage here would redden otherwise-passing CI - # while hiding nothing. Losing a run of flake history is not a trust bug the - # way stale coverage is. + # while hiding nothing. + # + # `handle_no_reports_found` is left at its default (false) on both uploads on + # purpose: it would also swallow "the report was never written", which is the + # silent-degradation this ticket exists to remove. A second red step on an + # already-red job is noise; a green job that uploaded nothing is a trust bug. - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 From 79df357fb62f22c8b6e8a54175047bfb80f843d5 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 13:05:49 +1000 Subject: [PATCH 03/10] fix(ci): close the fail-open the first fix introduced; scope the failure issue to the verify step (LAB-2528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second expert-panel pass, run because the fix for a panel's own findings is not covered by that panel. Both agents independently found the same defect, and it is the one this ticket exists to remove — reintroduced two lines below the `|| echo ""` it replaced. MAJ, fail-open (introduced in 228e904): the release-count check was written `if [ "$(jq -r 'length' <<<"$RELEASES")" -ne 0 ]`. Three faults compounding: the command substitution hides jq's exit code, `set -e` does not fire inside an `if` condition, and when `[` itself errors on non-numeric input the test evaluates FALSE — falling straight through to `skip=true` and exit 0. Reachable whenever `gh` exits 0 with empty stdout: jq on empty input exits 0 with no output, so the job reported a green "no published releases, skipping" having verified nothing. Now validated in its own statement with `jq -e 'if type == "array" then length else null end'`, so a parse error, an absent array, a JSON null and an object all land on the annotated hard failure rather than the skip path or a raw jq trace. `--limit` raised 30 -> 100 and the error now reports the actual count, so the window is diagnosable rather than an arbitrary constant that arms itself as the repo grows. MAJ, misleading alarm: `if: failure() && skip == 'false'` fixed the empty-tag case but still filed a public "Attestation verification failed for v0.17.1" issue when `setup-python` failed, or when `pip download` hit a release-day PyPI publish lag or a yank — blaming the release pipeline's attestations for something that never reached the attestation check, on the day maintainers are busiest. Now gated on the verify step's own `steps.verify.outcome`, retitled to "Attestation health check failed" (the step covers both the download and the verification), and the body sends the reader to the log to find out which. ci.yml: the fork scoping is kept, but the comment now states the residual risk it creates instead of only the risk it avoids — on a fork PR a dropped tokenless upload is silent and carryforward answers the patch question with an earlier commit's numbers. Accepted because a fork PR cannot reach the self-hosted runner without a maintainer approving the run; the real fix is a local `--cov-fail-under` floor, tracked separately rather than smuggled in here. Evidence: the harness now asserts eight cases against the step body extracted from the YAML, including the three malformed-payload cases that previously produced a green skip (empty stdout, unparseable stdout, JSON null) — all now non-zero with the annotation. actionlint passes. Deferred with reasons, not silently widened: verifying the newest release in addition to `isLatest` (a publisher can flag a malicious release prerelease and leave `isLatest` on the previous stable — a coverage gap, not a fail-open, and the same "which artifacts should the weekly check cover" question as the already-deferred 1-of-21 artifact gap); `gh issue create` dedup (pre-existing; duplicate weekly issues are noise rather than silence, and the obvious implementation wants a `|| echo 0` swallow this PR is removing). --- .github/workflows/attestation-check.yml | 39 ++++++++++++++++++------- .github/workflows/ci.yml | 10 +++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/attestation-check.yml b/.github/workflows/attestation-check.yml index fc586dab..493619dd 100644 --- a/.github/workflows/attestation-check.yml +++ b/.github/workflows/attestation-check.yml @@ -29,14 +29,28 @@ jobs: # pins the artifact users actually install. An unfiltered `--limit 1` can # return a draft or prerelease, which would either verify an RC green while # the stable wheel goes unchecked, or fail on a wheel PyPI never got. - if ! RELEASES=$(gh release list --repo "$REPO" --limit 30 --json tagName,isLatest); then + if ! RELEASES=$(gh release list --repo "$REPO" --limit 100 --json tagName,isLatest); then echo "::error::gh release list failed for $REPO — cannot determine the latest release; refusing to report a green skip" exit 1 fi + # Validate the payload in its own statement before anything branches on it. + # The `type=="array"` guard makes every non-array shape (null, an object, + # an error envelope) return null, which `jq -e` reports as a non-zero exit + # alongside parse errors and no-output — so empty or truncated stdout from + # a `gh` that still exited 0 lands on the annotated hard failure instead of + # a raw jq trace. Doing this inline as `[ "$(jq ...)" -ne 0 ]` would hide + # jq's exit code in a command substitution, suppress `set -e` inside the + # `if`, and — when `[` itself errored on non-numeric input — evaluate FALSE + # straight into the skip path: the same fail-open shape as the + # `|| echo ""` this commit removes. + if ! COUNT=$(jq -e 'if type == "array" then length else null end' <<<"$RELEASES"); then + echo "::error::gh release list returned no parseable release array for $REPO — refusing to report a green skip" + exit 1 + fi TAG=$(jq -r 'map(select(.isLatest))[0].tagName // empty' <<<"$RELEASES") if [ -z "$TAG" ]; then - if [ "$(jq -r 'length' <<<"$RELEASES")" -ne 0 ]; then - echo "::error::$REPO has releases but none is marked latest (all drafts/prereleases?) — refusing to skip a release that may need attesting" + if [ "$COUNT" -ne 0 ]; then + echo "::error::$REPO has $COUNT release(s) but none is marked latest — refusing to skip a release that may need attesting" exit 1 fi echo "$REPO has no published releases — nothing to attest, skipping" @@ -57,6 +71,7 @@ jobs: python-version: '3.12' - name: Verify attestations + id: verify if: steps.release.outputs.skip != 'true' run: | # `gh attestation` has no `list` subcommand — the old query errored in @@ -93,18 +108,22 @@ jobs: REPO: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} - # Only a real verification failure opens an issue. Without the skip guard a - # failed *lookup* also landed here, filing a public "Attestation verification + # Gated on the verify step's own outcome, not a bare `failure()`. Previously a + # failed *lookup* landed here and filed a public "Attestation verification # failed for " issue — empty tag, blaming attestations for an API outage that - # never reached the verify step. A guard that misreports is the same trust bug - # as the silence it replaced; for a lookup failure the red run is the signal. + # never reached the verify step; a setup-python or PyPI-lag failure did the + # same with a tag attached. A tripwire that cries wolf is the same trust bug + # as one that stays silent, so the title says "health check" (the step covers + # both the PyPI download and the attestation check) and the body sends the + # reader to the log to find out which. For any earlier failure the red run is + # the signal and no issue is filed. - name: Open issue on failure - if: failure() && steps.release.outputs.skip == 'false' + if: failure() && steps.verify.outcome == 'failure' run: | gh issue create \ --repo "$REPO" \ - --title "Attestation verification failed for $TAG" \ - --body "Weekly attestation health check failed. Verify that the release workflow produced valid attestations." \ + --title "Attestation health check failed for $TAG" \ + --body "The weekly attestation health check failed for ${TAG}. Check the run log to see whether the wheel download or the attestation verification failed, then verify that the release workflow produced valid attestations." \ --label "bug" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6bb5e05..68711179 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,6 +169,16 @@ jobs: # branch protection trains maintainers to merge over red CI: the opposite of # what this ticket is hardening. The flag therefore applies exactly where OIDC # really authenticates. + # + # ACCEPTED RESIDUAL RISK, stated because the scoping creates it: on a fork PR a + # dropped tokenless upload is still silent, and carryforward then answers the + # patch question with an earlier commit's numbers — a stale green on exactly the + # least-trusted contributions. Accepted because a fork PR cannot reach the + # self-hosted `cachekit` runner without a maintainer approving the run, so a + # human is already in that loop, and no fork PR has run here to date. The real + # fix is to stop depending on an external upload for the floor (a local + # `--cov-fail-under` on the PR pytest invocation); that is a coverage-policy + # change, tracked separately rather than smuggled into this diff. - name: Upload coverage to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 From 2fc24fc8b492938ad15287551bf99b6fc13d8ea6 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 16:08:47 +1000 Subject: [PATCH 04/10] fix(ci): resolve the latest release server-side instead of paging for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit, PR #270: capping the lookup at N releases means the isLatest release can fall outside the window, leaving TAG empty on a non-empty list and hard-failing a perfectly healthy repo. Raising N only moves the cliff. /releases/latest — what `gh release view` with no tag resolves — is the same newest-non-draft-non-prerelease release the isLatest flag marks, computed server-side, so there is no window for it to fall outside of. `gh release list --limit 1` keeps answering the one question that genuinely needs the listing: does this repo publish anything at all. All three outcomes preserved: lookup failure red, zero releases skip, releases-without-a-latest red. A tripwire that cries wolf is the same trust bug as one that stays silent. Refs LAB-2528 --- .github/workflows/attestation-check.yml | 35 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/attestation-check.yml b/.github/workflows/attestation-check.yml index 493619dd..f5b920ac 100644 --- a/.github/workflows/attestation-check.yml +++ b/.github/workflows/attestation-check.yml @@ -25,12 +25,20 @@ jobs: # and skipping it is the exact shape LAB-984 fell into. # 3. zero releases at all -> skip; nothing has been published yet. # - # `isLatest` is GitHub's own newest-non-draft-non-prerelease marker, so it - # pins the artifact users actually install. An unfiltered `--limit 1` can - # return a draft or prerelease, which would either verify an RC green while - # the stable wheel goes unchecked, or fail on a wheel PyPI never got. - if ! RELEASES=$(gh release list --repo "$REPO" --limit 100 --json tagName,isLatest); then - echo "::error::gh release list failed for $REPO — cannot determine the latest release; refusing to report a green skip" + # Two lookups, each answering exactly one question, because one call + # cannot answer both without guessing a page size. `gh release list` + # is a paged view over ALL releases (drafts included) — it answers + # "does this repo publish anything at all?", and --limit 1 is enough + # for that. `gh release view` with no tag resolves /releases/latest + # server-side: the same newest-non-draft-non-prerelease release the + # `isLatest` flag marks, but with no page window it can fall outside + # of. An earlier form filtered `isLatest` out of a --limit N listing, + # which meant picking an N and hard-failing a perfectly healthy repo + # once the latest release aged past it. A tripwire that cries wolf is + # the same trust bug as one that stays silent, so the window is gone + # rather than widened. + if ! RELEASES=$(gh release list --repo "$REPO" --limit 1 --json tagName); then + echo "::error::gh release list failed for $REPO — cannot determine whether any release exists; refusing to report a green skip" exit 1 fi # Validate the payload in its own statement before anything branches on it. @@ -47,14 +55,17 @@ jobs: echo "::error::gh release list returned no parseable release array for $REPO — refusing to report a green skip" exit 1 fi - TAG=$(jq -r 'map(select(.isLatest))[0].tagName // empty' <<<"$RELEASES") - if [ -z "$TAG" ]; then - if [ "$COUNT" -ne 0 ]; then - echo "::error::$REPO has $COUNT release(s) but none is marked latest — refusing to skip a release that may need attesting" - exit 1 - fi + if [ "$COUNT" -eq 0 ]; then echo "$REPO has no published releases — nothing to attest, skipping" echo "skip=true" >> "$GITHUB_OUTPUT" + # Releases exist, so a latest MUST resolve. Both ways this can fail are + # incidents, not idle states: an API failure means we know nothing, and + # "releases exist but none is latest" (all drafts/prereleases) on a repo + # that publishes to PyPI is tamper or misconfiguration. Skipping either + # is the exact shape LAB-984 fell into, so both go red. + elif ! TAG=$(gh release view --repo "$REPO" --json tagName --jq '.tagName'); then + echo "::error::$REPO has release(s) but no resolvable latest release — refusing to skip a release that may need attesting" + exit 1 else echo "Latest release: $TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" From e04f32e121b699dc8e0c3058729e2983bd50aceb Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 16:08:54 +1000 Subject: [PATCH 05/10] chore(deps): bump pip constraint past PYSEC-2026-3721 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not this PR's subject — riding along because it reds every PR in the repo, including this one, and CI-green is the review gate. pip-audit flagged pip 26.1.2 itself: doubly-encoded package URLs from an index can install files to arbitrary paths on disk, wheels included. Fixed in 26.2; the [tool.uv] constraint pinned the vulnerable floor. Lock resolves to 26.2.1 and nothing else moved. Repo-wide, not branch-specific: main carries the same floor and has not run CI since 2026-08-08, which is why nobody had seen it yet. Refs LAB-2528 --- pyproject.toml | 5 +++-- uv.lock | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3860e791..f3eb8b32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -249,8 +249,9 @@ constraint-dependencies = [ "werkzeug>=3.1.4", # pip is a dev-only transitive dep (pip-audit -> pip-api -> pip). 26.1.2 fixes # PYSEC-2026-196 (entry-point path traversal), GHSA-58qw-9mgm-455v (tar/zip - # confusion) and GHSA-jp4c-xjxw-mgf9 (self-update import ordering). - "pip>=26.1.2", + # confusion) and GHSA-jp4c-xjxw-mgf9 (self-update import ordering); 26.2 fixes + # PYSEC-2026-3721 (doubly-encoded index URLs install to arbitrary paths). + "pip>=26.2", # h2 is a transitive dep (httpx[http2] -> h2). 4.4.1 fixes # GHSA-6hr6-w5qg-qmwg (duplicate Host headers forwarded on HTTP/2 -> # HTTP/1.1 downgrade — request smuggling primitive). diff --git a/uv.lock b/uv.lock index 4f9df255..0281576a 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,7 @@ resolution-markers = [ constraints = [ { name = "fonttools", specifier = ">=4.60.2" }, { name = "h2", specifier = ">=4.4.1" }, - { name = "pip", specifier = ">=26.1.2" }, + { name = "pip", specifier = ">=26.2" }, { name = "urllib3", specifier = ">=2.7.0" }, { name = "werkzeug", specifier = ">=3.1.4" }, ] @@ -1283,11 +1283,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.2" +version = "26.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" }, ] [[package]] From 8ec07c3735ee07aab7ef0ecfe80270aeb9f9e4fd Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 16:29:58 +1000 Subject: [PATCH 06/10] docs(ci): sync the pip floor in both pip-audit rationales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit, PR #270: the constraint moved to 26.2 but the comment still said 26.1.2. Fixed in ci.yml too, not just the file CodeRabbit named — the comment itself says to keep the two identical so they cannot drift, and fixing one half of a keep-in-sync pair is how the drift starts. Refs LAB-2528 --- .github/workflows/ci.yml | 2 +- .github/workflows/security-fast.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68711179..22f227d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,7 +231,7 @@ jobs: - name: Scan Python dependencies for CVEs run: | # No suppressions: every prior CVE is resolved at source on the py3.10+ - # resolution. urllib3>=2.7.0 and pip>=26.1.2 are pinned via + # resolution. urllib3>=2.7.0 and pip>=26.2 are pinned via # [tool.uv] constraint-dependencies; pygments/pyarrow advisories cleared # by their py3.10+ fix versions. Keep this list IDENTICAL to # security-fast.yml's pip-audit so the two cannot drift. diff --git a/.github/workflows/security-fast.yml b/.github/workflows/security-fast.yml index eaa91e0b..f249f321 100644 --- a/.github/workflows/security-fast.yml +++ b/.github/workflows/security-fast.yml @@ -91,7 +91,7 @@ jobs: - name: Run pip-audit run: | # No suppressions: every prior CVE is resolved at source on the py3.10+ - # resolution. urllib3>=2.7.0 and pip>=26.1.2 are pinned via + # resolution. urllib3>=2.7.0 and pip>=26.2 are pinned via # [tool.uv] constraint-dependencies; pygments/pyarrow advisories cleared # by their py3.10+ fix versions. Keep this list IDENTICAL to ci.yml's # post-merge pip-audit so the two cannot drift. From 2ffb843791e131cca472f0d858ace45632673561 Mon Sep 17 00:00:00 2001 From: Winston Date: Sat, 12 Sep 2026 19:04:23 +1000 Subject: [PATCH 07/10] fix(ci): bump codecov-action to v7.0.0 so CLI signature verification can pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pinned v5.5.3 fetches Codecov's GPG key from keybase.io/codecovsecurity, an account Codecov deleted in June 2026 (HTTP 404, "SELF-SIGNED PUBLIC KEY NOT FOUND"). Key import yields "no valid OpenPGP data", `gpg --verify` fails with "No public key", and the wrapper's exit_if_error fires. With the PR's fail_ci_if_error: true on same-repo events that is now a hard failure — the Tests (Python 3.12) job on 8ec07c3 died exactly there. On main (fail_ci_if_error: false) the same failure has been silent: the log prints "Could not verify signature" then "CLI integrity verified" and runs the unverified 10 MB binary anyway. Every green main run since the deletion did this. v7.0.0 (fb8b3582) moves the key URL to keybase.io/codecovsecops, which serves the same key (fingerprint 2703 4E7F DB85 0E0B BC2C 62FF 806B B28A ED77 9869 — the RSA key that signed the failing run's SHA256SUM). The only other change on our code path since v5.5.3 is v6.0.1's template-injection hardening (inputs hoisted into env:). The Get OIDC token fork guard the in-file comment cites is unchanged. node24, which v6+ requires, is already required by checkout@v6 in the same job. Not chosen: skip_validation (disables the check), reverting fail_ci_if_error (defeats LAB-2528), v5.5.5 (node20 compatibility line we have no need for). Refs LAB-3408, LAB-2528. --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22f227d5..7be1635d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,9 +179,13 @@ jobs: # fix is to stop depending on an external upload for the floor (a local # `--cov-fail-under` on the PR pytest invocation); that is a coverage-policy # change, tracked separately rather than smuggled into this diff. + # Pin floor is v7.0.0 (or the v5.5.5 / v6.0.2 backports): every earlier release + # fetches Codecov's signing key from a keybase account Codecov deleted in June + # 2026, so CLI signature verification can never pass there — and with + # fail_ci_if_error false that failure is silent and an unverified binary runs. - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: files: ./coverage.xml use_oidc: true @@ -199,7 +203,7 @@ jobs: # already-red job is noise; a green job that uploaded nothing is a trust bug. - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: files: ./junit.xml report_type: test_results From ade459db9df8b9e9f1c030ea03308ecdb50a813c Mon Sep 17 00:00:00 2001 From: Winston Date: Sat, 12 Sep 2026 19:15:36 +1000 Subject: [PATCH 08/10] fix(ci): make the junit upload enforce the CLI signature too; tighten the pin note Expert-panel pass on 2ffb843 (bug-hunter-supreme and security-specialist, independently): fail_ci_if_error is not just "redden CI on upload error", it is the wrapper's signature-enforcement switch. exit_if_error only exits when CC_FAIL_ON_ERROR=true; with false a failed `gpg --verify` falls through to a same-origin SHA256SUM check, prints "CLI integrity verified", chmods and executes the downloaded binary with the OIDC token in env. The junit step's hard-coded false therefore kept the exact fail-open 2ffb843's own comment describes, two lines below it, and its "hiding nothing" rationale was wrong. The step now sets fail_ci_if_error: true and carries the PR's recorded "a Test Analytics outage must not redden CI" decision in step-level continue-on-error: true instead: the wrapper stops before exec, the step is marked failed-and-continued, the job stays green. Same observable CI outcome on an outage; no unverified execution. Pin note reworded per the panel: dropped the v5.5.5/v6.0.2 parenthetical (v6.0.2 is this very commit; v5.5.5 is the node20 line without v6.0.1's hardening, an in-file licence to downgrade), fixed "every earlier release" (false by version order), gave it its own paragraph, and named the accepted fail-closed: a keybase.io outage now fails same-repo CI red rather than running an unverified binary. Not changed: the coverage step's same-repo scoping. On a fork PR it still runs tokenless with fail_ci_if_error false, so the same unverified-binary path exists there; that is gated by a maintainer approving the run on the self-hosted runner and is the PR author's recorded design. Flagged on LAB-3408 for the owner rather than rewritten here. Refs LAB-3408, LAB-2528. --- .github/workflows/ci.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7be1635d..80a653b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,8 +153,8 @@ jobs: REDIS_URL: redis://localhost:6379 # autouse redis-isolation fixture uses external Redis when set (else spawns a binary the runner lacks) run: uv run pytest tests/performance/ -m "performance and slow" -q - # fail_ci_if_error is deliberately split between these two uploads, not - # defaulted (LAB-2528 finding 3). coverage.xml is the ONLY input to the + # The two uploads below are deliberately treated differently, not defaulted + # (LAB-2528 finding 3). coverage.xml is the ONLY input to the # project/patch statuses codecov.yml declares, and every flag there sets # `carryforward: true` — so a silently-dropped upload does not remove the # status, it answers "is this PR's new code 80% covered?" with a previous @@ -179,10 +179,13 @@ jobs: # fix is to stop depending on an external upload for the floor (a local # `--cov-fail-under` on the PR pytest invocation); that is a coverage-policy # change, tracked separately rather than smuggled into this diff. - # Pin floor is v7.0.0 (or the v5.5.5 / v6.0.2 backports): every earlier release - # fetches Codecov's signing key from a keybase account Codecov deleted in June - # 2026, so CLI signature verification can never pass there — and with - # fail_ci_if_error false that failure is silent and an unverified binary runs. + # + # Do not re-pin below v7.0.0 (fb8b3582): releases published before Codecov's + # June 2026 keybase migration fetch the CLI signing key from a deleted account, + # so `gpg --verify` can never pass. The wrapper stops there only when + # fail_ci_if_error is true; otherwise it prints "CLI integrity verified" and runs + # the unverified binary. Accepted flip side: a keybase.io outage now fails + # same-repo CI closed, not open. - name: Upload coverage to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 @@ -192,10 +195,12 @@ jobs: fail_ci_if_error: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} flags: ${{ github.event_name == 'push' && 'full' || 'critical' }}-python-${{ matrix.python-version }} - # Stays false, deliberately: junit.xml feeds Codecov Test Analytics - # (flaky-test history) only. Nothing gates on it and no status is computed - # from it, so a Codecov-side outage here would redden otherwise-passing CI - # while hiding nothing. + # fail_ci_if_error is true here too, for a different reason: it is also the + # wrapper's signature-enforcement switch (see the pin note above), so false + # would let an unverified binary run. The "don't redden CI" intent lives in + # continue-on-error instead: junit.xml feeds Codecov Test Analytics (flaky-test + # history) only and nothing gates on it, so a Codecov-side outage marks this + # step failed-and-continued and the job stays green. # # `handle_no_reports_found` is left at its default (false) on both uploads on # purpose: it would also swallow "the report was never written", which is the @@ -203,13 +208,14 @@ jobs: # already-red job is noise; a green job that uploaded nothing is a trust bug. - name: Upload test results to Codecov if: ${{ !cancelled() }} + continue-on-error: true uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: files: ./junit.xml report_type: test_results use_oidc: true flags: ${{ github.event_name == 'push' && 'full' || 'critical' }}-python-${{ matrix.python-version }} - fail_ci_if_error: false + fail_ci_if_error: true # Version sync + doc tests (push to main only) post-merge: From 09d508a606881e530d0042ac32ef2a01e7439318 Mon Sep 17 00:00:00 2001 From: Winston Date: Sat, 12 Sep 2026 19:27:52 +1000 Subject: [PATCH 09/10] docs(ci): record the fork-PR signature residual; scope the no-reports rationale Second expert-panel pass on ade459d, comment accuracy only, no behaviour change: - The ACCEPTED RESIDUAL RISK paragraph named only the silent tokenless drop on fork PRs. It now also names that the wrapper's CLI signature check is unenforced there (fail_ci_if_error is its switch), and states why that is accepted: a tampered cli.codecov.io binary cannot target fork runs selectively, so it hits same-repo runs first, where the step fails closed. "A human is already in that loop" was dropped: maintainer approval vets the PR's code, not Codecov's CDN. - The handle_no_reports_found paragraph claimed "a green job that uploaded nothing is a trust bug" for both uploads, while the junit paragraph above it now deliberately accepts a green job on an outage. Scoped to coverage.xml; on junit.xml the default keeps the failed step visible as an annotation. Refs LAB-3408, LAB-2528. --- .github/workflows/ci.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a653b3..2cbea42e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,9 +173,11 @@ jobs: # ACCEPTED RESIDUAL RISK, stated because the scoping creates it: on a fork PR a # dropped tokenless upload is still silent, and carryforward then answers the # patch question with an earlier commit's numbers — a stale green on exactly the - # least-trusted contributions. Accepted because a fork PR cannot reach the - # self-hosted `cachekit` runner without a maintainer approving the run, so a - # human is already in that loop, and no fork PR has run here to date. The real + # least-trusted contributions. The wrapper's CLI signature check is likewise + # unenforced there (fail_ci_if_error is its switch; see the pin note below). + # Accepted because a fork PR cannot reach the self-hosted `cachekit` runner + # without a maintainer approving the run, no fork PR has run here to date, and + # a tampered binary reaches same-repo runs first, where it fails closed. The real # fix is to stop depending on an external upload for the floor (a local # `--cov-fail-under` on the PR pytest invocation); that is a coverage-policy # change, tracked separately rather than smuggled into this diff. @@ -204,8 +206,10 @@ jobs: # # `handle_no_reports_found` is left at its default (false) on both uploads on # purpose: it would also swallow "the report was never written", which is the - # silent-degradation this ticket exists to remove. A second red step on an - # already-red job is noise; a green job that uploaded nothing is a trust bug. + # silent-degradation this ticket exists to remove. On coverage.xml a green job + # that uploaded nothing is a trust bug; on junit.xml continue-on-error already + # accepts a green job, and false there keeps the failed step visible as an + # annotation instead of hiding it. - name: Upload test results to Codecov if: ${{ !cancelled() }} continue-on-error: true From d0bab42d70b4fecb77851ed684088666335cc7c5 Mon Sep 17 00:00:00 2001 From: Mark S Date: Sun, 13 Sep 2026 13:27:36 +1000 Subject: [PATCH 10/10] docs(ci): record why continue-on-error on the junit upload is visibility-only Kody read the junit step's `continue-on-error: true` as defeating the `fail_ci_if_error: true` signature guarantee and proposed flipping the latter to false. Checked against dist/codecov.sh at the pinned SHA, that proposal reopens the exact hole ade459d closed, and the reading conflates "the job goes red" with "the wrapper refuses to execute". Comment only, no behaviour change; it records the two facts the next reader would otherwise have to re-derive from the wrapper: - continue-on-error is job-level and cannot reach inside the step. With CC_FAIL_ON_ERROR=true, exit_if_error exits at the failed gpg --verify (:133), before chmod +x (:142) and before the CLI runs (:268). A bad signature here runs no downloaded code; only the red X is downgraded to an annotation. - The coverage step above is the integrity canary for same-repo events: same action SHA, same `latest` CLI, same keybase key and SHA256SUM, hard-fail, and it runs first. A signature that cannot pass reddens the job there. No integrity-only failure signal exists to build a separate check on: every error class shares exit 1, the action declares no outputs, and a later step cannot read an earlier step's log. Refs LAB-3408, LAB-2528. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cbea42e..2835d675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,6 +203,11 @@ jobs: # continue-on-error instead: junit.xml feeds Codecov Test Analytics (flaky-test # history) only and nothing gates on it, so a Codecov-side outage marks this # step failed-and-continued and the job stays green. + # That downgrade is visibility only: the wrapper exits before it chmods or runs + # the binary, so a bad signature here still runs no downloaded code. Nor does it hide + # an integrity failure on same-repo events: the coverage step above verifies the + # same CLI, key and checksum hard-fail first, so a signature that cannot pass + # reddens the job there before this step runs. # # `handle_no_reports_found` is left at its default (false) on both uploads on # purpose: it would also swallow "the report was never written", which is the