Repository navigation
OpenSpec workflows use GNU-only find -printf, which fails silently off Linux #272
Description
The three OpenSpec reporting workflows list change directories with find -printf, which is a GNU extension. It works on ubuntu-latest and fails on macOS and any BSD userland:
$ find openspec/changes -mindepth 1 -maxdepth 1 -type d -not -name archive -printf '%f\n'
find: -printf: unknown primary or operator
Three occurrences, one per workflow:
.github/workflows/openspec-label.yml:69.github/workflows/openspec-tracking.yml:98.github/workflows/openspec-sweep.yml:84
Why this is worth fixing rather than shrugging at
The failure is silent, not loud. In openspec-sweep.yml the find sits inside a process substitution feeding a while loop:
done < <(find openspec/changes -mindepth 1 -maxdepth 1 -type d -not -name archive -printf '%f\n' | sort)A process substitution's exit status is not the loop's, so set -euo pipefail does not catch it. The loop simply receives no input, unarchived stays [], and the job reports success having examined nothing. Measured while verifying the sweep by hand on macOS: find printed its error to stderr and the script carried on to print [] and exit 0.
That is the same class of problem the whole reporting mechanism exists to avoid. A check that reports "nothing to report" because it could not read the directory is worse than one that fails, because "no findings" reads as healthy.
It is currently latent, and that is the point. Every runner is ubuntu-latest, so nothing is broken in CI today. It becomes real the moment someone reproduces a workflow step locally on a Mac (which is how it was found), or a runner image changes, or the logic is lifted into a script people run outside Actions.
What to do
Options, roughly in order of preference:
- Have the workflows call
openspec-tracking.cjsfor the listing.listUnarchivedChanges()already does exactly this in Node, portably, and is covered by tests. That removes the third copy of a rule that is currently stated once in JS and twice in bash, which is the same argument that consolidated the issue-body marker in feat(ci): report unarchived OpenSpec changes without blocking anything #269. - Detect the userland and branch. Probe once (
find . -maxdepth 0 -printf '' 2>/dev/null) and fall back to a portable form such asfind ... -exec basename {} \;orfor d in openspec/changes/*/. - Use a portable form unconditionally. Simplest, and gives up nothing here since the only thing
-printf '%f\n'buys over a plain loop is brevity.
Whichever is chosen, the fix should also make the read failure loud: if the directory cannot be listed, the workflow should say so explicitly rather than proceeding with an empty list, since an empty list is indistinguishable from a clean repository.
Verifying a fix
Run the listing step on macOS and on Linux and confirm both produce the same set, including a directory name containing a space (the encoding downstream was fixed for that in #269, so the listing should not reintroduce it):
mkdir -p "openspec/changes/probe with spaces" openspec/changes/probe-normal
# listing under test here
rmdir "openspec/changes/probe with spaces" openspec/changes/probe-normalFound while hand-verifying the dispatched OpenSpec Sweep run after #269 landed, because main carried no unarchived change and the CI run therefore never exercised the listing loop at all.
Refs #269
Activity
theCodeDrift commentedon Sep 6, 2026
Correcting the "Why this is worth fixing" section above: I asserted the failure is silent at all three sites, and that is only true at one. Measured on macOS with a
bash -creproduction of each shape:openspec-label.yml:69changes=$(find …)set -efiresopenspec-tracking.yml:98find … | sort | jqpipefailpropagates itopenspec-sweep.yml:84done < <(find …)count=0, exit 0So there are two distinct failures, not one:
Both still resolve to the same fix, but a fixer should verify both properties: the listing must work off Linux, and an unreadable directory must produce an explicit annotation rather than either an empty list or a failed run.
— AI Coding Agent