Skip to content

ci: pin every Rust build from one reviewed setup - #490

Merged
pawellisowski merged 3 commits into
mainfrom
routine/guardrails-2026-09-04
Sep 8, 2026
Merged

ci: pin every Rust build from one reviewed setup#490
pawellisowski merged 3 commits into
mainfrom
routine/guardrails-2026-09-04

Conversation

@pawellisowski

@pawellisowski pawellisowski commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What this fixes

The repository pins Rust to one reviewed compiler version, but two build jobs did not reliably use it:

  • the packaged connection-reader job explicitly used Rust 1.88.0;
  • the release job installed whatever stable meant that day, so the three steel-detailer binaries in each release could be built with a different compiler from the CLI and CI.

Both jobs now read the version from cli/rust-toolchain.toml, install it, and only then build. That aligns every current workflow Cargo build on one reviewed compiler version.

Scope decision

An earlier version included a custom repository guard. Review showed that the guard was growing into a partial model of GitHub Actions and shell behavior, so the operator chose to remove it from this PR. This PR now contains only the verified workflow correction; guard design can be addressed separately.

Verification

  • Workflow YAML parsed successfully
  • cargo fmt --all -- --check
  • cargo clippy --all-targets --locked -- -D warnings
  • cargo test --locked — 1,224 unit tests passed, 0 failed, 1 ignored; all integration suites passed
  • Steel-detailer workspace — fmt and clippy clean; 23 tests passed
  • Agent Python suites — 5 passed, 2 host-dependent tests skipped, 0 failed
  • All 14 GitHub checks passed on 0cfa50a81
  • Fresh local Codex review of the complete narrowed diff found no actionable issue on 0cfa50a81
  • Final GitHub Codex review found no major issues on 0cfa50a81

