Repository navigation
Test App Build Cache fails on every run when both fixture fingerprints are already cached #2034
Description
Test App Build Cache has failed on every run since 2026-08-24 14:42 UTC — 60+ consecutive failures across all branches, main included. It is red right now on main.
Root cause
#1996 changed how the release matrix is built:
- '{ include: [ {...iOS...}, {...Android...} ] }')"
+ '[ {...iOS...}, {...Android...} ] | { include: map(select(.build)) }')"That PR's own description states the intent: "The fixture release matrix filters to entries that will actually build, so a cached-fingerprint PR starts zero release runners." Saving those runners is right — but when both artifacts are already cached, map(select(.build)) yields include: [], and GitHub Actions rejects an empty matrix at the workflow level. The release job is never created and the whole run is marked failure.
Both-cached is the steady state, which is why this is not intermittent: it fails except when a fingerprint actually changed.
Before #1996 the matrix always carried both entries and the per-step if: matrix.build guards made a cached entry a no-op — which is also why runsOn: (if $iosBuild then "macos-26" else "ubuntu-latest" end) exists, so a non-building entry only costs a cheap ubuntu runner.
Evidence
- Last success:
32740236844, 2026-08-24 14:41:15 UTC. ci: consolidate CI workflow from 15 jobs to 8 #1996 merged 14:42:43 UTC. Every run after it has failed. - On the
mainrun32854978515:total_jobs: 1.Resolve native fingerprintsucceeded (all six steps green, "Set output 'matrix'"), noreleasejob was ever created, run conclusionfailure. - Same shape on every sampled run since.
Why it is worth fixing rather than tolerating
mainshows a permanently failing workflow, so a genuine fixture-build break is indistinguishable from the current noise.- The producer never runs, so the artifact for a new fingerprint is only ever built by a consumer's inline fallback — the cost this cache exists to avoid.
- The workflow header says it "Runs on every push to main and every PR so a same-head consumer can wait for a scheduled producer". A consumer waiting on a producer that errored out gets nothing to wait for.
Suggested fix
Keep #1996's saving; just don't hand Actions an empty matrix. Have fingerprint publish whether there is any work, and gate the job:
outputs:
matrix: ${{ steps.fingerprint.outputs.matrix }}
has-work: ${{ steps.fingerprint.outputs.has-work }} echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "has-work=$(jq -r '.include | length > 0' <<<"$MATRIX")" >> "$GITHUB_OUTPUT" release:
needs: fingerprint
if: needs.fingerprint.outputs.has-work == 'true'Alternatives, both worse: revert to always-include (gives up the runner saving #1996 bought), or emit a sentinel matrix entry (adds a state every step then has to guard against).
Worth checking as part of this: whether a skipped release still satisfies anything downstream that needs: it, so the fix doesn't just move the red.
Regression coverage
test/ci/ already tests workflow behaviour (size-workflow.test.ts, trusted-fixture-artifact.test.mjs). The valuable assertion is over the jq expression itself for all four fingerprint combinations — both-cached, iOS-only, Android-only, neither — pinning that the both-cached case produces something Actions accepts. That is the case that shipped broken and the one no existing test covers.
Verification
After the fix, a push to main with no native change should show Test App Build Cache green with release skipped, not errored. A run where a fingerprint did change should still build and publish.
Found while triaging CI on #2021. This is unrelated to that PR — reporting it because it is red on main for everyone. I also checked whether it explained the intermittent iOS fill / TEXT_INPUT_COMMIT_NOT_OBSERVED smoke flake that started 2026-08-25 ~12:00 and ruled that out: this workflow has been red since 24 Aug, including hours when iOS smoke was passing. The two are independent.
Activity
fix(ci): skip release instead of erroring when both fixtures are cach…