You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(ci): doc-snippet-types.yml's --build-filter step discards the gate's exit code inside a command substitution, so a failed filter yields an empty args and a step that succeeds #6221
Filed by the domain:devx @ objectui execution PM seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe, while reviewing PR #6215 (card #5465). ⛔ Filed unassigned, and deliberately not fixed in that PR — it is adjacent to its subject but outside its diff.
The line
.github/workflows/doc-snippet-types.yml, the step that derives the build filter:
- name: Derive the packages the covered snippets importid: filterrun: echo "args=$(node scripts/check-doc-snippet-types.mjs --build-filter)" >> "$GITHUB_OUTPUT"
The gate's exit status is discarded. A command substitution contributes its stdout to the surrounding word; the step's status is echo's. So if --build-filter ever exits non-zero, the step still succeeds and args is silently the empty string.
The next step is:
- name: Build those packagesrun: pnpm exec turbo run build ${{ steps.filter.outputs.args }} --concurrency=2
With args empty that becomes a bare turbo run build, i.e. every package in the workspace — precisely the unfiltered build the workflow's own header forbids in as many words:
⛔ Do not replace the filtered build with pnpm build. The filter is the reason this job is allowed to run on every pull request at all.
So the silent-failure mode is not "the build is skipped"; it is "the build quietly becomes the one thing this workflow was designed never to do", with no signal anywhere. Whether that then succeeds slowly or fails on an unrelated package, the log will not say the filter was the cause.
Why it does not manifest today
--build-filter is deliberately lenient: it exits 0 on an unbuilt tree, because this step runs one step before the build and must tolerate exactly that state. PR #6215 keeps that leniency on purpose and now pins the step order mechanically. So there is no live incident here and nothing to backfill — this is a latent hole that opens the day anything makes that flag exit non-zero.
⚠️ Which makes it worth recording now rather than later: the natural future change — "make --build-filter strict too, for consistency with the exit-2 work" — would be reasonable-looking and would land straight into this hole.
⭐ Why this is worth a card rather than a one-line fix
It is the same defect class, one layer up, as the one that falsified card #5465's premise, and the two were found within an hour of each other:
where
construct
consequence
the card's own measurement
node …mjs 2>&1 | tail -3; echo $?
$? is tail's status → 0. The card recorded a false green that the gate never produced, and the PM seat repeated it to three devs.
this workflow step
echo "args=$(node …mjs --build-filter)"
step status is echo's → success. A failed filter reads as a successful one.
Both are an exit code destroyed by the shell construct wrapped around it, and in both cases the result is indistinguishable from the healthy reading. The repository already teaches the remedy for the first form — "capture exit codes by redirect before any pipe" — and that rule, as written, does not obviously cover the second. A reader who has internalised "don't pipe" will still write the command substitution.
Possible dispositions, not chosen here
Split the step: run the gate into a variable with its status checked (set -o pipefail is not enough — there is no pipe), fail the step on non-zero, then write $GITHUB_OUTPUT.
Additionally, refuse an emptyargs in the build step. That is the belt-and-braces half and it is cheap: an empty filter can only ever mean something went wrong, since the covered-document population is never zero (its own floors already refuse that).
Worth considering separately: whether a mechanical check should look for exit-code-discarding shell constructs across .github/workflows/**. This is the second instance found in one evening, and the first one cost a wrong premise on a card plus three misinformed dispatch orders. ⚠️ Scope that carefully — a naive scan for $( would be almost all false positives, and a gate whose findings are mostly noise gets ignored, which is worse than none.
Bound
⛔ Not a defect in PR #6215, which is correct as written and whose leniency is what keeps this dormant. ⛔ No evidence this has ever fired: no run of this workflow is known to have produced an empty args. Filed because the mechanism is real, the failure is silent, and the obvious future edit walks into it.
Refs: #5465 / PR #6215 (where this was found, and the falsified-premise instance of the same class) · the workflow's own header, which states the unfiltered build as the thing to prevent.
Filed by the
domain:devx@ objectui execution PM seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe, while reviewing PR #6215 (card #5465). ⛔ Filed unassigned, and deliberately not fixed in that PR — it is adjacent to its subject but outside its diff.The line
.github/workflows/doc-snippet-types.yml, the step that derives the build filter:The gate's exit status is discarded. A command substitution contributes its stdout to the surrounding word; the step's status is
echo's. So if--build-filterever exits non-zero, the step still succeeds andargsis silently the empty string.The next step is:
With
argsempty that becomes a bareturbo run build, i.e. every package in the workspace — precisely the unfiltered build the workflow's own header forbids in as many words:So the silent-failure mode is not "the build is skipped"; it is "the build quietly becomes the one thing this workflow was designed never to do", with no signal anywhere. Whether that then succeeds slowly or fails on an unrelated package, the log will not say the filter was the cause.
Why it does not manifest today
--build-filteris deliberately lenient: it exits 0 on an unbuilt tree, because this step runs one step before the build and must tolerate exactly that state. PR #6215 keeps that leniency on purpose and now pins the step order mechanically. So there is no live incident here and nothing to backfill — this is a latent hole that opens the day anything makes that flag exit non-zero.--build-filterstrict too, for consistency with the exit-2 work" — would be reasonable-looking and would land straight into this hole.⭐ Why this is worth a card rather than a one-line fix
It is the same defect class, one layer up, as the one that falsified card #5465's premise, and the two were found within an hour of each other:
node …mjs 2>&1 | tail -3; echo $?$?istail's status → 0. The card recorded a false green that the gate never produced, and the PM seat repeated it to three devs.echo "args=$(node …mjs --build-filter)"echo's → success. A failed filter reads as a successful one.Both are an exit code destroyed by the shell construct wrapped around it, and in both cases the result is indistinguishable from the healthy reading. The repository already teaches the remedy for the first form — "capture exit codes by redirect before any pipe" — and that rule, as written, does not obviously cover the second. A reader who has internalised "don't pipe" will still write the command substitution.
Possible dispositions, not chosen here
set -o pipefailis not enough — there is no pipe), fail the step on non-zero, then write$GITHUB_OUTPUT.argsin the build step. That is the belt-and-braces half and it is cheap: an empty filter can only ever mean something went wrong, since the covered-document population is never zero (its own floors already refuse that).--build-filterstrict. That inverts the step-order requirement PR fix(scripts): doc-snippet gate exits 2 when it could not run, 1 when it found errors #6215 just pinned..github/workflows/**. This is the second instance found in one evening, and the first one cost a wrong premise on a card plus three misinformed dispatch orders.$(would be almost all false positives, and a gate whose findings are mostly noise gets ignored, which is worse than none.Bound
⛔ Not a defect in PR #6215, which is correct as written and whose leniency is what keeps this dormant. ⛔ No evidence this has ever fired: no run of this workflow is known to have produced an empty
args. Filed because the mechanism is real, the failure is silent, and the obvious future edit walks into it.Refs: #5465 / PR #6215 (where this was found, and the falsified-premise instance of the same class) · the workflow's own header, which states the unfiltered build as the thing to prevent.