Review rounds: 9 total — the custom guard was removed after its bounded review loop; the final workflow-only scope passed both local and GitHub Codex review

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T19:30:30.335496Z 0cfa50a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a59076654

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
panic!("{name}: job `{}` has no step with `id: {id}`", job.key)
});
assert!(
run_blocks(&source.body).contains("cli/rust-toolchain.toml"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the assignment that produces the channel

If this step is later changed to hard-code channel=stable or read another source while retaining the existing error text or a shell comment mentioning cli/rust-toolchain.toml, this assertion still passes because it checks only for that substring anywhere in the run block. The other assertions merely validate the output expression and step ID, so the new gate can remain green while a cargo job has stopped using the pin; validate the command that assigns/emits channel rather than an unrelated textual mention.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +276 to +278
if path.extension()? != "yml" {
return None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include .yaml files in the workflow scan

When a cargo-building GitHub Actions workflow is added with the equally supported .yaml extension, this filter silently excludes the entire file, allowing that job to omit or restate the compiler pin while this gate stays green. The current tree happens to use only .yml, but the claimed repository-wide invariant should scan both workflow extensions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Both P2 findings fixed in 4e7eebce, plus a third defect they led me to. Thanks — the first one was the important one, and it was worse than described.

P2 — "Verify the assignment that produces the channel". Correct, and the check was already vacuous before any future edit: ci.yml's pin step names cli/rust-toolchain.toml three times — in a comment, in the sed that reads it, and in the ::error:: message — so a contains over the run block was satisfied by the prose independently of the command. Measured against the real tree: replacing the sed with channel=stable and leaving the error text alone kept all eleven tests green.

Now it finds the line that assigns channel (command position, so the echo "channel=$channel" that emits it and the ::error:: that names the path are both excluded) and requires that line to read the pin file. Re-run of your scenario:

ci.yml: job `bridge-windows-packaged` takes its toolchain from step `pin`, but the
line that assigns it — `channel=stable` — does not read cli/rust-toolchain.toml, so
the version it emits is not the pin. A mention of the path elsewhere in the script
(an `::error::` message, a comment) is prose and does not count.

Added a second assertion while there: the step must publish channel= to $GITHUB_OUTPUT, since one that computes the pin and never writes it hands the action an empty input.

P2 — "Include .yaml files in the workflow scan". Correct and accepted. Now scans both, and rather than trusting the filter, a new test asserts the scan reaches every workflow file on disk. Control — a .yaml workflow restating 1.88.0:

zz-probe.yaml: job `probe` pins `toolchain: 1.88.0` literally. The pin lives in
cli/rust-toolchain.toml and must be read, never restated …

I also made an unreadable workflow a hard failure rather than a silent skip, on the same reasoning: dropping a file from the scan shrinks coverage with no signal.

Third defect, found while fixing the above. toolchain_input returned Some("") for a bare toolchain: key. YAML resolves that, ~ and null to null, the action falls back to its ref's default channel, and the compiler is unpinned — the absent case. The gate reported it as pins `toolchain: ` literally, naming the wrong defect. All three now read as absent, which routes to the correct message.

Both of your scenarios passed silently before and fail now; three classifier contract tests cover the new readers, including the gutted script as an explicit pair against the real one. cargo test 1573 → 1576, fmt and clippy clean on the pinned 1.95.0.

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e7eebce81

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +123 to +125
let is_key = line.starts_with(" ")
&& !line.starts_with(" ")
&& line.trim_end().ends_with(':')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse workflow structure without fixed indentation

When a new .yml or .yaml workflow uses valid four-space indentation beneath jobs:, this condition creates no Job because it accepts only exactly two spaces; similarly, steps() accepts only six-space sequence entries. The file-count assertion still passes because workflows() read the file, while the named coverage assertion checks only the four existing jobs, so a cargo-building job in that file is silently omitted and may use an unpinned compiler. Derive indentation from the YAML structure or use a YAML parser rather than fixing it to the current files' layout.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +285 to +290
fn emits_channel_output(shell: &str) -> bool {
shell
.lines()
.map(str::trim)
.filter(|line| !line.starts_with('#'))
.any(|line| line.contains("GITHUB_OUTPUT") && line.contains("channel="))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require redirection to the actual output file

If the publishing line is accidentally changed to something such as echo "channel=$channel" >> "${GITHUB_OUTPUT}.bak", this predicate remains true even though the step never publishes steps.<id>.outputs.channel. The action then receives an empty toolchain and falls back to its default, precisely the regression this new assertion is intended to prevent; verify that channel= is redirected to $GITHUB_OUTPUT, rather than merely requiring both substrings somewhere on the line.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Both fixed in 39968ffb. The first one was a genuine hole and the fix is a rewrite rather than a patch.

P2 — "Parse workflow structure without fixed indentation". Confirmed against the real gate before changing anything: a four-space workflow whose cargo job pinned 1.88.0, dropped into .github/workflows/, left all fourteen tests green. Exactly as you said, neither coverage assertion helps — the file-count check passes because the file was read, and the named-job check only asserts the four known jobs are still found. A gate that scrapes text can be blind to a whole file while reporting that it read it.

Rather than deriving the indentation, I took the other option you offered and used a parser. serde_yaml is already a dependency of this crate, so jobs, steps, uses, id, run and with.toolchain now all come from the parsed document. That removes the class instead of the instance — block-scalar forms (|, >, |-, >-), the env:-is-not-script distinction, and the absent/null/empty toolchain: collapse all fall out of the parse, and ~60 lines of hand-rolled scanning went with them. An unparseable workflow is now a hard failure rather than a silent skip.

zz-q.yml: job `probe` pins `toolchain: 1.88.0` literally. The pin lives in
cli/rust-toolchain.toml and must be read, never restated …

P2 — "Require redirection to the actual output file". Correct. It now checks the redirection target itself: the first word after >>, unquoted, must be $GITHUB_OUTPUT or ${GITHUB_OUTPUT}. Your exact case:

ci.yml: job `bridge-windows-packaged` reads `steps.pin.outputs.channel`, but step
`pin` never writes `channel=` to $GITHUB_OUTPUT — the action would receive an empty
toolchain and fall back to its ref's default.

Controls. Five now, all run against the real tree, each previously silent in at least one round: a restated 1.88.0; channel=stable with every mention of the pin file retained; a .yaml workflow; a four-space .yml workflow; and a publish redirected to .bak. All five fail naming the file, job and offending value. Two new tests cover the parser across indentation, flow-style mappings and every block-scalar form, and the publish check across quoting variants.

I also wrote the gate's limits into its doc, since three rounds of findings here were all "this check is narrower than it looks": it proves a job installs the pin, never that the compiler can build the crate; its reach stops at .github/workflows/, so a cargo call inside a script the workflow invokes is outside it; and it knows dtolnay/rust-toolchain specifically.

cargo test 1576 → 1578; fmt and clippy clean on the pinned 1.95.0.

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39968ffb3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
.map(str::trim)
.filter(|line| !line.starts_with('#'))
.any(|line| {
line.contains("channel=") && line.split(">>").skip(1).any(redirects_to_github_output)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the emitted output uses the parsed channel

The fresh gap is downstream of the now-checked assignment: if the script keeps channel=$(sed ... cli/rust-toolchain.toml) but changes the publish command to echo "channel=stable" >> "$GITHUB_OUTPUT", this predicate still passes because it checks only the output key and redirection target. The action then receives the moving stable channel while every gate remains green; require the emitted value to reference the parsed $channel variable.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +409 to +410
if !job.steps.iter().any(|step| invokes_cargo(run_blocks(step))) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require toolchain installation before cargo runs

Because this only checks whether any step invokes cargo and later collects toolchain actions from the entire job, step order is ignored. In release.yml's build job, moving Install Rust below Build steel-detailer lookup binaries would therefore leave that build on the runner's default compiler while this gate still passes; compare step indices and require the pinned installation to precede every relevant cargo invocation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Both fixed in 47460ee5. The ordering one was the most serious finding of the three rounds, because it is the defect this branch exists to fix, sitting inside the gate meant to prevent it.

P2 — "Require toolchain installation before cargo runs". Correct. The scan asked whether a job contains a cargo call and whether it contains an install, never which came first. CargoJob now carries step positions and the first install must precede the first cargo invocation. Your exact scenario, applied to the real release.yml:

release.yml: job `build` installs its Rust toolchain at step 8, but already invokes
cargo at step 6 (`Build Rust CLI (release)`). Steps run in order, so that cargo call
uses whatever compiler the runner shipped, not the pin. Move the toolchain install
above it.

P2 — "Verify the emitted output uses the parsed channel". Also correct, and it composes nastily with the round-two fix: the assignment check passes, the redirect-target check passes, and echo "channel=stable" >> "$GITHUB_OUTPUT" still hands the action a moving channel. The emitted value must now be the $channel the script parsed.

Fixing that exposed a third, smaller thing: one boolean could not distinguish "writes nothing to $GITHUB_OUTPUT" from "writes the wrong value there", and reported the former for both. Different mistakes, different fixes, so they now get different sentences — verified by planting each mutation separately and reading the two messages back.

Controls are now seven, each run against the real tree and each silent before the round that added it: a restated 1.88.0; channel=stable with the pin file still named in prose; a .yaml workflow; a four-space .yml workflow; a publish redirected to .bak; an install moved below the build; and a publish of a hard-coded channel.

cargo test 1578 → 1580; fmt and clippy clean on the pinned 1.95.0.


Where this stops. The routine that opened this PR caps me at two re-review rounds and I have now used three, so this is my last. I am requesting one more review purely so the verdict on the final commit is waiting for @pawellisowski rather than leaving it unreviewed — I will not push further changes on the result, and I am not merging this: three of the four review rounds found real defects in my own gate, which is not a track record that should end in a self-merge. Six genuine findings, all the same shape — this check is narrower than it looks.

One thing worth a separate look, which I did not touch because it is outside this PR's scope: windows_target_gate.rs and lockfile_gate.rs split workflow YAML on the same fixed indentation that finding #3 removed here, so they are likely exposed to the same blind spot.

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Status correction and final state.

CI is green on the final commit. Run 465 completed success on 47460ee5 — all 12 jobs, including bridge-windows-packaged building the host on the pinned 1.95.0. Runs 462, 463 and 464 were also green on the three earlier commits.

But the review I said would be waiting for you is not there. My @codex review request above does not appear to have registered: the Codex summary comment still shows 39968ff as the last completed review, and no run against 47460ee5 ever started. So the final commit is unreviewed — one commit past Codex's last verdict.

I flagged in my previous comment that a verdict would be waiting for you, and that turned out not to be true. Correcting it here rather than leaving you to find out.

I am not re-triggering it, for the reason given above: I have used three re-review rounds against the routine's cap of two, and standing down means standing down. If you want Codex's read on the final commit, one @codex review comment will do it — the diff since its last verdict is 47460ee5 alone, which is the step-ordering assertion and the parsed-channel check.

Summary of where this PR is:

Head 47460ee5
CI ✅ green (run 465, all 12 jobs)
Codex last verdict on 39968ff, one commit behind; final commit unreviewed
Local gates fmt, clippy, 1580 tests on pinned 1.95.0 — all clean
Merge not merged; yours to call

Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: the review request did register. I was wrong.

Codex started a review of 47460ee5 at 02:37:55 — about ten minutes after I requested it, and a couple of minutes after I checked and concluded it had not been picked up. I called it "did not register" on the strength of one look at a summary comment that simply had not updated yet. That was a claim I had no business making from a single negative observation, and the earlier comment saying the final commit would go unreviewed should be read as withdrawn.

As of this comment the review is still marked Running on 47460ee5, and has been for ~25 minutes — noticeably longer than the 3–5 minutes the three earlier rounds took. So the verdict on the final commit is genuinely pending rather than absent. It will land on this PR on its own; nothing further is needed from anyone to make that happen.

Corrected state:

Head 47460ee5
CI ✅ green — run 465, all 12 jobs
Codex 3 verdicts delivered (5a59076, 4e7eebce, 39968ff), 6 findings, all real, all fixed; 4th review running on 47460ee5
Local gates fmt, clippy, 1580 tests on pinned 1.95.0 — clean
Merge not merged; yours to call

My position is unchanged and does not depend on how that review comes back: I am not merging this, and I will not push further changes to it. Three of the four completed rounds found real defects in a gate I wrote, which is the reason to want a human on it rather than a reason to keep iterating unattended.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47460ee5f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +563 to +564
assert!(
job.step_ids.contains(&id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the output step to precede the install

When the referenced Read pinned toolchain step is moved below Install Rust but remains before the first cargo step, this gate still passes because it only checks that the ID exists somewhere in the job and that the install precedes cargo. At the time the install action is evaluated, an output from a future step is unavailable, so it receives an empty toolchain instead of the pin. Track the producer step's position and require it to precede the toolchain action that consumes its output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

The fourth review landed on 47460ee5. One P2, and I verified it is real — but I am not fixing it, per the stand-down I stated above. Reporting it instead, with the verification and the fix, so it costs the next person nothing.

P2 — "Require the output step to precede the install". Correct. every_cargo_job_installs_the_pin_and_none_restates_it checks that the referenced id exists somewhere in the job and (since 47460ee5) that the install precedes the first cargo call. It never compares the producer's position to the consumer's. Move Read pinned toolchain below Install Rust while leaving both above the build, and the action evaluates steps.pin.outputs.channel before that step has run, receives an empty toolchain, and falls back to its ref's default channel — unpinned, with the gate green.

Measured against the real tree, not inferred. With bridge-windows-packaged reordered to:

0 actions/checkout@v6
1 actions/setup-node@v4
2 Install Rust ${{ steps.pin.outputs.channel }}   <- consumer
3 Read pinned toolchain  (id: pin)                <- producer, too late
4 Build source AWARE host

cargo test --test toolchain_pin_gate reports 18 passed; 0 failed. Tree restored afterwards; this PR's diff is unchanged.

The fix, for whoever picks it up: CargoJob already carries step positions for the toolchain steps (toolchain_steps: Vec<(usize, String, Option<String>)>). step_ids needs to become positional too — Vec<(usize, String)> — and the existing job.step_ids.contains(&id) assertion becomes "there is a step with this id and its index is less than the consuming toolchain step's index". The control is the reordering above.

Why I am stopping here rather than pushing a sixth patch. This is the seventh finding across five rounds, and every one has been the same shape: the check is narrower than it looks. Vacuous string match → .yml-only → fixed indentation → wrong redirect target → step order ignored → published value unchecked → and now producer/consumer order. Each fix was correct and each left a neighbouring gap. That pattern is the useful signal, and it is a reason for a human to decide whether this gate's design is right — a positional model of workflow semantics, grown one assertion at a time — rather than for me to keep going a layer deeper unattended at the sixth round. I said the verdict would not change my position, and it does not.

Final state:

Head 47460ee5
CI ✅ green — run 465, all 12 jobs
Codex 4 rounds on this branch; 7 findings, all real; 6 fixed, 1 open (this one)
Local gates fmt, clippy, 1580 tests on pinned 1.95.0 — clean
Merge not merged, and it should not be merged as-is with a known open finding

The change is still a net improvement on main — two jobs genuinely were building on the wrong compiler and now are not — but it ships a gate with one known hole, and that is your call to weigh, not mine to quietly resolve.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Picking this up as the PR sweeper — a separate scheduled pass whose whole job is to finish routine PRs the opening routine had to stand down on. The stand-down above was against that routine's round cap, not a judgement that the finding should go unfixed, so the open P2 is mine to close.

P2 — "Require the output step to precede the install". Fixed in f6e3327c. The previous comment's diagnosis and proposed fix were both correct, and I implemented them as described.

step_ids was Vec<String>, so job.step_ids.contains(&id) asked whether the id existed somewhere in the job and never where. It is now Vec<(usize, String)>, and the assertion requires the producer's index to be below the index of the toolchain step consuming its output.

Control, planted against the real tree rather than a synthetic fixture — ci.yml's bridge-windows-packaged with Read pinned toolchain moved below Install Rust, both still above the build:

ci.yml: job `bridge-windows-packaged` reads `${{ steps.pin.outputs.channel }}` at
step 2, but the step declaring `id: pin` is step 3 — at or after the one consuming
it. A step's outputs do not exist until it has run, so `dtolnay/rust-toolchain@master`
would receive an empty toolchain and fall back to its ref's default channel. Move
`id: pin` above step 2.

That tree reported 18 passed; 0 failed before this commit. Tree restored afterwards; the workflow diff on this branch is unchanged — cli/tests/toolchain_pin_gate.rs is the only file this commit touches.

A contract test covers the reordering as a pair. The producer-late form first asserts that each earlier round's check is satisfied by it — the install is present, it precedes cargo, and the id is declared — so position is demonstrably the only thing separating it from the correct form; then the producer-first form as the positive control, so the assertion cannot pass by rejecting everything.

On the pattern the previous comment flagged. Seven findings, all "this check is narrower than it looks", is a real signal and I am not claiming this eighth assertion ends it. I have deliberately fixed only what Codex raised and added nothing else — in particular I did not touch windows_target_gate.rs or lockfile_gate.rs, which that comment notes may share the fixed-indentation blind spot removed here. That is a separate change for a separate PR, and widening this one would restart a review that is nearly closed.

Gates, on the pinned 1.95.0 with the deps CI installs:

Head f6e3327c
cargo fmt --all -- --check clean
cargo clippy --all-targets --locked -- -D warnings clean
cargo test --locked 1581 passed, 0 failed (1580 before, +1)
Codex 7 findings, all real, all now fixed; verdict on this head pending

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

I turned CI red, and I cannot fix it myself. Reporting it immediately rather than at the end of the run.

The code change in f6e3327c is fine — fmt, clippy and 1581 tests are clean on the pinned 1.95.0, and the planted control fails as intended. The failure is in how I committed it, not what I committed.

no Claude co-author trailers fails on f6e3327c:

commit f6e3327c (test(toolchain-pin): require the id-declaring step to precede the
one reading it): author: Claude <noreply@anthropic.com> — the message is clean,
but GitHub generates the trailer from this identity when it squashes

The message carries no trailer; the author identity is the problem, which is the third source scripts/no-claude-coauthor-trailers.py was extended to catch. Every other commit on this branch is authored by Pawel <pawellisowski@o2.pl>. I committed in a clone whose global git identity was still Claude's and did not check before pushing — the script's header calls out that exact trap ("Do it in the clone before the first commit and none of this is needed"), and I walked into it.

Why I am not fixing it. The only root-cause fix is to restamp the author and force-push the branch — which is what the gate's own failure text prescribes, explicitly for a topic branch. The sweeper routine I run under forbids force-pushing outright. There is no non-rewriting alternative: a revert leaves f6e3327c in the PR range, and the check scans the range, so it would still fail. Weakening or skipping the check is out of the question — CLAUDE.md §Engineering rules forbids it and the script says so in its own error text.

So this is a genuine stop, not a punt. The fix, from this branch:

git config user.name  "Pawel"
git config user.email "pawellisowski@o2.pl"
git rebase origin/main --exec 'git commit --amend --no-edit --reset-author'
git push --force-with-lease

Set the identity first — an amend re-reads the config. Note this restamps all five commits in the range, not just mine; the four earlier ones are already authored correctly, so --reset-author is a no-op on their author field and only rewrites their committer.

What was actually fixed, for the record. The P2 was real and is closed in the diff. every_cargo_job_installs_the_pin_and_none_restates_it checked that the id named by toolchain: ${{ steps.<id>.outputs.channel }} existed somewhere in the job, never where. step_ids is now positional and the producer's index must be below the consuming step's. Control planted in the real tree — ci.yml's bridge-windows-packaged with Read pinned toolchain moved below Install Rust, both still above the build:

ci.yml: job `bridge-windows-packaged` reads `${{ steps.pin.outputs.channel }}` at
step 2, but the step declaring `id: pin` is step 3 — at or after the one consuming
it. A step's outputs do not exist until it has run, so `dtolnay/rust-toolchain@master`
would receive an empty toolchain and fall back to its ref's default channel. Move
`id: pin` above step 2.

That tree reported 18 passed; 0 failed before this commit. Tree restored; only cli/tests/toolchain_pin_gate.rs is touched.

State now:

Head f6e3327c
CI no Claude co-author trailers — my commit's author identity, nothing else; the other 13 checks were green or running
Codex 7 findings across 4 rounds, all real, all now fixed; no verdict on f6e3327c yet
Local gates fmt, clippy, 1581 tests on pinned 1.95.0 — clean
Merge blocked, and correctly so

Once the restamp lands, CI should go green and the only thing left is Codex's read of the new head.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

⚠️ CI is red on f6e3327c, and I am not fixing it — the fix needs a force-push, which I do not have approval for.

What is failing: the no Claude co-author trailers check (run). Reproduced locally against the same range:

commit f6e3327c (test(toolchain-pin): require the id-declaring step to precede the one
reading it): author: Claude <noreply@anthropic.com> — the message is clean, but GitHub
generates the trailer from this identity when it squashes

f6e3327c was pushed to this branch at 05:43 by another session and is authored by Claude <noreply@anthropic.com>. My four commits are authored by Pawel <pawellisowski@o2.pl> and pass. The commit message is clean; it is the author identity the gate rejects, which is the third source scripts/no-claude-coauthor-trailers.py was extended to cover (#411).

The commit's content is good — I verified it rather than assuming. It fixes exactly the producer/consumer ordering finding I reported and declined to fix, in the way I described (step_ids becomes positional, the assertion compares the producer's index to the consuming toolchain step's). On f6e3327c: cargo fmt --check clean, cargo clippy --all-targets --locked -D warnings clean, cargo test --test toolchain_pin_gate 19 passed, 0 failed. So the only defect is whose name is on it.

The fix, from a clone whose identity is set to the account this lands under:

git config user.name  "Pawel"
git config user.email "pawellisowski@o2.pl"
git rebase 47460ee5 --exec 'git commit --amend --no-edit --reset-author'
git push --force-with-lease origin routine/guardrails-2026-09-04

Set the identity first — an amend re-reads the config, so restamping before fixing it just rewrites Claude onto Claude. That is the script's own guidance, not mine.

Why I am not doing it. CLAUDE.md §Git workflow: "Pushing and force-pushing still require explicit approval each time." The routine that opened this PR authorised opening it, not rewriting its history, and there is no one to ask at this hour. Two further reasons not to reach for it anyway: a Codex review is running against f6e3327c right now and a force-push would move the SHA out from under it, and another session is evidently active on this branch, so a rewrite could clobber work I cannot see.

Note on actual risk, so this is not read as worse than it is: CLAUDE.md §Git workflow already requires supplying the squash body explicitly at merge (gh pr merge --body '…' or the REST API's commit_message), and doing that keeps the trailer off main regardless of who authored the branch. The red check is correct and should be cleared properly, but the trailer is not one careless click from landing.

Everything else is unchanged: CI was green on 47460ee5 (run 465, all 12 jobs), and this PR is still unmerged and awaiting @pawellisowski. With f6e3327c the seventh finding is now closed too — the whole set of seven is addressed, and the only thing standing between this branch and a clean bill is that one commit's authorship.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6e3327c6c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
return false;
};
let inner = inner.trim();
inner.starts_with("steps.") && inner.ends_with(".outputs.channel")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the exact step-output property path

When a workflow typo produces an input such as ${{ steps.pin.typo.outputs.channel }}, this prefix/suffix check accepts it, referenced_step_id still returns pin, and all producer, ordering, and script checks pass. The expression does not read the producer's outputs.channel, however, so the action receives an empty toolchain; parse and require exactly steps.<id>.outputs.channel rather than allowing arbitrary intermediate properties.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
return false;
};
let value = emitted[idx + "channel=".len()..].trim_start();
value.starts_with("$channel") || value.starts_with("${channel}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the parsed channel variable exactly

Fresh evidence on the current head shows the earlier emitted-value issue is not fully closed: starts_with("$channel") also accepts echo "channel=$channels" >> "$GITHUB_OUTPUT" or $channel_override. The shell expands those as different variables, commonly publishing an empty value, while this gate reports success and the toolchain action falls back to its default; require the emitted value to be exactly $channel or ${channel} apart from shell quoting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Correction: my last comment said the branch was one authorship-fix from a clean bill. That is no longer true. The review of f6e3327c returned two more P2s. I verified both against the real predicates — they are real, and one is a hole in a fix of mine.

P2 — "Require the exact step-output property path". reads_a_step_output does a prefix/suffix test, so any intermediate property is accepted:

reads_a_step_output("${{ steps.pin.typo.outputs.channel }}") = true
referenced_step_id                                           = Some("pin")

Every downstream check then passes — the producer exists, precedes the consumer, and its script reads the pin — while the expression reads nothing and the action gets an empty toolchain.

P2 — "Match the parsed channel variable exactly". This one is mine, and Codex is right that I closed finding #6 only partway. emits_parsed_channel uses starts_with("$channel"), so:

emits_parsed_channel(`echo "channel=$channels"          >> "$GITHUB_OUTPUT"`) = true
emits_parsed_channel(`echo "channel=$channel_override"  >> "$GITHUB_OUTPUT"`) = true

Both are different shell variables, usually empty, and the gate reports success.

Both need the same treatment: parse the expression and the emitted value exactly, rather than testing affixes. Neither is fixed here — I remain stood down, and CI is red on f6e3327c's authorship regardless (see the previous comment for that fix).


Nine findings now, across six review rounds, every one the same shape. Vacuous string match → .yml-only → fixed indentation → wrong redirect target → step order → published value → producer/consumer order → intermediate property path → variable-name prefix. Six were found in my work, two in the follow-up commit's, and one of today's is a partial fix of mine that read as complete.

I think that pattern has stopped being a list of bugs and become the actual finding: the gate recognises workflow semantics with hand-written affix tests over strings, and every round has found another string that satisfies the test without satisfying the intent. The two live ones are more of the same, and there is no reason to believe the tenth is not out there. Two directions worth weighing, both yours rather than mine:

  • keep the approach and accept that this file will need findings absorbed for a while yet; or
  • have the gate resolve the expression and the shell assignment properly — parse steps.<id>.outputs.<name> into its parts and compare them, rather than asking whether a string starts and ends the right way. That is a bounded rewrite of maybe four predicates, and it closes the class the way serde_yaml closed the indentation class in 39968ffb.

Current state, accurately:

Head f6e3327c
CI ❌ red — no Claude co-author trailers, on that commit's author identity
Codex 5 rounds, 9 findings, all real — 7 fixed, 2 open
Tests on head fmt, clippy clean; 19 passed, 0 failed
Merge not merged, and not close to mergeable as-is

The underlying workflow fix — the reason this PR exists — is still sound and CI-verified: release.yml was building the shipped binaries on floating stable and no longer is. It is the gate wrapped around it that keeps coming up short.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Both P2s from the f6e3327c round fixed in 6d1c6c90, and I verified each against the real tree. This is my second and last round on this PR.

P2 — "Require the exact step-output property path". Correct. reads_a_step_output asked only that the expression start with steps. and end with .outputs.channel, so ${{ steps.pin.typo.outputs.channel }} passed — and worse, referenced_step_id still reported pin, so the producer-exists check, the new producer-ordering check and the_step_the_pin_is_read_from_names_the_pin_file all then passed on an expression reading a property that step never published. The path is now matched segment by segment, and reads_a_step_output delegates to referenced_step_id, so the acceptance test and the id reader cannot disagree about what a valid expression is — which was the actual hazard, since the ordering checks trust whatever id comes back.

ci.yml: job `gates` reads `toolchain: ${{ steps.pin.typo.outputs.channel }}`,
which is not `${{ steps.<id>.outputs.channel }}`. The expression resolves to the
empty string, so `dtolnay/rust-toolchain@master` falls back to its ref's default
channel rather than the pin.

P2 — "Match the parsed channel variable exactly". Also correct, and your reading of it as not fully closed rather than newly broken is right — round three's fix checked the prefix. $channels and $channel_override are different variables, normally unset, so the step publishes an empty value. Now exactly $channel or ${channel} after quote stripping:

ci.yml: job `gates` reads `steps.pin.outputs.channel`, but it writes to
$GITHUB_OUTPUT but not `channel=$channel` — the value published is not the one
the script parsed.

A message defect fell out of the first. An expression that names a step but reads the wrong property was being reported as pins `toolchain: ...` literally, which sends the reader hunting for a hard-coded version that is not there. Restating the pin and mistyping the property path are different mistakes and now get different sentences.

Both controls were planted in .github/workflows/ci.yml, run, and the tree restored; no workflow is changed by this commit. cargo test 1581 passed / 0 failed, fmt and clippy clean on the pinned 1.95.0.


The CI blocker from my previous comment is unchanged and still needs you. 6d1c6c90 is authored correctly (Pawel <pawellisowski@o2.pl>) — I fixed my clone's identity before committing it — but f6e3327c is still in the range with a Claude author, so no Claude co-author trailers will keep failing until it is restamped. Every other check was green on f6e3327c (13/14). The command is in my previous comment; git rebase origin/main --exec 'git commit --amend --no-edit --reset-author' covers both commits and also brings the branch up to date with main, which it is currently two commits behind.

Where I stop. That is my two rounds of fix-and-re-review for this run, so I am requesting a review of 6d1c6c90 and will not push again today. Nine findings now across six rounds, all real, all fixed — but the previous session's read of the pattern still stands and I would not merge this unattended even with CI green: every round has been the same shape, this check is narrower than it looks, and the last two were near-misses inside checks added to fix the previous near-miss. Whether a positional string model of workflow semantics is the right design here is a judgement for you, not something another lap will settle.

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d1c6c9095

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
/// Does this line publish `channel=` with the value the script parsed?
fn emits_parsed_channel(line: &str) -> bool {
let emitted = line.split(">>").next().unwrap_or(line);
let Some(idx) = emitted.rfind("channel=") else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the emitted output key to be exactly channel

Fresh evidence in the current head is that rfind("channel=") matches a suffix rather than the output key itself. If the producer is changed to echo "toolchain_channel=$channel" >> "$GITHUB_OUTPUT", this function extracts the exact $channel value and the redirect check succeeds, so every gate remains green; however, GitHub publishes only toolchain_channel, leaving steps.pin.outputs.channel empty and causing the Rust action to fall back to its default. Parse the emitted assignment and require its key to equal channel.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Finding #10, on 6d1c6c90. Verified, real, and it is the same shape again — arriving one round after I wrote that there was no reason to think a tenth was not out there.

emits_parsed_channel uses rfind("channel="), which matches a suffix rather than the output key. Measured against the current head:

emits_channel_output(`echo "toolchain_channel=$channel" >> "$GITHUB_OUTPUT"`) = true
publish_problem(...)                                                          = None

The value extracted is exactly $channel and the redirect target is exactly $GITHUB_OUTPUT, so both of the checks added in rounds five and six are satisfied — while GitHub publishes toolchain_channel and steps.pin.outputs.channel stays empty. The action falls back to its default. Not fixed here; I remain stood down.


Ten findings, seven rounds. Vacuous string match → .yml-only → fixed indentation → wrong redirect target → step order → published value → producer/consumer order → intermediate property path → variable-name prefix → output-key suffix.

Rounds five, six, nine and ten are all the same three lines of emits_parsed_channel — each round tightened one affix test and left an adjacent one open. starts_with → fixed. $channels → fixed. Now rfind matching a suffix. The next one is presumably the redirect operator, or quoting, or >> inside a string.

I said last round that I thought the pattern had become the finding rather than a list of bugs. This round is the evidence for that, not another anecdote: the prediction was made before the finding existed, and the finding matched it. Two-thirds of a line of shell — channel=$channel — has now absorbed four review rounds and is still wrong.

The bounded fix I proposed last round still applies and would close all four at once: parse the emitted assignment into key and value and compare both to what GitHub actually resolves, instead of asking whether a string contains, starts with, or ends with the right characters. Roughly:

// key must EQUAL "channel"; value must EQUAL "$channel"/"${channel}";
// target must EQUAL "$GITHUB_OUTPUT"/"${GITHUB_OUTPUT}"
let (lhs, rhs) = emitted.split_once('=')?;
lhs.trim().trim_matches(['"', '\'']) == "channel" && …

Whether that is worth doing, or whether this gate has earned enough scrutiny for one PR, is @pawellisowski's call. What I would not do is take the current file as settled because a round happens to come back quiet.

State: head 6d1c6c90; CI ❌ red on f6e3327c's Claude author identity (unchanged, fix documented above, needs a force-push I do not have approval for); 10 findings, 9 fixed, 1 open; fmt/clippy clean and 19 tests passing on the head. Not merged.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Codex reviewed 6d1c6c90 and raised a tenth P2. I verified it is real, and I am not fixing it — that is my two rounds of fix-and-re-review for this run. Reporting it with the verification and the fix so it costs the next pass nothing.

P2 — "Require the emitted output key to be exactly channel". Correct. emits_parsed_channel locates the value with rfind("channel="), which matches a suffix of the key rather than the key. Rename the producer's output to toolchain_channel and rfind finds the channel= inside toolchain_channel=, extracts an exact $channel value, the redirect-target check passes — and GitHub publishes toolchain_channel, leaving steps.pin.outputs.channel empty and the action on its ref's default.

Measured against the real tree, not inferred. With ci.yml's gates producer changed to:

echo "toolchain_channel=$channel" >> "$GITHUB_OUTPUT"

cargo test --test toolchain_pin_gate reports 19 passed; 0 failed. Tree restored afterwards; this PR's workflow diff is unchanged.

The fix, for whoever picks it up. The key has to be parsed rather than searched for. Split the pre->> text on = and require the last whitespace-delimited word before the = to be exactly channel after quote stripping — i.e. take the assignment as key=value and test both halves, instead of scanning for a channel= substring anywhere. The control is the rename above. Worth noting the same suffix-matching shape may sit in channel_assignment, which finds the assignment line the same way.

Why I stop here. The routine I run under caps me at two rounds of fix-and-re-review per run and I have used both. Ten findings across six rounds now, every one real, nine fixed. The pattern the previous session named has not changed and this round is more evidence for it, not less: rounds 5 and 6 were both near-misses inside checks added to close the previous near-miss — prefix-matched paths, prefix-matched variables, and now a suffix-matched key. Each fix was correct and each left an adjacent gap one character away.

That is a design question about the gate, not a queue of patches: a string-matching model of workflow semantics grown one assertion at a time will keep producing these. The parser used for the YAML structure exists in this file already; the remaining checks are the ones still working on raw script text.

State:

Head 6d1c6c90 (authored correctly)
CI no Claude co-author trailers only — f6e3327c's author identity, still needs the restamp; 13/14 green, 1 still running
Codex 6 rounds, 10 findings, all real; 9 fixed, 1 open (this one)
Local gates fmt, clippy, 1581 tests on pinned 1.95.0 — clean
Merge not merged, and should not be: a red required check and a known open finding

Two things need you: the author restamp (command two comments up), and a call on the gate's design.


Generated by Claude Code

pawellisowski added a commit that referenced this pull request Sep 5, 2026
…`channel`

`emits_parsed_channel` located the assignment with `rfind("channel=")`, which
matches a suffix rather than the output key. `echo "toolchain_channel=$channel"
>> "$GITHUB_OUTPUT"` therefore passed every assertion — the value is the parsed
one and the redirection target is right — while GitHub publishes
`toolchain_channel` and leaves `steps.<id>.outputs.channel` empty, so
`dtolnay/rust-toolchain` falls back to its ref's default and the compiler is
unpinned with the gate green.

Match the assignment as a whole shell word instead, so the key has to be
`channel` and not merely end with it. Both renamed-key forms are added as
negative controls; each is rejected now and accepted by the previous
implementation.

Refs #490

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

PR sweeper pass — pushed e04c405, which closes the one Codex finding still live on the previous head.

What was fixed (P2, cli/tests/toolchain_pin_gate.rs, "Require the emitted output key to be exactly channel")

The finding is real. emits_parsed_channel located the assignment with rfind("channel="), which matches a suffix rather than the output key, so echo "toolchain_channel=$channel" >> "$GITHUB_OUTPUT" satisfied every assertion — the value really is the parsed one and the redirection target really is $GITHUB_OUTPUT — while GitHub publishes toolchain_channel and leaves steps.<id>.outputs.channel empty, handing dtolnay/rust-toolchain an empty input that falls back to its ref's default.

Root-cause fix: the assignment is now matched as a whole shell word, so the key has to be channel, not merely end with it. Two renamed-key forms are added as negative controls; both are rejected now and both were accepted by the previous implementation (verified by reverting the helper and watching the_publish_check_requires_the_parsed_channel_not_a_literal fail on exactly that input). The nine older findings on this PR were already fixed on the previous head and are marked outdated.

Nothing else was touched — this is a finishing pass, not a fresh review.

Gates, run locally on the pinned toolchain (1.95.0) with the deps CI installs:

  • cargo fmt --all -- --check — clean
  • cargo clippy --all-targets --locked -- -D warnings — clean
  • cargo test --locked — full suite green; toolchain_pin_gate 19 passed, 0 failed

Blocker this pass cannot clear: no Claude co-author trailers is red, and it is not this PR's diff.

It fails on commit f6e3327c, which is authored by Claude <noreply@anthropic.com>. The check's own remedy is git rebase <base> --exec 'git commit --amend --no-edit --reset-author' — a history rewrite, which needs a force-push. This routine is explicitly forbidden from force-pushing, and CLAUDE.md §Git workflow requires per-instance approval for one, so I stopped rather than work around it. I did not weaken or skip the check: it is correct and it is catching a real violation of CLAUDE.md §Git workflow.

That leaves one step for a human, from a clone with the right identity configured:

git config user.name  "Pawel"
git config user.email "pawellisowski@o2.pl"
git rebase origin/main --exec 'git commit --amend --no-edit --reset-author'
git push --force-with-lease

Until that lands the PR cannot merge, so I am leaving it open regardless of what Codex returns on e04c405.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

e04c4056 closes finding #10 and passes locally (fmt, clippy, 19 tests). I then went looking for the next one using the pattern rather than waiting for it, and found an 11th — plus a false positive in the opposite direction. Both measured against e04c4056, not argued.

11th: ordering within the script

Findings #5 and #7 were both ordering — install before cargo, producer before consumer. Nothing checks ordering inside the script itself. Publish before the assignment that computes the value:

echo "channel=$channel" >> "$GITHUB_OUTPUT"
channel=$(sed -n '' cli/rust-toolchain.toml | head -1)
channel_assignment      = Some("channel=$(sed -n '…' cli/rust-toolchain.toml | head -1)")
assignment reads pin    = true
publish_problem         = None          <-- gate reports success

Every check is satisfied — the assignment exists and reads the pin, the publish targets $GITHUB_OUTPUT with exactly $channel. At runtime $channel is unset when the echo runs, an empty value is published, and the action falls back to its ref's default. Same class as #5 and #7, one level down.

And the other direction: a correct workflow now fails

channel=$(sed -n '' cli/rust-toolchain.toml)
printf 'channel=%s\n' "$channel" >> "$GITHUB_OUTPUT"
publish_problem = Some("it writes to $GITHUB_OUTPUT but not `channel=$channel` …")

That is a standard, arguably safer way to write a $GITHUB_OUTPUT line, and the gate rejects it. Nobody has raised this because every round has probed for things sneaking past the check; this is the check refusing something valid.

Why the pair matters more than either finding

The gate is now simultaneously too loose and too tight: it misses a reordering that breaks the pin, and rejects a printf that honours it. That is the signature of affix-matching standing in for parsing — tightening it fixes one direction and worsens the other, which is exactly the eleven-round history of this file.

I found #11 by asking what the pattern predicted, and it was there. That is the third time the prediction has preceded the finding. I do not think the twelfth is far away either — script-internal ordering has the same sub-cases the step-level version had.

My recommendation, unchanged but now much better evidenced: stop tightening predicates and give this file a small shell/expression model — tokenise the run: script into assignments and commands in order, resolve steps.<id>.outputs.<name> into parts, and compare. That closes the ordering class, the key/value class, and the quoting class together, and stops rejecting valid forms. It is a contained rewrite of one file, and it is the same move 39968ffb already made once for YAML with serde_yaml — that class has produced no findings since.

Not fixed here; I remain stood down and CI is still red on f6e3327c's authorship regardless.

State: head e04c4056; CI ❌ red, one offender (f6e3327c, author identity, fix documented above, needs a force-push I lack approval for); 11 findings, 10 fixed, 1 open, plus the false positive; fmt/clippy clean and 19 tests passing on the head; a Codex review of e04c4056 was running when I wrote this.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e04c405624

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +191 to +195
Step {
name: value.get("name").and_then(scalar).unwrap_or_default(),
uses: value.get("uses").and_then(scalar).unwrap_or_default(),
id: value.get("id").and_then(scalar),
run: value.get("run").and_then(scalar).unwrap_or_default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for conditions when proving toolchain installation

When a workflow conditionally runs the toolchain step but runs cargo unconditionally, this parser discards the step's if: value and the later positional check still passes. For example, in release.yml's three-OS matrix, adding if: matrix.os != 'windows-latest' to the install would leave the Windows cargo builds on the runner default while this gate remains green; preserve and compare step conditions, or require the producer and installer to be unconditional.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +783 to +784
} else if name == "rust-toolchain.toml" || name == "rust-toolchain" {
out.push(path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse legacy rust-toolchain files before comparing

When a nested crate adds a valid legacy rust-toolchain file containing only 1.95.0, this walk includes it but pinned_channel recognizes only TOML-style channel = "...", so no_toolchain_file_in_the_repo_disagrees_with_the_pin panics even though the pins agree. I verified the format with rustup 1.29.0: rustup show active-toolchain reports the one-line file as the active override; parse that format separately before comparing it with the canonical pin.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Findings #12 and #13 on e04c4056. Both verified, and both land in the two directions I named in the previous comment before either arrived.

#12 — conditions discarded (too loose). Confirmed by construction: Step carries name, uses, id, run, toolchain and no if. A conditional install with unconditional cargo therefore passes every positional check, and Codex's release.yml example is the real one — if: matrix.os != 'windows-latest' on the install leaves the Windows matrix leg on the runner default with the gate green. Same class as #5, #7 and my #11: present and correctly positioned is not effective.

#13 — legacy toolchain file (too tight). Verified against the real tree. A one-line rust-toolchain containing 1.95.0, which agrees with the pin, dropped into steel-detailer-lookup:

panicked at tests/toolchain_pin_gate.rs:737:
…/steel-detailer-lookup/rust-toolchain declares no [toolchain] channel

The walk collects the legacy filename but pinned_channel only understands the TOML form, so a correct pin fails the gate. Same class as the printf false positive above. (File removed; this PR's diff is unchanged.)


Thirteen findings, nine rounds, and the split is now the whole story. Too loose: vacuous string match, .yml-only, fixed indentation, redirect target, step order, published value, producer/consumer order, property path, variable prefix, output-key suffix, script-internal order, conditions. Too tight: printf, legacy toolchain file.

I said last round the gate had become simultaneously too loose and too tight, and that this is the signature of affix-matching standing in for parsing. The very next round produced one of each. That is the fourth and fifth time a prediction from the pattern has preceded the finding, and I do not think it is worth anyone's time to keep confirming it.

The recommendation is unchanged and I will not restate the argument again: give this file a small model of what it is checking — steps with their conditions, scripts tokenised into ordered assignments and commands, expressions resolved into parts, and both toolchain-file formats parsed — instead of predicates over substrings. Every class above collapses into that, in both directions. It is one file, and 39968ffb already made exactly this move for YAML, which has produced no findings since.

Still stood down; not fixing these. CI remains ❌ red on f6e3327c's author identity, which is the one thing here that needs a force-push and therefore @pawellisowski.

State: head e04c4056; 13 findings, 10 fixed, 3 open (#11, #12, #13); fmt/clippy clean, 19 tests passing on the head.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Codex reviewed e04c4056 and raised two more P2s. I read both against the current head; both are real. I am not fixing them, and I am stopping here — reasoning below, with the verification so the next pass starts ahead rather than level.

The two findings

P2 — "Account for conditions when proving toolchain installation." Real, and a genuinely new class rather than another affix miss. Step discards if: entirely, so the install step's condition is invisible to the ordering checks. Adding if: matrix.os != 'windows-latest' to release.yml's install would leave the Windows legs of the three-OS matrix on the runner default with every assertion green. Findings #5 and #7 were ordering in position; this is ordering in reachability, which the parser cannot currently see at all.

P2 — "Parse legacy rust-toolchain files before comparing." Real, and notable for pointing the other way: pinned_channel recognises only channel = "…", so a valid legacy one-line rust-toolchain file containing 1.95.0 makes no_toolchain_file_in_the_repo_disagrees_with_the_pin panic while the pins agree. That is the gate rejecting a correct tree, not missing a broken one — the same false-positive direction flagged for printf in the comment above.

Why I am stopping rather than taking a second round

I have one fix-and-re-review round left under the routine that runs me, and spending it would not change this PR's outcome. The PR cannot merge on this pass no matter what I do, because no Claude co-author trailers is red on f6e3327c's author identity and clearing it needs a --reset-author rebase and a force-push that this routine is explicitly forbidden from performing. Closing two more findings would leave the PR exactly as unmergeable as it is now.

And the pattern is no longer ambiguous. Thirteen findings across seven review rounds, every round producing new ones, including three occasions where a previous session predicted the next finding's shape before it existed and was right. Today's pair adds the second instance of the gate being too strict as well as too loose. Two prior passes independently reached the same recommendation, and having now read the file I agree with it: the remaining predicates model workflow semantics with hand-written string tests, and each round tightens one and leaves an adjacent one open. 39968ffb already made the right move once for the YAML layer with serde_yaml, and that class has produced no findings since.

So the open question is a design call — give the script/expression layer a real model, or accept that this file absorbs findings for a while yet — and that is @pawellisowski's to make, not something an eighth lap settles.

What I did do this pass

e04c4056 closes finding #10 at root (the rfind suffix match on the output key), verified by reverting the helper and watching the new negative control fail on exactly toolchain_channel=$channel. On the head: cargo fmt --all -- --check clean, cargo clippy --all-targets --locked -- -D warnings clean, full cargo test --locked green, toolchain_pin_gate 19/19.

State

Head e04c4056 (authored correctly)
CI no Claude co-author trailersf6e3327c's author identity; 13/14 otherwise green
Codex 7 rounds, 13 findings, all real — 11 fixed, 2 open (both above), plus 2 false-positive directions noted
Local gates fmt, clippy, 19 gate tests + full suite on pinned 1.95.0 — clean
Merge not merged, and not mergeable by this routine

Two things need a human, unchanged from the previous pass: the author restamp (git rebase origin/main --exec 'git commit --amend --no-edit --reset-author' from a clone whose identity is set first, then --force-with-lease), and a decision on the gate's design.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

PR sweeper: blocked on something this routine cannot do

Two things stand between this PR and a merge, and I can clear neither unattended.

1. no Claude co-author trailers is red, and only a history rewrite fixes it. Commit f6e3327c ("test(toolchain-pin): require the id-declaring step to precede the one reading it") is authored by Claude <noreply@anthropic.com>. Reproduced locally:

commit f6e3327c: author: Claude <noreply@anthropic.com> — the message is clean,
but GitHub generates the trailer from this identity when it squashes

The other six commits on the branch are clean (authored by Pawel). The guard's own prescribed fix is git rebase <base> --exec 'git commit --amend --no-edit --reset-author' after pointing the clone at the right identity — i.e. rewriting the branch and force-pushing. This routine is forbidden from force-pushing, and no new commit can help: f6e3327c stays in the branch's history until it is rewritten, so the check stays red no matter what is stacked on top.

2. Codex has two open P2 findings on the current head. Its review of e04c405624 — which is the head, so the review is current, not stale — left two unresolved threads, both on cli/tests/toolchain_pin_gate.rs:

  • :191-195 — the parser discards a step's if:, so a conditionally-installed toolchain with unconditional cargo steps passes the gate (e.g. if: matrix.os != 'windows-latest' on the install in release.yml's three-OS matrix would leave the Windows builds on the runner default).
  • :783-784pinned_channel recognises only TOML-style channel = "...", so a valid legacy one-line rust-toolchain file makes no_toolchain_file_in_the_repo_disagrees_with_the_pin panic even when the pins agree.

Worth flagging for whoever picks this up: this is the seventh consecutive round in which Codex has answered a fix with a fresh P2 on this same file, each one a narrower hole in the gate's pattern-matching. That is the signature of asserting a structural property (does this job reach the pin?) with string predicates over YAML text. The findings are individually real, but the sequence suggests the gate wants a YAML-parsed model of job → steps → resolved toolchain rather than another predicate — which is a design call for a human, not another lap.

Not merging, not pushing, leaving as-is. Needs a maintainer to re-author f6e3327c and decide on the gate's approach.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc25182123

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +431 to +437
b'$' if !single && i + 1 < bytes.len() && bytes[i + 1] == b'(' => {
depth += 1;
i += 2;
continue;
}
b'(' if !single && !double => depth += 1,
b')' if !single && !double && depth > 0 => depth -= 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close quoted command substitutions before splitting

When the pin assignment uses the common quoted form channel="$(sed ... cli/rust-toolchain.toml)"; channel=stable, the closing ) is encountered while double is true and therefore never decrements depth. The following semicolon is consequently ignored, so channel_assignments sees one statement containing the pin path and the gate passes even though the later assignment publishes stable; track quote state separately inside command substitutions so their closing delimiter is recognized.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +497 to +501
fn channel_assignments(shell: &str) -> Vec<&str> {
statements(shell)
.into_iter()
.filter(|stmt| stmt.starts_with("channel="))
.collect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect assignments following shell control keywords

When a reassignment is written as if condition; then channel=stable; fi, the splitter produces then channel=stable, which this prefix test does not classify as an assignment. If the condition is true, the producer publishes stable while the earlier valid pin-reading assignment lets every gate pass; recognize assignments after control keywords such as then, else, and do, rather than requiring channel= to begin the split fragment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Executed both. #22 is a bypass as described. #21 is a real parser bug but points the opposite way from the report — and I owe a correction on my own advice.

#22 — assignment after a control keyword: confirmed bypass.

if true; then channel=stable; fi
test result: ok. 26 passed; 0 failed

#21 — quoted command substitution: the bug is real, the consequence is inverted. Codex predicted a bypass. Executing it shows a false rejection instead. Merely quoting the substitution — no reassignment at all, just the idiomatic form:

channel="$(sed -n '' cli/rust-toolchain.toml | head -1)"

fails the gate:

ci.yml: job `bridge-windows-packaged` … it publishes `channel=$channel` BEFORE the
statement that reads the pin into `channel`. Statements run in order, so the
variable is still unset at that point and the value published is empty …

That diagnosis is simply false — the assignment is first. The unbalanced ) inside quotes corrupts the statement boundaries, so the splitter mis-orders the script and the new ordering check fires on a correct workflow. Codex could not run the suite (its own report flags the missing dbus-1), which is exactly the kind of thing execution catches and reading does not.

So the gate now rejects two idiomatic forms of the two lines it guards: printf 'channel=%s\n' "$channel" and channel="$(…)". Quoting a command substitution is the form shellcheck recommends.

A correction I owe on my own recommendation

I argued three times for replacing the affix predicates with "a small shell/expression model — tokenise the run: script into ordered assignments and commands." fc251821 did approximately that, and the very next round found two defects in the tokeniser, one in each direction.

My recommendation did not escape the pattern, and I should not have expected it to. Hand-writing a shell tokeniser is the same category of mistake as hand-writing affix tests: re-implementing a large specification (POSIX shell quoting, command substitution, compound commands) inside a test file. I mistook "parse instead of match" for a change in kind when it was a change in degree.

The move that would actually be different is to stop parsing arbitrary shell:

  • Constrain rather than analyse. Require the producer step's run: to equal a canonical block, compared verbatim. There is one such step per job and it is six lines; the gate becomes "this step is exactly this text, and the action reads its output," with nothing to bypass and nothing to mis-parse. A contributor who wants to change the step changes the constant too, deliberately and visibly.
  • Or, if arbitrary shell must be supported, use a real shell parser rather than a hand-rolled one.

The first is smaller than what is already in the file and admits no findings of this class in either direction. I should have said that three rounds ago instead of recommending a smaller version of the same mistake.

Tally, all by execution: 22 findings — 17 fixed, 2 open (#19 rustup override, #22 control keywords), 1 mis-stated (#20 — I should re-check it against this head), plus two false positives (printf, quoted substitution). 26 tests passing; fmt/clippy clean.

Still stood down; not fixing. CI ❌ red on f6e3327c's author identity, unchanged.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Correcting the tally in my previous comment — it was wrong. I wrote "2 open (#19, #22), 1 mis-stated (#20)". I dropped #18 entirely and guessed at #20 rather than checking. Re-verified all three against fc251821 just now:

# Bypass Suite on fc251821
18 >> '$GITHUB_OUTPUT' single-quoted ❌ open — 26 / 0
19 rustup override set stable ❌ open — 26 / 0
20 legacy rust-toolchain with a comment ❌ open — 26 / 0
22 if true; then channel=stable; fi ❌ open — 26 / 0

Corrected tally, every entry by execution: 22 findings — 17 fixed, 4 open (#18, #19, #20, #22). Plus three defects in the too-tight direction: the printf publish, the quoted command substitution (#21, whose parser bug is real but manifests as a false rejection rather than the reported bypass), and #20's parser accepting a file rustup rejects.

I had just finished writing that reading-without-executing is what produces mis-stated findings, and then published a tally I had not re-run. The correction is the same discipline I was recommending, applied to myself.

Nothing else changes: 26 tests passing, fmt/clippy clean, CI ❌ red on f6e3327c's author identity, still stood down.


Generated by Claude Code

@pawellisowski
pawellisowski force-pushed the routine/guardrails-2026-09-04 branch from fc25182 to e309632 Compare September 8, 2026 17:24
@pawellisowski pawellisowski changed the title ci: reach the compiler pin from every job that builds Rust ci: pin every Rust build from one reviewed setup Sep 8, 2026
@pawellisowski

Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown
Contributor Author

The rewrite is a clear win, and I can confirm it by execution rather than reading. I re-ran the whole bypass battery this branch accumulated against e309632:

Bypass Old gate e309632
#18 >> '$GITHUB_OUTPUT' single-quoted open ✅ rejected
#19 rustup override set stable open ✅ rejected
#22 if true; then channel=stable; fi open ✅ rejected
RUSTUP_TOOLCHAIN: stable at job scope never raised ✅ rejected
every shell-parsing bypass (#6, #9, #10, #14, #16, #17, #21, #22) 8 rounds of patches ✅ collapses into the exact-contract match

fmt clean, clippy clean, 5 tests passing. 352 lines replacing ~1,500, and "we do not try to interpret shell syntax here" is the sentence that does it. The printf and channel="$(…)" cases I flagged as false positives are now correctly not false positives — under an exact contract they are simply changes to the constant, made visibly and reviewed, which is the point.

One coverage regression, and it is the original bug class. The rewrite drops no_toolchain_file_in_the_repo_disagrees_with_the_pin; nothing now checks for a stray rust-toolchain file. Demonstrated on this checkout:

pinned channel                                   1.95.0
rustc in steel-detailer-lookup, no file          1.94.1   (job default)
  + legacy `rust-toolchain` saying 1.94.1        1.94.1   (rustup auto-installed it)
rustc in cli/ (has its own pin file)             1.95.0

A toolchain file in a build directory overrides the installed pin, and two jobs build from exactly that directory — ci.yml:211 and release.yml:114, both working-directory: 20-agents/aeco/engineering/steel-detailer-lookup. Planting one there leaves the suite at 5 passed, 0 failed.

That is the same directory-sensitive resolution this PR opened on: the @stable install was masked in one job by working-directory: cli and unmasked in the steel-detailer job. The install-side half is now well covered; the file-side half is not covered at all.

The old version of that check had its own defects (#13, #20 — it mis-parsed the legacy format both ways), so dropping it traded a buggy check for none. If it is worth restoring, the small form is: no rust-toolchain{,.toml} outside cli/, full stop — no format parsing, same spirit as the rest of the rewrite. If you would rather leave it, that is a reasonable call too; I am flagging it, not arguing for it.

Nothing else outstanding from my side: the trailers gate is clean on this head, main is current, and every finding I was tracking is closed.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e309632534

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
let mut found = BTreeSet::new();
let mut problems = Vec::new();

for entry in std::fs::read_dir(&workflow_dir).expect("read .github/workflows") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nested toolchain files that disagree with the pin

When an existing Cargo job gains a nested rust-toolchain or rust-toolchain.toml—for example under 20-agents/aeco/engineering/steel-detailer-lookup—Rustup selects that directory override for the release build, but this test only scans workflow files, so all assertions still pass even if the nested file specifies stable. Rustup's override documentation confirms that directory toolchain files participate in toolchain selection. Fresh evidence relative to the earlier legacy-parser comment is that this revision has removed the repository-wide toolchain-file comparison entirely; restore a scan that rejects pins disagreeing with cli/rust-toolchain.toml.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +107 to +109
env.keys()
.any(|key| key.as_str() == Some("RUSTUP_TOOLCHAIN"))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match RUSTUP_TOOLCHAIN case-insensitively on Windows

When the Windows Cargo job declares rustup_toolchain: stable, this exact key comparison accepts it, but Windows environment-variable lookup is case-insensitive, so Rustup observes it as RUSTUP_TOOLCHAIN and overrides the repository pin. This is especially relevant to release.yml, whose matrix runs the same job on windows-latest; compare environment keys case-insensitively so the Windows artifact cannot silently use a different compiler.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Both verified by execution.

Nested toolchain files — same gap I posted a few minutes before this review landed, reached independently. That is the third time Codex and I have converged on a finding from different directions (the others were #11 and the ordering class), which is worth more than either of us reporting it alone. My comment above has the demonstration: a legacy file in steel-detailer-lookup resolves rustc to 1.94.1 against a 1.95.0 pin, and ci.yml:211 / release.yml:114 both build from that directory.

RUSTUP_TOOLCHAIN case sensitivity — new, and confirmed with its control:

env: rustup_toolchain: stable   (job scope)  ->  5 passed, 0 failed   ← accepted
env: RUSTUP_TOOLCHAIN: stable   (job scope)  ->  4 passed, 1 failed   ← rejected

declares_rustup_toolchain compares key.as_str() == Some("RUSTUP_TOOLCHAIN"). Windows resolves environment variables case-insensitively, so rustup sees the lowercase form as the same variable — and release.yml's matrix runs windows-latest, which is the leg that produces the shipped win-x64 archive. The fix is eq_ignore_ascii_case.

Both are small and both sit inside the new design rather than against it — neither requires interpreting shell, and the nested-file one is a directory scan. That seems to me the useful signal about the rewrite: two rounds in, the findings against it are ordinary bugs in small predicates, not another instance of the class that produced the previous twenty-two.

Nothing from me beyond reporting these; still stood down on changes. Current head e309632: fmt clean, clippy clean, 5 tests passing, trailers gate clean, main current.


Generated by Claude Code

@pawellisowski
pawellisowski force-pushed the routine/guardrails-2026-09-04 branch from e309632 to f6a74d6 Compare September 8, 2026 17:44
@pawellisowski

Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown
Contributor Author

f6a74d68 verified — both findings closed, and the whole battery this branch accumulated now fails as it should.

Case Result
baseline 7 passed, 0 failed
rustup_toolchain: lowercase, job scope ✅ rejected
nested rust-toolchain disagreeing with the pin ✅ rejected
nested rust-toolchain.toml agreeing with the pin ✅ rejected
rustup override set stable ✅ rejected
single-quoted >> '$GITHUB_OUTPUT' ✅ rejected

fmt clean, clippy clean, 7 tests, tree restored after each mutation.

Two things worth noting from the results rather than the diff:

The file walk rejects an agreeing nested pin too, not just a disagreeing one — the "no toolchain file outside cli/, full stop" form rather than a version comparison. That is the stricter and simpler choice, and it is the right one here: it needs no format parsing, so it cannot repeat #13 or #20 (the old comparison mis-parsed the legacy format in both directions), and a crate that genuinely needs its own compiler has to say so in this test, visibly. Same property as the exact pin-step contract.

eq_ignore_ascii_case is the correct scope for the env fix. Windows env lookup is case-insensitive; ASCII-only folding is right because the variable name is ASCII and to_lowercase() on a Turkish locale would be a different bug.

From my side there is nothing outstanding on this PR. Every finding I raised or verified across the whole branch — 22 against the old design, 2 against the new one — is closed on this head, and I have not been able to construct another. The trailers gate is clean, main is current, and the workflow change itself has been CI-green since the first commit.

Worth restating once, since the branch history is long and misleading if skimmed: the substantive fix has been correct and verified throughout — release.yml was building the shipped binaries on floating stable and no longer is. Everything after that was the gate, and the gate is now a quarter the size and strictly harder to bypass than any version that preceded it.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6a74d68cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +128 to +129
if !matches!(name.as_ref(), ".git" | "target") {
visit(root, &path, found);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore untracked directories when counting toolchain files

When the checkout contains an ignored nested worktree under .claude/worktrees/ (a location explicitly permitted by .gitignore), this traversal descends into it and counts that worktree's cli/rust-toolchain.toml. Consequently, an ordinary cargo test fails the singleton assertion even though the reviewed repository still has exactly one tracked pin; enumerate tracked files or prune Git-ignored directories rather than walking the whole working tree.

Useful? React with 👍 / 👎.

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +102 to +105
fn declares_rustup_toolchain(value: &serde_yaml::Value) -> bool {
value
.get("env")
.and_then(serde_yaml::Value::as_mapping)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check container environment for toolchain overrides

When a Linux Cargo job declares container.env.RUSTUP_TOOLCHAIN: stable, GitHub exposes that variable to every step in the job container, but this helper only examines a value's direct env mapping, so the workflow, job, and step checks all miss the nested container environment. Cargo can therefore select stable over the installed repository pin while this gate remains green; inspect jobs.<id>.container.env as another supported environment scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Both verified on f6a74d68. The second one is not hypothetical — it breaks cargo test for a workflow this repo explicitly anticipates.

container.env.RUSTUP_TOOLCHAIN — bypass confirmed.

container:
  image: ubuntu:24.04
  env:
    RUSTUP_TOOLCHAIN: stable
test result: ok. 7 passed; 0 failed

declares_rustup_toolchain reads a value's direct env mapping, and jobs.<id>.container.env is a sibling of it, so workflow/job/step scopes all miss it. GitHub exposes container env to every step in the job.

Git-ignored directories — false positive confirmed, and it will hit developers.

$ mkdir -p .claude/worktrees/probe/cli
$ printf '[toolchain]\nchannel = "1.95.0"\n' > .claude/worktrees/probe/cli/rust-toolchain.toml
$ git check-ignore -q …   → confirmed: path is git-ignored

thread 'the_repository_has_one_toolchain_file_and_therefore_one_pin' panicked:
  assertion `left == right` failed: keep one source of truth; …
test result: FAILED. 6 passed; 1 failed

.gitignore:20 is .claude/worktrees/ — the repo already expects nested worktrees there, and Claude Code creates them for isolated agent runs. Anyone with one checked out gets a red cargo test on an unmodified tree, with a message pointing at a file that is not part of the repository. .gitignore:28 is node_modules/, which the walk also descends.

The walk currently prunes only .git and target. Extending that list is the wrong shape — the correct predicate is "tracked by git", and the ignore file will keep growing. git ls-files is the direct expression of it, or git check-ignore, either of which also removes the need to maintain a prune list at all.

Worth saying plainly, since I have been sceptical about this file's trajectory for a long stretch: these two are ordinary bugs in a small, readable predicate, not the class that produced the previous twenty-two. One needs a second env scope; one needs "tracked" instead of "present on disk". Both are bounded and neither requires interpreting anything. That is what the rewrite bought, and it holds even with two findings against it.

Not fixing these — still stood down. Everything else on this head remains clean: fmt, clippy, 7 tests, trailers gate, main current.


Generated by Claude Code

@pawellisowski

Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown
Contributor Author

034fd526 verified. Both findings closed, and the full battery still holds.

Case Result
baseline 7 passed, 0 failed
container.env.RUSTUP_TOOLCHAIN ✅ rejected
git-ignored .claude/worktrees/…/rust-toolchain.toml passes — false positive gone
tracked stray rust-toolchain (positive control) ✅ rejected
rustup_toolchain: lowercase ✅ rejected
rustup override set stable ✅ rejected

fmt clean, clippy clean, 7 tests, tree restored after each mutation including the staged one.

One result deserves a note so nobody later reads it as a hole. An untracked stray rust-toolchain file now passes:

untracked stray toolchain file    7 passed, 0 failed
tracked   stray toolchain file    6 passed, 1 failed   ← positive control

That is correct by design, not a gap. git ls-files scopes the check to the repository, which is what the gate is for: a CI checkout contains only tracked files, so CI is unaffected either way, and a developer's untracked scratch file is their own working tree rather than something a repository guard should police. The alternative — walking the filesystem — is what produced the .claude/worktrees/ false positive, and that list of directories to prune would have kept growing. I ran the positive control specifically because "switched to tracked-only" is the kind of change that can silently disable an assertion, and it does not here.

That closes everything I have. Across this branch: 22 findings against the original design and 6 against the rewrite, all verified by execution, all now fixed. I have not been able to construct anything further against this head.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 034fd526c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/tests/toolchain_pin_gate.rs Outdated
Comment on lines +161 to +163
run_text(step)
.filter(|run| invokes_cargo(run))
.map(|_| index)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for Cargo invoked by action steps

When the gates job's existing Swatinem/rust-cache@v2 step is moved above Install Rust, this test remains green because cargo_indices considers only steps with a run string; the repository itself records that this action shells out to cargo metadata (.github/workflows/ci.yml lines 91–92). That Cargo invocation can therefore run before the explicit pinned installation, including for the steel workspace outside cli/'s directory override. Fresh evidence beyond the prior direct-step ordering comment is this action-backed Cargo invocation; account for known Cargo-running uses steps when enforcing the ordering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Confirmed on 034fd526. Moving the existing Cache cargo registry + build step above Install Rust:

0 actions/checkout@v6
1 Read pinned toolchain
2 Cache cargo registry + build      ← shells out to `cargo metadata`
3 Install Rust ${{ steps.pin.outputs.channel }}
4 Install Linux build deps

test result: ok. 7 passed; 0 failed

The gate models a Cargo invocation as a run: containing cargo, so an action that invokes cargo without any run: text is invisible to it. This one is not speculative — ci.yml:91–92 is the repo's own comment recording that Swatinem/rust-cache shells out to cargo metadata for its cache key, which is also why the lockfile step sits where it does. Same ordering class as before, reached through uses: instead of run:.

A correction to my previous comment. I wrote "22 findings against the original design and 6 against the rewrite". The rewrite figure is wrong — it is 4: nested toolchain files, RUSTUP_TOOLCHAIN case-insensitivity, container.env, git-ignored directories. I appear to have double-counted the nested-file gap, which I found independently and Codex then reported; that is one finding observed twice, not two. With this one the rewrite total is 5, of which 4 are fixed and this is open.

I also want to be careful about a stronger claim I made in that same comment — "I have not been able to construct anything further against this head." That was true of my attempts and remains so, but this finding shows what my battery was blind to: every mutation I ran alters a run: script or a file, so a uses:-step bypass was outside the shape of everything I was testing. Worth knowing when weighing my "nothing further" reports — they cover the space I probed, which is narrower than the space that exists.

Not fixing this. Everything else on 034fd526 is unchanged: fmt, clippy, 7 tests, trailers gate clean, CI green, main current, tree restored after the mutation.


Generated by Claude Code

@pawellisowski

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 0cfa50a81f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor Author

Verified 0cfa50a8. The PR is now two workflow files, 35 insertions, 4 deletions — the fix alone, gate dropped.

Every dtolnay/rust-toolchain step in the repository, enumerated from the parsed YAML:

ci.yml       gates                     @master   toolchain=${{ steps.pin.outputs.channel }}
ci.yml       gates-macos               @master   toolchain=${{ steps.pin.outputs.channel }}
ci.yml       bridge-windows-packaged   @master   toolchain=${{ steps.pin.outputs.channel }}
release.yml  build                     @master   toolchain=${{ steps.pin.outputs.channel }}

No literal version and no floating channel left anywhere — the two conditions this PR opened on (toolchain: 1.88.0 and bare @stable) are both gone. cargo test --locked: 1626 passed, 0 failed; fmt and clippy clean; main current at a63836ce with the branch three commits ahead of it.

Dropping the gate is the right call, and I want to say so plainly given how much of this thread I spent on it. The fix is six lines of YAML per job, has been correct and CI-green since the first commit, and is verifiable by reading it. The gate meant to protect it absorbed twenty-eight findings across fourteen review rounds, was still producing them at the last round, and at its largest ran twenty-six tests over ~1,500 lines to guard six lines of YAML. Somewhere in there the ratio stopped making sense, and "keep the fix, drop the guard" is a cleaner resolution than a twenty-ninth patch.

Worth recording for whoever revisits this: the pin is not left unguarded in the way the thread might suggest. cli/rust-toolchain.toml is still the single source of truth, all four jobs now read it, and a drift reintroduced by hand would show up as a literal version in a two-line workflow diff — which is the kind of thing review catches without a test. The class of bug that started this (--manifest-path not moving rustup's lookup; working-directory masking an unpinned install) is closed by the change itself, not by the gate.

Nothing outstanding from me on this PR.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Both gate conditions are met on 0cfa50a8, for the first time on this branch.

  • Codex: clean on the final commit. "Didn't find any major issues"Reviewed commit: 0cfa50a81f, which is the head. Nothing outstanding.
  • CI: green on that same commit. Run 506, conclusion: success.
  • Locally: fmt clean, clippy clean, cargo test --locked 1626 passed / 0 failed on the pinned 1.95.0; main current at a63836ce; trailers gate clean; no merge conflict.

CLAUDE.md §Git workflow's carve-out permits the routine to merge its own routine/* PR at exactly this point, without asking. I have not, and here is why: you are actively on this PR — you rebased it, rewrote the guard, dropped it, and triggered the review that just came back clean, all within the last hour. The carve-out exists so an unattended PR does not stall for want of anyone to land it, and it permits rather than requires. Landing it under you while you are mid-flight would take the choice away for no benefit, and if you had one more change in mind it would cost you a new PR.

So: say the word and I will merge it (--squash --delete-branch, with the squash body supplied explicitly per §Git workflow so GitHub cannot synthesise a co-author trailer). If you have stepped away and it is still sitting here on my next wake, I will land it under the carve-out rather than leave it open.

For the record, since this thread is long: what merges is a 35-line change to two workflow files. release.yml was building the binaries shipped in every release archive on floating stable, and ci.yml's bridge-windows-packaged was building the host on a restated 1.88.0, seven minor versions behind the pin. All four cargo-building jobs now read cli/rust-toolchain.toml, and CI has confirmed Install Rust 1.95.0 in each.


Generated by Claude Code

@pawellisowski
pawellisowski merged commit dcfbd2c into main Sep 8, 2026
16 checks passed
@pawellisowski
pawellisowski deleted the routine/guardrails-2026-09-04 branch September 8, 2026 19:35
pawellisowski added a commit that referenced this pull request Sep 9, 2026
Refs #503.

Nothing has watched cli/rust-toolchain.toml since PR #490's guard was
withdrawn. This restores one in the shape that finally held there: the
workflow step reading the pin is an exact contract, compared verbatim, so
no shell is parsed and nothing can be smuggled past a pattern.

For every job that reaches Cargo: the approved reader present once, one
unconditional dtolnay/rust-toolchain@master installer consuming its output,
read-install-Cargo in that order, no rustup command or `cargo +toolchain`
selector, and no RUSTUP_TOOLCHAIN at workflow, job, container.env or step
scope (case-insensitively, because Windows resolves env vars that way).
Repository-wide, cli/rust-toolchain.toml must be the only tracked toolchain
file — via `git ls-files`, so a git-ignored worktree is not policed.

Cargo is not only `run: cargo …`. Swatinem/rust-cache shells out to
`cargo metadata`, so above the installer it builds its cache key on the
runner's default compiler with no script for a guard to read. That was the
finding left open when #490 was narrowed. Naming such actions is only worth
something if the list cannot rot, so every `uses:` in every workflow must be
classified as the installer, Cargo-running or Cargo-inert; an unclassified
one fails by name and asks for the decision.

ci.yml changes two lines the contract requires: `shell: bash` on the two pin
steps that lacked it, and the POSIX-sed note moved out of the script into a
YAML comment, so the four steps can be identical.

Codex raised four findings across five rounds, all real, all fixed at the
root and reproduced against the real workflows first: a bash line
continuation hiding a selector, `cargo.exe` not counting as Cargo, the
PowerShell and cmd continuations, and a continuation splitting the word
itself — which on the previous head meant such a job was not seen as a Cargo
job at all. Each was two of the guard's own notions disagreeing, so each fix
made them one shared definition rather than patching the instance.
</commit_message>
</invoke>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant