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): two workflow steps discard a piped command's exit status into a Playwright cache key — playwright-Linux- or -null on failure, and the scoping rule is now measured (3 hits repo-wide, 2 real) #6231
Filed by the domain:devx @ objectui execution PM seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe. This is the follow-up #6221 deferred, now filable because the scoping rule has been measured rather than guessed.
The two instances
Verified on origin/main — byte-identical lines in two files:
The exit status is discarded twice over: echo owns the step's status, and there is a pipe inside the substitution whose status is jq's. Either failure yields an empty or nullversion and a successful step.
so a failure degrades it to playwright-Linux- or playwright-Linux-null. ⚠️jq -r prints the string null and exits 0 for a missing field, so a renamed or moved key in pnpm list's JSON produces the -null bucket with every command in the pipeline reporting success.
Consequence, stated at its real size
⛔ This is milder than #6221's unfiltered build and should not be sold as urgent. A wrong cache bucket means a cache miss — browsers get re-downloaded, the job is slower, nothing is incorrect. The sharper risk is the stable-wrong bucket: playwright-Linux-null is a valid key that persists, so two different Playwright versions could share one cache entry and a stale browser could be restored. No evidence that has happened.
⭐ The scoping rule, measured — this is why the card is filable now
#6221 deliberately did not add a repo-wide scan, on the grounds that a naive $( sweep would be almost all false positives and a gate whose findings are mostly noise gets ignored. That caution is now a measurement, taken by the #6221 dev across .github/workflows/**:
predicate
hits
real
any command substitution outside comments
38, across 27 files
almost all false positives — the caution was right
a command substitution whose whole value is interpolated into an echo appending to $GITHUB_OUTPUT
3 repo-wide
2 (the pair above)
The third hit is benign: performance-budget.yml:138, echo "entry_file=$(basename $ENTRY_FILE)" — basename on an already-validated path.
⭐ And the forms that are safe are safe by the repo's own convention, which a gate must not flag:
a bare X=$(cmd) assignment does propagate its status under set -e;
the explicitly-checked if ! CHANGED=$(git diff …) form appears five times (ci.yml ×4, lint.yml).
So the narrow predicate has a 3-hit surface with a 2/3 true-positive rate, against 38 hits at near-zero. That is the difference between a gate worth writing and one that gets deleted.
Fix them and add the narrow gate, using the measured predicate above rather than a $( sweep. ⚠️ Whoever scopes it must keep the two safe conventions passing — a gate that reds on if ! CHANGED=$(git diff …) would be reverted the same day.
Fix the two and record the predicate in the CI guide without automating it.
⚠️ Note the two lines are identical, in two workflows, which is itself the argument for either 2 or a shared step: the copy already happened once.
Bound
⛔ Not a defect in PR #6228 (#6221), which fixed a different and more serious instance and correctly declined to widen. ⛔ No evidence either line has ever failed. Filed because the mechanism is real, the failure is silent, and — unlike an hour ago — the scan that would catch the class is now a specified predicate with a measured false-positive surface.
Refs: #6221 / PR #6228 (the same defect class, one layer more serious, plus the measurement that scoped this) · #5465 / PR #6215 (the | tail instance that started this family and falsified a card's premise).
Filed by the
domain:devx@ objectui execution PM seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe. This is the follow-up #6221 deferred, now filable because the scoping rule has been measured rather than guessed.The two instances
Verified on
origin/main— byte-identical lines in two files:.github/workflows/ci.yml:1010.github/workflows/live-e2e.yml:140The exit status is discarded twice over:
echoowns the step's status, and there is a pipe inside the substitution whose status isjq's. Either failure yields an empty ornullversionand a successful step.The value feeds the next step's cache key:
so a failure degrades it to⚠️
playwright-Linux-orplaywright-Linux-null.jq -rprints the stringnulland exits 0 for a missing field, so a renamed or moved key inpnpm list's JSON produces the-nullbucket with every command in the pipeline reporting success.Consequence, stated at its real size
⛔ This is milder than #6221's unfiltered build and should not be sold as urgent. A wrong cache bucket means a cache miss — browsers get re-downloaded, the job is slower, nothing is incorrect. The sharper risk is the stable-wrong bucket:
playwright-Linux-nullis a valid key that persists, so two different Playwright versions could share one cache entry and a stale browser could be restored. No evidence that has happened.⭐ The scoping rule, measured — this is why the card is filable now
#6221 deliberately did not add a repo-wide scan, on the grounds that a naive
$(sweep would be almost all false positives and a gate whose findings are mostly noise gets ignored. That caution is now a measurement, taken by the #6221 dev across.github/workflows/**:echoappending to$GITHUB_OUTPUTThe third hit is benign:
performance-budget.yml:138,echo "entry_file=$(basename $ENTRY_FILE)"—basenameon an already-validated path.⭐ And the forms that are safe are safe by the repo's own convention, which a gate must not flag:
X=$(cmd)assignment does propagate its status underset -e;if ! CHANGED=$(git diff …)form appears five times (ci.yml×4,lint.yml).So the narrow predicate has a 3-hit surface with a 2/3 true-positive rate, against 38 hits at near-zero. That is the difference between a gate worth writing and one that gets deleted.
Possible dispositions (not chosen here)
set -o pipefailis relevant here — unlike 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 emptyargsand a step that succeeds #6221 there really is a pipe), fail the step on non-zero, and additionally refuse an empty ornullversion before it reaches a cache key.$(sweep.if ! CHANGED=$(git diff …)would be reverted the same day.Bound
⛔ Not a defect in PR #6228 (#6221), which fixed a different and more serious instance and correctly declined to widen. ⛔ No evidence either line has ever failed. Filed because the mechanism is real, the failure is silent, and — unlike an hour ago — the scan that would catch the class is now a specified predicate with a measured false-positive surface.
Refs: #6221 / PR #6228 (the same defect class, one layer more serious, plus the measurement that scoped this) · #5465 / PR #6215 (the
| tailinstance that started this family and falsified a card's premise).