ci(contract): catch a stale client-runtime pin, which nothing did - #1043
ci(contract): catch a stale client-runtime pin, which nothing did#1043LukasWodka wants to merge 5 commits into
Conversation
…ckend#2460) The envelope-contract drift job compares the vendored contracts against the PINNED client-runtime ref. That is deliberate -- an unrelated upstream commit must not redden every open PR here -- but it means the job cannot see the pin itself going stale: it compares v2 against v2 and agrees. Measured, not hypothetical. client-runtime#376 merged 2026-08-24 and the pin comment carried a note to re-point once it did. That note went unactioned for 2.5 weeks, during which client-runtime#544 landed envelope contract v3 -- and this repo went on verifying both installers against v2, with the drift job GREEN throughout. The pin comment claimed "the weekly run is what catches a pin gone stale enough to matter". No job compared the pin against the producer's default branch, so nothing caught it: a comment asserting a property the code did not have. So this adds the check the comment described: * `pin-staleness` fetches envelope_contract.json at the pinned ref AND on client-runtime's default branch and compares them. Both reads FAIL CLOSED -- a fetch that did not happen is not evidence the pin is fresh, and two unknowns must never compare equal; * it WARNS on a pull_request and FAILS on the weekly/dispatch run. Failing a PR would punish whoever happens to touch an installer line for a pin somebody else left behind, which is how a tier gets skipped. The scheduled run is where the alarm is actionable and blocks nobody; * the failure message names every surface adoption has to touch, because that was not obvious: re-vendoring the JSON is not enough -- gen-envelope-embed.sh also rewrites the bash vectors AND the embedded literals in scripts/lib/install-client-helm.sh and scripts/install-k8s.ps1. `scripts/contract-summary.py` prints `<version>|<cpu>m / <mem> MiB` for a contract file. It exists because the first version of this job inlined the reader as a heredoc inside a YAML block scalar, which broke the block's indentation and is unreadable even when it parses. stdlib only, and it raises rather than degrading to "unknown" on a file it cannot read. Verified against the real refs, both directions: pinned e4c4813 reads v2 (1000m / 3072 MiB) against develop's v3 (900m / 3136 MiB) -> STALE, guard fires; with the pin set to develop -> silent. WHAT THIS DELIBERATELY DOES NOT DO: adopt v3. v3's overhead is the control-plane footprint of chart 1.9.109, and the interim trim moved that at 1.9.112 (the chart now renders 2272 MiB / 650 m). Adopting v3 alone would shrink the smallest envelope and re-break the 2-core / 8 GiB node that "the ticket's 8 GiB node now FITS unreduced" pins -- 5120 + 3136 > 8192 MiB. Adoption waits for a contract regenerated against the chart as it actually renders, and the trim's own PR says the requests were never sized from usage (1050m / 3276 Mi requested against 36m / ~1022 Mi used), so that number is still moving. This change makes the gap VISIBLE in the meantime, which is the part that was missing. scripts/manifest.sha256 needs no update: the manifest's surface is the set the bootstrap fetches, and CI tooling is not in it. `--check` confirms it is current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
… (backend#2460)
Both Bugbot findings, both right.
HIGH: `pin-staleness` read the PRIVATE tracebloc/client-runtime with
GITHUB_TOKEN, which is scoped to THIS repo and cannot see a sibling. So
both Contents reads failed, the fail-closed branch fired, and the job went
red with a cannot-read error on every run instead of comparing pins --
visible on this PR own first run. The warn-on-PR path could never even
be reached.
The fix was already in this file. The envelope-contract job twenty lines
below mints an App installation token and its comment says exactly this:
"GITHUB_TOKEN is scoped to THIS repo and cannot read another private one".
I added a second cross-repo reader without reading the first. Same App,
same least privilege -- named `repositories`, contents:read only.
MEDIUM: the curl calls had no bounds and the job no timeout-minutes, so a
stalled api.github.com would hang to the Actions six-hour cap -- a
cannot-tell that never fails. Now --connect-timeout 10 --max-time 60
--retry 2 per read, and timeout-minutes: 10 on the job, so a read that
cannot finish fails rather than hangs.
The other reds on the previous run were not this PR: the opensuse prereq
job failed on distro-mirror connectivity and says so itself ("not this
PR. Re-run this job"), and the cancelled Helm runs were superseded by
later pushes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…ns (backend#2460) Bugbot, and it is the sharpest finding on this PR: the guard reproduced the defect it exists to close, one file over. `scripts/.client-runtime-ref` governs TWO vendored contracts and says so in capitals -- "ONE REF, TWO CONTRACTS since backend#2378 ... Bump it, and re-vendor BOTH". The first version of `pin-staleness` compared only envelope_contract.json and then printed "the pin is current". An upstream-only change to log_redactions.json would have left the pin stale with this new guard reporting GREEN -- exactly the silent green it was written to end. Checking one of two and reporting on both is worse than not checking, because it looks like cover. I quoted that "ONE REF, TWO CONTRACTS" comment earlier in this same change while choosing which ref to pin to, and still wrote a one-file loop. Now both are fetched and compared, the contract list is one variable so a third is one line, each read still fails closed independently, and the message names which contract moved. The envelope contract keeps its version summary in the output because it has one; log_redactions.json is reported by name. Exercised against the real refs, three ways: pin e4c4813 (pre-#544) envelope DIFFERS, redactions match -> STALE pin = develop both match -> current envelope match + redactions DIFFER -> STALE redactions moved (Bugbot's case; the one-file check reported "current" on these inputs) A fourth, unplanned: `ba58303^` predates log_redactions.json existing, so the read 404s and the job refuses with "cannot read" rather than passing. That is the fail-closed path working on a real ref. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…g the verdict (backend#2460) Two more from Bugbot, both right. MEDIUM — unvalidated ref. `pin-staleness` interpolated the pin into a contents API URL with no shape check, while the sibling `envelope-contract` job gates the same value before handing it to checkout. This job is a SECOND place the pin escapes into, so it needs its own gate rather than trusting that the other job ran first. Same expression: no path traversal, no option-looking refs. Exercised: a real SHA and `develop` accepted; `../../etc/passwd`, `--upload-pack=evil`, `a;rm -rf /` and empty refused. MEDIUM — and this one defeated the job's own design. `contract-summary.py` ran under `set -e` AFTER the comparison but BEFORE the warn-on-PR branch. So a develop-side schema change -- exactly the stale case this job exists to surface -- made the display helper raise, aborted the script, and FAILED a pull request that was supposed to warn. The warn path was unreachable in its most important case. The version line is cosmetic: `cmp` already decided, above. It is now `|| true` with an explicit fallback message, so a summary that cannot be produced says so and the verdict stands. The reads that MATTER still fail closed -- an unfetchable contract still exits 1, because that is a cannot-tell about the comparison itself rather than about how to print it. Verified: a contract file with an unrecognised schema yields an empty summary and the script CONTINUES to the verdict, where before `set -e` ended the run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Re-checked at b9983ca4 — none of my five comments below have been addressed yet (no code change, no reply). Re-flagging so they don't get lost under the resolved Bugbot threads:
-
scripts/.client-runtime-refline 7 — the false claim "the weekly run is what catches a pin gone stale enough to matter" is removed here, but an identical copy of that sentence is still sitting in.github/workflows/envelope-contract-drift.yml's header comment (# weekly run is what catches a pin gone stale enough to matter.). The PR's own thesis makes that line false too — please fix both instances in this PR, not just one. -
envelope-contract-drift.ymlline 145 — the comment claimsCONTRACTSis a "left half is the path in client-runtime, right half is where this repo vendors it" mapping, but the value ("envelope_contract.json log_redactions.json") is a flat list — both entries are fetched from client-runtime, neither is a vendored-in-this-repo path. This is exactly the "comment asserting a property the code doesn't have" pattern the PR exists to eliminate. Please drop the left/right framing or implement it. -
envelope-contract-drift.ymlline 163 — every fail-closedexit 1(empty ref, invalid ref shape, unreadable pinned/develop contract) fires unconditionally, before theEVENT = pull_requestbranch further down. That means a fork PR (no secrets → token mint fails) or a transient client-runtime API hiccup reddens the PR outright — the exact outcome the "warns on a PR, fails on the schedule" design was written to avoid. Please route read/validation failures through the same warn-on-PR gate as the staleness verdict, or explicitly narrow the stated guarantee. -
envelope-contract-drift.ymlline 160 —/tmp/pinned-$path//tmp/head-$pathonly works because both current contract paths are flat filenames. Add a third contract that lives under a subdirectory and the redirect target's parent won't exist, aborting the job underset -e— contradicting the "adding a third contract is one line" comment. Sanitize the temp filename (e.g.${path//\//_}). -
envelope-contract-drift.ymlline 162 — the develop-branch comparison target is hardcoded to the literaldevelop, but the job/PR describe it as comparing against client-runtime's default branch. They coincide today, so not a live bug, but if client-runtime's default branch is ever renamed this silently 404s. Consider deriving it or naming the coupling explicitly in the comment.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Strong guard, and mutation-proof where it counts: this PR's own pin-staleness run was fed the genuinely-stale current pin and fired the STALE path (envelope v2 vs develop v3, DIFFERS, ::warning::PIN STALE), while correctly reporting log_redactions.json matches — so it discriminates per-contract and compares against upstream develop, not itself. CI is fully green (mutation-check, both Bugbots, drift all pass), the 5 Bugbot threads are all resolved with fixes visible in the diff, and the manifest claim checks out (CI-only files, drift green).
Two non-blocking self-consistency fixes, worth doing precisely because rule 7 is this PR's whole thesis:
-
The false claim you delete from
scripts/.client-runtime-ref— "the weekly run is what catches a pin gone stale enough to matter" — still sits verbatim in the header of the workflow you're editing (.github/workflows/envelope-contract-drift.yml, ~line 24). The fix is half-applied; the same rule-7 assertion survives one comment over. -
The
CONTRACTScomment says "Left half is the path in client-runtime, right half is where this repo vendors it," but the value is a flat two-path list of producer paths — there's no left/right pairing. That's a comment asserting a structure the code doesn't have.
Both are one-line comment edits. Findings 3–5 (cannot-read reddens a PR; /tmp/pinned-$path slash-safety; hardcoded develop vs "default branch") are inline as design/latent notes, not blockers. No approval yet — happy to approve once 1–2 are tidied.
…his PR deletes (backend#2460) Five review findings, all of them the same defect this job exists to catch. 1+2 (blocking): the false claim removed from scripts/.client-runtime-ref still sat verbatim in this workflow header, and the CONTRACTS comment described a producer/vendored left-right mapping over a flat list of producer paths. Both were comments asserting properties the code does not have. 3: every fail-closed exit fired before the EVENT branch, so a fork PR (no token) or a transient API hiccup reddened the PR outright -- the outcome warn-on-PR exists to avoid. Read failures now route through cannot_tell(). The split is deliberate: an empty or malformed ref IS the PR diff and still fails hard, since warning there would let a PR land a broken pin behind a message nobody reads. 4: /tmp/pinned-$path only worked because both paths are flat. Proven: with a third contract under a subdirectory the old script aborts under set -e with "/tmp/pinned-schemas/nested_contract.json: No such file or directory"; the sanitised name handles it. 5: the comparison target was the literal develop while the job name and PR say default branch. Now derived from the producer. Verified: yaml parses, bash -n, shellcheck -S warning clean, actionlint clean, and 8 stubbed cases pin the warn/fail split (read-fail PR exit 0 + warning, read-fail schedule exit 1, bad ref exit 1 on a PR, clean run exit 0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
|
All five addressed in
One deliberate deviation, on finding 3. I did not route the ref checks through the warn gate — only the reads. An empty or malformed ref is this PR's own diff ( Two things proved rather than asserted: Finding 4 — old vs new step, same stub, with a third contract added under a subdirectory: Your predicted failure reproduces exactly. Finding 3 — 8 stubbed cases pin the split: read-fail on a PR → exit 0 + Also ran: yaml parse, |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8dd5042. Configure here.
| private-key: ${{ secrets.RELEASE_TRAIN_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| repositories: client-runtime | ||
| permission-contents: read |
There was a problem hiding this comment.
Token step still fails pull requests
Medium Severity
The create-github-app-token step has no continue-on-error or outcome check, so a fork or Dependabot pull_request (empty RELEASE_TRAIN_APP_* secrets) or an App outage fails the job before cannot_tell runs. That still reddens the PR instead of warning, which is the outcome the job's comments say they avoided.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8dd5042. Configure here.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Four of the five points are cleanly fixed and well-verified — thanks for the thorough replies and the mutation-style proofs on each (temp-filename sanitization, both-contracts check, derived default branch, the duplicate false comment).
One is still open, and it's the sharpest of the five. The cannot_tell() gate correctly routes the read failures (contract fetch, default-branch fetch) through warn-on-PR/fail-on-schedule — good, and the choice to keep ref-shape validation hard-failing (since that's this PR's own diff surface) is reasonable and well-argued. But the token-minting step (create-github-app-token, minting the client-runtime reader) has no continue-on-error or outcome check ahead of it. On a fork PR — this repo is public — or during an App outage, that step fails outright and the job dies before cannot_tell() ever runs. That's exactly the fork-PR case point 3 named, just one step earlier than where the fix landed.
Cursor's own new finding on this head (.github/workflows/envelope-contract-drift.yml#L121, "Token step still fails pull requests") says the same thing — please address that before this is mergeable, either with continue-on-error: true + an explicit outcome check feeding cannot_tell(), or an equivalent guard around the token step.
Everything else looks good to me.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Re-reviewed at 8dd5042 — both of my rule-7 self-consistency nits are addressed, thanks:
- The false "the weekly run is what catches a pin gone stale enough to matter" claim in
envelope-contract-drift.yml's header is gone — the comment now explicitly names it as the half-applied fix and points atpin-stalenessas what actually catches a stale pin. - The
CONTRACTScomment now correctly describes a flat list of producer paths ("neither is a path in this repo"), matching the value.
I'm satisfied on my findings. The one thing still holding an approve is the open Cursor/Bugbot thread at envelope-contract-drift.yml:121, and I verified it's on-point: the workflow triggers on pull_request to develop/main, and the Mint a read-only installation token step has no continue-on-error. On a fork PR (which, as your own comment at ~line 167 notes, mints no token) the create-github-app-token step fails, and with no continue-on-error the job reds there — before the cannot_tell() warn-on-PR logic you built ever runs. That's the exact "reddening an unrelated PR" outcome the split was written to avoid, defeated one step up.
Fix would be continue-on-error: true on the token step plus treating an empty steps.token.outputs.token downstream as a cannot_tell (warn on PR, fail on schedule) — or, if fork PRs can't reach this workflow in practice, resolving the Bugbot thread with why. Happy to approve once that thread is closed.


Why
The envelope-contract drift job compares this repo's vendored contracts against the pinned
client-runtimeref. That's deliberate — an unrelated upstream commit must not redden every open PR here — but it means the job cannot see the pin itself going stale. It compares v2 against v2 and agrees.This is measured, not hypothetical:
client-runtime#376merged 2026-08-24, andscripts/.client-runtime-refcarried a note to re-point once it did.client-runtime#544landed envelope contract v3.The pin comment claimed "the weekly run is what catches a pin gone stale enough to matter." No job compared the pin against the producer's default branch, so nothing caught it. A comment asserting a property the code did not have.
What this adds
pin-staleness— fetchesenvelope_contract.jsonat the pinned ref and onclient-runtime's default branch, and compares them.pull_request, fails on the weekly/dispatch run. Failing a PR would punish whoever happens to touch an installer line for a pin somebody else left behind — which is how a whole tier gets skipped. The scheduled run is where the alarm is actionable and blocks nobody.gen-envelope-embed.shalso rewrites the bash vectors and the embedded literals inscripts/lib/install-client-helm.shandscripts/install-k8s.ps1. Four surfaces, one bump.scripts/contract-summary.py— prints<version>|<cpu>m / <mem> MiBfor a contract file. It exists because my first version of this job inlined the reader as a heredoc inside a YAML block scalar, which broke the block's indentation and is unreadable even when it parses. stdlib only, and it raises rather than degrading tounknownon a file it cannot read.Verified against the real refs, both directions:
e4c4813(current)What this deliberately does NOT do: adopt v3
v3's overhead is the control-plane footprint of chart 1.9.109. The interim trim moved that at 1.9.112 — the chart now renders 2272 MiB / 650 m. Adopting v3 alone would shrink the smallest envelope and re-break the 2-core / 8 GiB node that
install_client_helm: the ticket's 8 GiB node now FITS unreducedpins:I tried the adoption first and backed it out: it reddens 11 expectations here and 16 upstream, all hand-written independent pins, and the interim trim's own PR says the requests were never sized from usage (1050m / 3276 Mi requested against 36m / ~1022 Mi used). So the number is still moving, and baking it in now means redoing 27 pins twice.
Adoption waits for a contract regenerated against the chart as it actually renders. This change makes the gap visible in the meantime, which is the part that was missing.
Test plan
envelope-contract-drift.ymlparses; both jobs enumerate (pin-staleness,envelope-contract)contract-summary.py:black+ruffclean; raises on a non-integercontract_versionand on a missing file rather than printingunknownscripts/gen-manifest.sh --check— up to date, and unchanged: the manifest's surface is the set the bootstrap fetches, and CI tooling is not in itNote
Low Risk
CI and documentation only; no installer or contract adoption in this PR. Touches cross-repo GitHub API reads and secret-scoped App tokens in the workflow.
Overview
Adds a
pin-stalenessCI job so a stalescripts/.client-runtime-refis no longer invisible while the existing drift job stays green (it only compares vendored files to the pinned upstream ref).The job fetches
envelope_contract.jsonandlog_redactions.jsonat the pin and on client-runtime’s default branch (via a read-only App token), fails closed on unreadable API responses, and warns onpull_requestvs errors on schedule/dispatch when upstream has moved. Comments in the workflow andscripts/.client-runtime-refdrop the incorrect claim that the weekly drift run alone catches a stale pin.scripts/contract-summary.pyis a stdlib helper that prints envelope version and overhead for log output; it errors on bad input instead of silently reporting “unknown.”Reviewed by Cursor Bugbot for commit 8dd5042. Bugbot is set up for automated code reviews on this repo. Configure here.