Symptom
CI is red on main (and every recent commit). The GitHub Actions shell-test suite reports:
idd-tree-lock FAIL(rc=1)
✗ f8 fresh unreadable lock -> held (not stolen): expected exit 3 got 1
21 suites run, 1 fails. The failure is environment-specific: the same fixture passes locally on macOS (20/20 green) but fails on the GNU/Linux CI runner. A red main obscures the CI signal for every future PR, so this is the highest-priority fix in the backlog.
Root cause — BSD-first stat breaks on GNU/Linux
scripts/idd-tree-lock.sh:65:
file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null; }
This tries BSD syntax first. The two stat dialects interpret the flags incompatibly:
| flag |
BSD / macOS |
GNU / coreutils (Linux CI) |
-f |
format string (%m = mtime) |
--file-system (a boolean; prints multi-line filesystem status) |
-c |
illegal option → clean usage error, no stdout |
format string (%Y = mtime) |
On the Linux runner stat -f %m "$LOCK" is parsed as --file-system with operands %m and $LOCK: the %m operand doesn't exist (nonzero exit → the || also runs stat -c %Y), but $LOCK does exist, so GNU stat prints its filesystem status (multi-line human text) to stdout. file_mtime therefore returns "<multi-line fs garbage>\n<real mtime>".
That garbage then flows into holder_is_live():
mtime="$(file_mtime "$LOCK")"
if printf '%s' "$mtime" | grep -qE '^[0-9]+$'; then # -q matches the last (integer) line → true
age=$(( $(now_epoch) - mtime )) # ← arithmetic on multi-line string: ERROR
[ "$age" -lt "$TTL" ] && return 0 || return 1 # age is empty → [ fails → return 1 (NOT live)
fi
Fixture f8 plants an empty, fresh lock file and expects it to be treated as a mid-acquire window → held (exit 3). On macOS the mtime-freshness path returns cleanly, so holder_is_live returns 0 → acquire exits 3. On Linux the corrupted arithmetic makes holder_is_live return 1 (falsely "not live") → acquire proceeds down the reclaim path → exit ≠ 3. Hence expected exit 3 got 1.
This is the only occurrence of the pattern in the codebase (grep-confirmed).
Fix
Try GNU-first, which resolves cleanly on both platforms:
file_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
- GNU/Linux:
stat -c %Y succeeds → returns a bare integer, || short-circuits (no filesystem garbage ever produced).
- BSD/macOS:
stat -c %Y fails cleanly (illegal option → usage error to stderr, no stdout — verified locally: rc=1, empty stdout) → || falls through to stat -f %m → correct mtime.
Acceptance
- A new regression fixture shims a GNU-semantics
stat onto PATH and asserts the f8 scenario (empty fresh lock) still reports held (exit 3) — RED with the current BSD-first order, GREEN after the swap. This makes the Linux-only failure reproducible on any host.
- The full
test.sh suite stays green locally (macOS).
- CI on
main returns green.
Symptom
CI is red on
main(and every recent commit). The GitHub Actions shell-test suite reports:21 suites run, 1 fails. The failure is environment-specific: the same fixture passes locally on macOS (20/20 green) but fails on the GNU/Linux CI runner. A red
mainobscures the CI signal for every future PR, so this is the highest-priority fix in the backlog.Root cause — BSD-first
statbreaks on GNU/Linuxscripts/idd-tree-lock.sh:65:This tries BSD syntax first. The two
statdialects interpret the flags incompatibly:-f%m= mtime)--file-system(a boolean; prints multi-line filesystem status)-c%Y= mtime)On the Linux runner
stat -f %m "$LOCK"is parsed as--file-systemwith operands%mand$LOCK: the%moperand doesn't exist (nonzero exit → the||also runsstat -c %Y), but$LOCKdoes exist, so GNU stat prints its filesystem status (multi-line human text) to stdout.file_mtimetherefore returns"<multi-line fs garbage>\n<real mtime>".That garbage then flows into
holder_is_live():Fixture f8 plants an empty, fresh lock file and expects it to be treated as a mid-acquire window → held (exit 3). On macOS the mtime-freshness path returns cleanly, so
holder_is_livereturns 0 → acquire exits 3. On Linux the corrupted arithmetic makesholder_is_livereturn 1 (falsely "not live") → acquire proceeds down the reclaim path → exit ≠ 3. Henceexpected exit 3 got 1.This is the only occurrence of the pattern in the codebase (grep-confirmed).
Fix
Try GNU-first, which resolves cleanly on both platforms:
stat -c %Ysucceeds → returns a bare integer,||short-circuits (no filesystem garbage ever produced).stat -c %Yfails cleanly (illegal option → usage error to stderr, no stdout — verified locally:rc=1, empty stdout) →||falls through tostat -f %m→ correct mtime.Acceptance
statontoPATHand asserts the f8 scenario (empty fresh lock) still reports held (exit 3) — RED with the current BSD-first order, GREEN after the swap. This makes the Linux-only failure reproducible on any host.test.shsuite stays green locally (macOS).mainreturns green.