Commit Graph
4 Commits
Author SHA1 Message Date
claude 5b46986cd7 test(hardlink): make the errexit invariant real, and stop gpg deciding a gate
CI / shellcheck + selftests (pull_request) Successful in 1m23s
Two review findings on #13.

THE STATED INVARIANT WAS NOT IN FORCE. The header claimed a `set -e` abort
could never be mistaken for the `1` that means "measured, and the answer is
mtime". It could not fire at all: `checksum_freshness_probe || probe_rc=$?`
runs the left side with errexit suppressed, and that suppression propagates
into the subshell. An unguarded step there fell through to `exit 0` and
reported ACTIVE — a fourth outcome the header said was impossible. The guard
audit was complete, so nothing was broken; the wrong mechanism had the credit,
in a comment inviting the next editor to add an unguarded step and rely on it.

The suggested fix — `set -e` as the first line of the subshell — does not work,
and measured on bash 5.3 it makes things worse rather than not-better:

  set -e inside a subshell called via `||` or `if`   still falls through: rc=0
  set -e inside, called with errexit off at the site aborts, but with the
                                                     FAILING COMMAND's status —
                                                     `false` gives 1, which is
                                                     exactly the value that
                                                     means "mtime"

So both halves are needed and neither is decoration: `set +e` around the call
site, so the subshell can arm its own errexit at all, and `trap 'exit 2' ERR`
inside it, so an abort lands on "not measured" instead of on an answer. The
explicit `|| exit 2` guards stay as the first line of defence. All of that is
now written down where the false claim was.

Red-proven with one unguarded `false` between the `touch` and the second
build, on a box where freshness is live: without the fix, mode `on` and 4
assertions — the reviewer's fourth outcome, reproduced; with it, mode
`unmeasured`, the warning, 3 assertions; unmodified, mode `on` and 4.

Recorded in the same block, since the next person to hit a `2` will ask: why it
skips rather than fails. The gated scenario is the only thing here that depends
on freshness mode, and failing would turn a statement about one machine into a
red gate reading "the hardlink scheme is broken" across three consuming repos.
What would change the answer is `2` becoming the everyday CI outcome, and run
2583 measures that it is not — gitdan-ci reports `1`.

GPG. The two suites that commit in scratch repos inherited the developer's
GLOBAL `commit.gpgsign`, so whether this gate passes could depend on their gpg
agent — seen as a red `restore-mtimes-selftest.sh` caused by a full disk
breaking gpg, in a suite with nothing to say about either. Pinned off locally,
in the throwaway repos only: `git config commit.gpgsign false` beside the
identity the scratch repo already sets, and `-c commit.gpgsign=false` on
prune-cache-selftest's three commits, matching its existing `-c` style.
Red-proven under a GIT_CONFIG_GLOBAL with gpgsign on and a nonexistent gpg
program: without it `fatal: failed to write commit object`, with it both
suites pass.
2026-08-26 14:28:22 -05:00
claude 3f97d3d1e7 fix(selftest): make the two compiler-backed suites survive a CI runner
CI / shellcheck + selftests (pull_request) Failing after 1m36s
The new gate's first run went red on both suites that drive a real Cargo.
Neither failure was a defect in what they test; both were assumptions about
the machine, which had only ever been a dev box with a nightly installed and
no colour forcing. Fixed here rather than waived — turning a gate on is what
obliges fixing what it finds.

COLOUR. Every assertion in restore-mtimes-selftest.sh, and one in
hardlink-clone-selftest.sh, reads cargo's own words out of a build log
(`Compiling libdep`, `Fresh probe`). gitdan-ci's runner image forces colour, so
cargo wrote `Compiling\e[0m libdep` into the log and `grep -q "Compiling
libdep"` stopped matching. The suite then reported the opposite of what
happened — the failure printed the log, and the log plainly said `Compiling
libdep`. Both suites now pin CARGO_TERM_COLOR=never, which is the format their
assertions are written against.

