build(ci): pin ruff like shellcheck, and route CI through the wrapper #399

Sammanfogat
supernaut sammanfogade 1 incheckning från feat/pin-ruff in i main 2026-08-06 08:58:56 +00:00
Ägare

Follows the scripts/shellcheck.sh arrangement rather than inventing a second one.

The gap

ruff was the only linter with no version anywhere:

# scripts/bake-runner-image.sh
NODE_VERSION="${NODE_VERSION:-24.18.0}"
GITLEAKS_VERSION="${GITLEAKS_VERSION:-8.30.1}"
TOFU_VERSION="${TOFU_VERSION:-1.10.6}"
...
pip install --break-system-packages ansible ansible-lint ruff     # <-- no pin

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 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 from the repo.

It is not hypothetical — it bit this session. apply-reconcile.py passed uvx ruff locally and
then failed CI on S606.

Changes

scripts/ruff.sh pinned container (ghcr.io/astral-sh/ruff:0.16.1) first, local binary as a fallback that warns loudly and names both versions
.forgejo/workflows/ci.yml calls the wrapper instead of bare ruff check .
scripts/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

CI 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.

ruff --version | grep -qx "ruff ${RUFF_VERSION}" || { ...; exit 1; }

Verified with both controls — passes on ruff 0.16.1, fails on a wrong pin.

Read-only mount forces --no-cache

ruff writes .ruff_cache/ beside the config and dies with Read-only file system 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 directory appearing. Gitignored regardless, since a local fallback still writes one.

Deliberately out of scope

ansible and ansible-lint are still unpinned in the bake. Same shape of gap, but their upgrade
blast 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 target
  • RUFF_NO_CONTAINER=1 ./scripts/ruff.sh — fallback branch reaches its no-binary error correctly
  • ./scripts/shellcheck.sh — clean across all scripts
  • bash -n scripts/bake-runner-image.sh — parses

One 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.sh documents. A fair advertisement for pinning linters.

Follows the `scripts/shellcheck.sh` arrangement rather than inventing a second one. ## The gap `ruff` was the only linter with **no version anywhere**: ```bash # scripts/bake-runner-image.sh NODE_VERSION="${NODE_VERSION:-24.18.0}" GITLEAKS_VERSION="${GITLEAKS_VERSION:-8.30.1}" TOFU_VERSION="${TOFU_VERSION:-1.10.6}" ... pip install --break-system-packages ansible ansible-lint ruff # <-- no pin ``` 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 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 from the repo. It is not hypothetical — it bit this session. `apply-reconcile.py` passed `uvx ruff` locally and then failed CI on **S606**. ## Changes | | | | --- | --- | | `scripts/ruff.sh` | pinned container (`ghcr.io/astral-sh/ruff:0.16.1`) first, local binary as a fallback that warns loudly and names both versions | | `.forgejo/workflows/ci.yml` | calls the wrapper instead of bare `ruff check .` | | `scripts/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 | **CI 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. ```bash ruff --version | grep -qx "ruff ${RUFF_VERSION}" || { ...; exit 1; } ``` Verified with both controls — passes on `ruff 0.16.1`, fails on a wrong pin. ## Read-only mount forces `--no-cache` ruff writes `.ruff_cache/` beside the config and dies with `Read-only file system` 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 directory appearing. Gitignored regardless, since a local fallback still writes one. ## Deliberately out of scope `ansible` and `ansible-lint` are still unpinned in the bake. Same shape of gap, but their upgrade blast 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 target - `RUFF_NO_CONTAINER=1 ./scripts/ruff.sh` — fallback branch reaches its no-binary error correctly - `./scripts/shellcheck.sh` — clean across all scripts - `bash -n scripts/bake-runner-image.sh` — parses One 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.sh` documents. A fair advertisement for pinning linters.
supernaut lade till 1 incheckning 2026-08-06 08:55:12 +00:00
build(ci): pin ruff like shellcheck, and route CI through the wrapper
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m43s
0b3a5eb514
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.
supernaut sammanfogade incheckning 42c501fab3 till main 2026-08-06 08:58:56 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra!399
Ingen beskrivning angiven.