ci: lockfile gate, release tag guard, and SHA-pinned actions#3
Merged
Conversation
release.yml builds with `--locked`, but `just test`/`just lint` do not, so a stale lock passed every PR check and first failed at tag-push time -- the one moment it is most expensive to discover. Add a `just check-lock` recipe covering all six committed lockfiles (the workspace plus each of the five excluded plugins/*, which the root cargo commands never see) and run it in ci.yml's test job ahead of `just test`. Kept as its own recipe rather than bolting `--locked` onto test/lint so a local `just test` keeps auto-refreshing the lock as it does today. It uses `cargo metadata`, which resolves without compiling, so the gate costs about a second. Verified it actually catches something, not just that it passes: temporarily adding an unlocked dependency to a workspace member and, separately, to plugins/counter makes the recipe fail in both cases. Claude-Session: https://claude.ai/code/session_01QUGuvDUj6gG5xQPiN3X2bG
Tarballs are named from GITHUB_REF_NAME while the binary reports CARGO_PKG_VERSION, and nothing connected the two -- a `v0.2.0` tag would happily ship a binary that identifies itself as 0.1.0, under a filename claiming otherwise. Add a guard between Build and Package on every leg. It asks the freshly built binary (`rustline --version`) instead of re-reading Cargo.toml, so it checks the artifact actually being shipped; that costs nothing, since Package already runs the same binary to emit shell completions and every target is native to its runner. Gated on `github.ref_type == 'tag'`. A workflow_dispatch dry run is on a branch, whose name is not a version, so an ungated check would fail every dry run -- confirmed by running the step's shell body locally against a branch-shaped ref, alongside the matching and mismatching tag cases. Claude-Session: https://claude.ai/code/session_01QUGuvDUj6gG5xQPiN3X2bG
Every action was on a mutable major tag (@v4, @v2) and dtolnay/rust-toolchain on a *branch*, so upstream could change under us -- and release.yml runs with `contents: write`, which is where that matters. Pin all seven to full commit SHAs in both workflows, each carrying a trailing `# vX.Y.Z` comment, and add .github/dependabot.yml so the pins get refreshed by PR rather than silently rotting into frozen unpatched versions. Dependabot is deliberately github-actions only: Rust bumps go through /upgrade-packages, and cargo PRs would bury the action refreshes this exists to surface. Two things worth recording about the pins themselves: dtolnay/rust-toolchain's ref is not merely a version, it selects the toolchain -- each branch (stable, nightly, 1.90.0) ships an action.yml whose `toolchain` input defaults to that branch's own name. Its pin is therefore the tip of the `stable` branch, verified by reading action.yml at that SHA and confirming the default is `stable`. Repointing it at another branch's commit would silently change the Rust version with nothing in the diff to say so. It publishes no release tags, so dependabot cannot bump that one. Swatinem/rust-cache is pinned to the v2.9.1 release commit rather than to what the floating v2 tag resolved to; v2 sat exactly one changelog-only commit ahead of the release, so this is functionally identical and makes the comment true. Note this pins the action *code*, not the Rust version: `stable` still resolves at job time, so a new stable release can still redden CI with no repo change. Addressing that would need a rust-toolchain.toml, which CLAUDE.md rules out because it would change every developer's local toolchain resolution. Each pinned SHA was verified against the version its comment claims via the GitHub API. Claude-Session: https://claude.ai/code/session_01QUGuvDUj6gG5xQPiN3X2bG
CLAUDE.md's Development section described the CI/release workflows in enough detail that all three changes on this branch would have left it stale. - `just check-lock` added to the recipe list and to the "commit Cargo.lock alongside any dependency change" bullet, which now has a gate behind it. - The CI paragraph notes the test job runs check-lock ahead of just test. - A new bullet covers the SHA pins, dependabot's github-actions-only scope, the dtolnay/rust-toolchain ref-selects-the-toolchain wrinkle, and the fact that pinning fixes action code but not the Rust version. - The release roadmap entry documents the tag/version guard and why it is gated on ref_type == 'tag'. README gains check-lock in its recipe list, framed around what it catches. Claude-Session: https://claude.ai/code/session_01QUGuvDUj6gG5xQPiN3X2bG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Clears the four CI/release follow-ups left over from the 2026-07-25 branch reviews.
What's here
1. Fail a PR whose
Cargo.lockis stalerelease.ymlbuilds with--locked; nothing else did, so a stale lock passed every PR check and first failed at tag-push time — the most expensive moment to find out. Newjust check-lockrecipe covers all six committed lockfiles (workspace + each of the five excludedplugins/*, which the rootcargocommands never see), wired into ci.yml'stestjob.Kept as its own recipe rather than bolting
--lockedontotest/lint, so a localjust testkeeps auto-refreshing the lock as it does today. Usescargo metadata, which resolves without compiling — about a second.2. Refuse a tag that disagrees with the built binary
Tarballs are named from
GITHUB_REF_NAMEwhile the binary reportsCARGO_PKG_VERSION, and nothing connected the two — av0.2.0tag would happily ship a binary calling itself0.1.0. The guard asks the freshly built binary (rustline --version) rather than re-readingCargo.toml, so it checks the artifact actually being shipped, at no cost (Package already runs that binary for completions).Gated on
github.ref_type == 'tag'— aworkflow_dispatchdry run is on a branch, whose name is not a version, so an ungated check would fail every dry run.3. SHA-pin the actions + Dependabot
All seven actions pinned to full commit SHAs in both workflows with
# vX.Y.Zcomments, plus.github/dependabot.ymlso the pins get refreshed by PR instead of rotting into frozen unpatched versions. Dependabot is deliberately github-actions only — Rust bumps go through/upgrade-packages, and cargo PRs would bury the action refreshes.4.
macos-13runner label — already resolved by3ac7c08, which dropped the Intel Mac leg outright rather than keep depending on that label. No change needed here.Two things worth knowing about the pins
dtolnay/rust-toolchain's ref selects the toolchain. Each branch (stable,nightly,1.90.0) ships anaction.ymlwhosetoolchaininput defaults to that branch's own name. Its pin is the tip of thestablebranch — verified by readingaction.ymlat that SHA. Repointing it at another branch's commit would silently change the Rust version with nothing in the diff to say so. It publishes no release tags, so Dependabot can't bump that one.stablestill resolves at job time, so a new stable release can still redden CI with no repo change. Fixing that needs arust-toolchain.toml, which CLAUDE.md rules out (it would change every developer's local toolchain resolution).Also:
Swatinem/rust-cacheis pinned to thev2.9.1release commit rather than what the floatingv2tag resolved to —v2sat exactly one changelog-only commit ahead, so this is functionally identical and makes the comment truthful.Verification
just check-lockproven to catch something, not merely to pass: temporarily adding an unlocked dependency to a workspace member and, separately, toplugins/countermakes it fail in both cases.::error::annotation, and a branch-shaped ref fails (which is what makes theref_typegate load-bearing rather than decorative).just check-lock,just lint, andjust testall pass — 937 tests, 13 binaries, 0 failures.dependabot.ymlparse as YAML.actionlintwas not available in this environment and was not run.No Rust source is touched on this branch.
https://claude.ai/code/session_01QUGuvDUj6gG5xQPiN3X2bG