Skip to content

v4.13.1

Choose a tag to compare

@github-actions github-actions released this 21 Aug 15:38
· 99 commits to main since this release

v4.13.1 — a test could delete your temp directory

Full entry: CHANGELOG.md § [4.13.1]. This file records release evidence and the upgrade note.

Take this release if you run the test suite on Windows. Reported by the paperclip+hermes-v1
consumer as escalation E7, after losing data to it three times.

The defect

Three tests defined their cleanup before assigning the variable it deletes:

finish() { [ -n "${TMP:-}" ] && rm -rf "$TMP"; ... }   # line 27
...
TMP="$(mktemp -d)"                                      # line 48

On Windows, TMP is a pre-set environment variable — typically
C:\Users\<you>\AppData\Local\Temp. Any early exit between those lines therefore ran rm -rf
against the operator's entire user temp directory. The [ -n ... ] guard did not help: a non-empty
value is exactly what caused it.

The trigger is a shallow .fusebase-flow-source clone — the documented adoption command
(git clone --depth 1). So it fired on the recommended path, under
FF_FULL=1 bash hooks/tests/run-tests.sh, which the adoption docs suggest for verification.

Affected: test-history-extraction.sh, test-budget-literals.sh, test-token-waste-classify.sh.

The fix

  • Variables renamed to script-namespaced names that cannot collide with an environment variable. Renaming, not initialising — initialising would leave the hazard for the next editor.
  • Every rm -rf is now conditional on a path the script itself created, not on "the variable is non-empty".
  • Tripwire comments at each site naming this incident.
  • test-po-investigate.sh was hardened too. Its tripwire states plainly that it was not destructive — no reachable cleanup existed before its assignment — but its delete was unguarded, so one inserted early-exit would have made it the fourth.

The regression rows — and why the obvious rule was wrong

Two rows now guard this, in a fast-tier phase so they run on every local default run, not only in CI.

Getting the rule right took three attempts, which is the part worth recording:

Rule Flags
ban rm -r on environment-named variables 22 files, nearly all safe
ban cleanup reachable before assignment 106 files, nearly all harmless — an unassigned $FIX aborts under set -u
the conjunction of both exactly 3 before the fix, 0 after

Only the conjunction is the defect: an unassigned $TMP does not abort — it silently resolves to a
real directory. The rule independently corroborates that test-po-investigate.sh was never
destructive, by not flagging it.

Shallow clones now skip, with a gate

A depth-1 clone genuinely lacks the pre-extraction blob — a missing premise, not a failed
contract — and the documented adoption path should not turn a consumer's first run red.

But the skip is gated on the repository actually being shallow. On a full clone, an unreachable
parent means history was rewritten, and that remains a hard FAIL. Otherwise the skip would hide the
exact regression the phase exists to catch. Verified both ways: depth-1 → 6 skip rows; the same clone
unshallowed → FAIL: extraction-commit-resolved.

Each skipped assertion emits its own row naming the reason and stating NOT VERIFIED, so it cannot
read as a pass.

Release evidence

Item Value
Tag verification the v4.13.1 run is the authoritative gate; a local run is never release evidence
Pre-tag verification 3227b2c green on both platforms
Reproduction consumer's documented trigger, against a throwaway directory: victim destroyed pre-fix, survived post-fix, all four subjects

Known residual — not fixed here

test-upgrade-source-boundary.sh also fails on a shallow clone, with a different cause: it
contains zero references to TMP/TEMP/TMPDIR and no git-history operations. Its failures were
MSYS fork exhaustion and eol_guard.py is not loadable beside managed_content_manifest.py (ModuleNotFoundError) — the latter clone-independent and apparently real. It gets its own ticket
rather than being folded into a data-loss fix.