Skip to content

Test App Build Cache fails on every run when both fixture fingerprints are already cached #2034

Description

@thymikee

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 main run 32854978515: total_jobs: 1. Resolve native fingerprint succeeded (all six steps green, "Set output 'matrix'"), no release job was ever created, run conclusion failure.
  • Same shape on every sampled run since.

Why it is worth fixing rather than tolerating

  • main shows 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions