Skip to content

CI red on main: idd-tree-lock file_mtime() uses BSD-first stat, breaks f8 fixture on GNU/Linux #245

Description

@kiki830621

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions