build(ci): pin ruff like shellcheck, and route CI through the wrapper #399
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-infra!399
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "feat/pin-ruff"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
Follows the
scripts/shellcheck.sharrangement rather than inventing a second one.The gap
ruffwas the only linter with no version anywhere:So CI ran whatever ruff was newest on the day the image was last baked — a version the repo could
not name and no developer could reproduce.
This is the 2026-07-30 skew that produced
scripts/shellcheck.sh, with a worse property: that timeboth versions were at least knowable. ruff adds rules in minor releases, so the skew made CI
either stricter or laxer than every developer's machine depending on a bake date, and which
direction was invisible from the repo.
It is not hypothetical — it bit this session.
apply-reconcile.pypasseduvx rufflocally andthen failed CI on S606.
Changes
scripts/ruff.shghcr.io/astral-sh/ruff:0.16.1) first, local binary as a fallback that warns loudly and names both versions.forgejo/workflows/ci.ymlruff check .scripts/bake-runner-image.shRUFF_VERSION, plumbs it through the quoted heredoc's sudo env like the other pins, and asserts it after installCI is fixed now, not at the next bake. Because CI goes through the wrapper it uses the pinned
container, so it no longer depends on what is baked into the image at all — the baked binary becomes
the fallback. Pinning it there keeps that fallback honest rather than silently divergent.
The post-install assertion is deliberate: an unpinned install is the bug being fixed, so a silent
fallback to some other version has to fail the bake rather than be discovered later.
Verified with both controls — passes on
ruff 0.16.1, fails on a wrong pin.Read-only mount forces
--no-cacheruff writes
.ruff_cache/beside the config and dies withRead-only file systemwithout it. Thatis the better default anyway: a linter should not be able to modify the repo, and it stops a
root-owned cache directory appearing. Gitignored regardless, since a local fallback still writes one.
Deliberately out of scope
ansibleandansible-lintare still unpinned in the bake. Same shape of gap, but their upgradeblast radius is the whole estate rather than one lint step, so they want their own change with their
own testing. Noted in the script.
Gates
./scripts/ruff.sh— container path,All checks passed!; also exercised with a narrowed targetRUFF_NO_CONTAINER=1 ./scripts/ruff.sh— fallback branch reaches its no-binary error correctly./scripts/shellcheck.sh— clean across all scriptsbash -n scripts/bake-runner-image.sh— parsesOne small confession recorded in the header: this file's own prose tripped SC1072/SC1073 on its
first run, by wrapping a line so it began with the other linter's name and parsed as a directive —
the exact hazard
shellcheck.shdocuments. A fair advertisement for pinning linters.ruff was the one linter with no version anywhere. The runner image installed it with a bare `pip install ... ruff`, two lines below pinned NODE_VERSION, GITLEAKS_VERSION and TOFU_VERSION — so CI ran whatever was newest on the day the image was last baked, a version the repo could not name and no developer could reproduce. That is the 2026-07-30 skew again with a worse property: that time both versions were at least knowable. ruff adds rules in minor releases, so the skew made CI either stricter or laxer than every developer's machine depending on a bake date, and which direction was invisible. Three changes, mirroring the shellcheck arrangement rather than inventing a second one: * scripts/ruff.sh — pinned container first (ghcr.io/astral-sh/ruff), local binary as a fallback that warns loudly and names both versions. * CI calls the wrapper instead of a bare `ruff check .`, so CI is fixed now rather than at the next bake. * bake-runner-image.sh pins RUFF_VERSION, plumbs it through the quoted heredoc's sudo env like the other pins, and ASSERTS it after install — an unpinned install is the bug, so a silent fallback to another version must fail the bake. The container mount is read-only, which forces --no-cache: ruff writes .ruff_cache/ beside the config and dies without it. That is the better default anyway — a linter should not be able to modify the repo — and it stops a root-owned cache dir appearing. Gitignored regardless, since a local fallback still writes one. ansible and ansible-lint remain unpinned in the bake. Same shape of gap, but their blast radius is the whole estate rather than a lint step, so they want their own change. Noted in the header: this file's own prose tripped SC1072/SC1073 on the first run by wrapping a line onto the other linter's name, which is a fair advertisement for pinning linters.