Red-proven: with the export removed and CARGO_TERM_COLOR=always,
restore-mtimes-selftest reproduces the CI failure verbatim ("expected to find:
Compiling libdep"); with it, 14/14 pass under the same forced colour.

THE NIGHTLY PROBE ASKED THE WRONG QUESTION. `cargo +nightly -V` answers "did a
cargo proxy called with +nightly exit 0", which is not "will this build have
checksum freshness": `-V` short-circuits before `-Z` is validated at all. So
the probe said "on" on a runner where the flag was not in effect, and the
suite asserted the checksum-freshness mutation family against a build that
never had it — failing in the CONTROL, where a failure reads as "the hazard is
gone" rather than "the toolchain is wrong". It now probes the capability:
`cargo +nightly -Z checksum-freshness locate-project`, the narrowest command
that actually parses the flag. It rejects the stable channel and an unknown
flag name alike, needs no network and builds nothing.

Red-proven: the old channel probe, pointed at a cargo without the flag,
reproduces the CI failure exactly.

AND THE OFF PATH DID NOT WORK EITHER. The suite's header claimed that without
checksum freshness "the test still covers the build/ and *.d families". It did
not: the final scenario backdates the source to 2001 and asserts the rebuild
is not Fresh, which is checksum-freshness-only reasoning. Under Cargo's
ordinary mtime freshness a 2001 source IS older than the artifact and Fresh is
the correct answer, so the scenario asserted a bug. It is now gated on the
mode and skipped loudly, like the control's dep-* assertion already was: 5
assertions with a nightly, 3 without.

CI installs a nightly as well as stable (nightly first, so stable stays the
default) — that hazard is the one this whole scheme exists to close, and a CI
that skips it is checking the cheap half.
2026-08-26 12:46:03 -05:00
claude f76789358d ci(gitea): gate scripts/ with shellcheck and the selftests
This repo had eight scripts, six selftest suites and nothing that ran any of
them. It is a composite action consumed at `@v1` — a moving tag — by three
repos' CI, so a bad edit here reaches zemyna, emowheel and lublub at once and
is discovered by whichever of them builds next.

One job: `shellcheck -x --source-path=scripts scripts/*.sh`, then
`bash scripts/selftest.sh`. `-x` follows the `. cache-lib.sh` every script
sources, which is where most of the logic being checked lives; without it
shellcheck reports SC1091 and analyses each file with a hole in it.

The full suite, not `--fast`: `hardlink-clone-selftest.sh` and
`restore-mtimes-selftest.sh` are the two that drive a real Cargo rather than a
fixture, and selftest.sh's own header says `--fast` is for iterating, not for
signing off a change. So the job installs a stable toolchain. The scratch
workspaces they build use path dependencies only, so nothing reaches
crates.io, and the job references no credentials at all.

Modelled on daniel/gitdan's workflow, including the two clauses of the
draft-skip guard (`github.event_name != 'pull_request' ||
!github.event.pull_request.draft` — the first is what stops the second from
also skipping pushes to `main`, where there is no `pull_request` context) and
the per-commit concurrency group for `push`.

No `container.volumes:` entry, deliberately: the suites want a cold target dir
every run, since "did this rebuild?" is exactly what restore-mtimes-selftest
asks. This job takes no share of the shared CI cache disk budget.

Turning the gate on surfaced three pre-existing findings, all fixed here
rather than suppressed or waived:

- `restore-mtimes-selftest.sh` SC2038: `find | xargs touch` -> `-print0 | xargs -0`.
- `hardlink-clone-selftest.sh` SC2295: `${f#$base_fix/}` -> `${f#"$base_fix"/}`.
- `cache-lib.sh` SC2016: a per-line disable with the rationale. The single
  quotes are load-bearing — the program is for the inner shell, where `$f` is
  its loop variable, `$rc` its accumulator and `$$` its pid — so this is the
  documented-false-positive case, not a stopgap.
2026-08-26 12:31:50 -05:00
claudeandClaude Opus 5 248af3061e feat(cargo-cache): hardlink-clone a per-ref Cargo cache from a published snapshot
Replaces the phase-0 resolution probe with the real actions, merging the two
independent per-branch Cargo cache implementations on this forge into the
design neither of them had.

## The merge

- zemyna seeds a PR branch by `cp -al` hardlink clone (near-free: cost scales
  with inode count, not bytes) from the base branch's LIVE target dir — a
  torn read waiting for a second job slot (its own #911).
- emowheel seeds from a PUBLISHED IMMUTABLE SNAPSHOT (no race by
  construction) but with `cp -a`, duplicating ~35 GB per branch.

This ships hardlink-clone FROM a published snapshot: zemyna's cost profile,
emowheel's soundness, and #911 closed structurally rather than by the runner
happening to have one execution slot.

## The bug both implementations have

A build inside a `cp -al` clone DOES mutate the directory it was cloned from.
Cargo replaces real artifacts, but writes its metadata — and build scripts
write their OUT_DIR — with a plain truncating write, straight through the
shared inode. Measured set: `.fingerprint/<unit>/dep-<target>` (under
CARGO_UNSTABLE_CHECKSUM_FRESHNESS), `build/<pkg>/{output,root-output,out/**}`,
`deps/*.d` and `<profile>/*.d`.

The checksum-freshness case is a wrong answer, not a slow build: a PR clone
rewrites the base's dep-info to describe the PR's sources while the base's
cache still holds the artifact built from the base's; once the PR merges, the
base's next run finds the checksums match, reports `Fresh`, and links a binary
built from the pre-merge code. Reproduced end to end.

Fix: hardlink the artifacts (the GB), real-copy the metadata (the MB) — about
3.7% of a 6.9 GB Bevy target dir, against 100% for a full copy.

## Contents

- `cargo-cache/action.yml` — consume: resolve keys, seed from the base's
  snapshot via staging + one atomic rename, strip Cargo lock files, unshare
  the mutable paths, restore mtimes from git history, lock, prune.
- `cargo-cache-publish/action.yml` — publish: record the build watermark,
  atomically republish the snapshot on a protected branch, release the lock
  (`mode: release-lock` for the `if: always()` step).
- `scripts/` — all logic, so it is testable standalone; the YAML is wiring.
- `scripts/*selftest.sh` + `selftest.sh` — five suites, 63 assertions, every
  fix paired with a control that reproduces the bug. All green locally.

Eviction merges emowheel's liveness pass (dead branches pruned
unconditionally, not gated on disk pressure) with LRU-under-pressure, but
inverts the order within the pressure pass: `target-*` before `snapshot-*`,
because a snapshot is hardlinked to everything cloned from it, so evicting one
frees almost no real bytes while costing every future PR its warm start.

restore-mtimes.sh is ported from emowheel (the watermark variant, which closes
the merge hazard zemyna's copy still has) with its provenance de-projectised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sqh2vscfzisk83VuPVQX9L
2026-08-23 14:18:01 -05:00