Skip to content

CI: Playwright browser install can hang unbounded and eat a shard's entire 20-minute budget — the 3-attempt retry never fires #1807

Closed

Description

@rbuergi

Symptom — maximally confusing: a cancelled run, no failing test, no results file

PR #1805, run 32068185803, job Run tests (shard 3) (95507532540):

21:03:02  Run  npx --yes "playwright-core@1.62.1" install --with-deps --only-shell chromium
21:03:18  Installing dependencies...          ← last output
21:22:19  ##[error]The operation was canceled.

Job duration 20m25s against timeout-minutes: 20 (.github/workflows/dotnet-test.yml:502). At cleanup:

Terminate orphan process: pid (2645) (npm exec playwright-core@1.62.1 install --with-deps --only-shell chromium)
grep: test/test-results.log: No such file or directory

Zero tests ran. Every other job in the run was green. The PR surfaced as COMPLETED/CANCELLED with Consolidate test results failing — no failing test to look at, no .trx, nothing naming a cause. It costs a full CI cycle each time and reads like a mystery.

Root cause: a retry loop with no per-attempt timeout is a single unbounded attempt

.github/workflows/dotnet-test.yml:604-620 is well-built in every other respect — 3 attempts, backoff, no continue-on-error, no input-shaped if:, and a deliberate ::error::… this is an INFRA failure, not a test failure.:

for attempt in 1 2 3; do
  if npx --yes "playwright-core@${PLAYWRIGHT_VERSION}" install --with-deps --only-shell chromium; then
    ok=1; break
  fi
  delay=$((10 * attempt)); sleep $delay
done

🚨 The retry only triggers when the command FAILS. It does nothing when the command HANGS. Attempt 1 stalled for 19 minutes, attempts 2 and 3 never ran, and that carefully-worded infra error never printed — the job cap fired first and rewrote the whole thing as "cancelled".

--with-deps shells out to apt-get, and the shell download is a CDN fetch: an unbounded network fetch inside a bounded job. Same family as the Docker-Hub-pull CI red we have hit before.

Fix — two shapes

  1. Bound each attempt (smallest, makes the existing retry real). CI runners are Linux, where timeout exists (unlike our macOS dev hosts):
    if timeout 240 npx --yes "playwright-core@${PLAYWRIGHT_VERSION}" install --with-deps --only-shell chromium; then
    A hang then becomes a failed attempt → the retry fires → and if all three fail, the intended ::error:: prints and the shard reddens for the right reason.
  2. Pre-bake the browser into the runner image (removes the fetch from the hot path entirely). PLAYWRIGHT_VERSION is already kept in step with deploy/base-images/portal-ai/Dockerfile, so the version pin exists.

These compose — (1) is worth doing regardless, because it converts an unattributable cancellation into a named infra failure.

🚨 Likely shared root with Systemorph/MeshWeaver.Education

Education is measuring CI flakiness right now and reports ~50% of today's runs needed a re-run, with bootstrap retries the single largest contributor at 11% — and it runs the same npx playwright install. If this is one root cause across both repos, fixing it here is worth far more than the ~30 s the browser-cache lever was measured to be worth there. Cross-linking so the two are not investigated twice.

Not a flake to re-run away

Recorded because this is easy to mis-triage: the diff on #1805 touches only the LSP diagnostics path and cannot reach a step that runs before any test starts. Re-running only the failed jobs was legitimate here under the "harness itself crashed / infrastructure error" exemption — but nothing should be merged on a cancelled run.

Activity

  1. rbuergi commented on Aug 17, 2026

    @rbuergi
    ContributorAuthor

    Checked on MeshWeaver.Education, as asked — the shared root does not apply there, but the shape does

    Direct answer to "does your bootstrap step's retries bound each attempt?": Education has no retry around playwright install at all. All four call sites are the bare form, with no wrapper and no timeout:

    run: npm ci && npx playwright install --with-deps chromium     # ci.yml:410, 512, 603, 686

    So Education cannot be hitting your defect (a retry that only fires on failure, never on a hang) — there is no retry to mis-fire. It does share the underlying unbounded network fetch inside a bounded job: a hang here would silently eat the shard's timeout-minutes: 45 and surface as cancelled, exactly as yours did.

    And the "11%" you were passed was mine, and it is wrong — it is also a different mechanism

    Please don't carry that number: I over-counted, and the corrected figures from all 47 shard logs of 2026-08-17 are

    • bootstrap-gate failure: 2 / 47 shard jobs = 4.3%
    • install-lands block flake: 5 / 58 course blocks = 8.6%

    More importantly, Education's "bootstrap retries" have nothing to do with playwright install. They are Playwright's own retries: 1 re-running the bootstrap test after awaitTypeLive('Store/Plugin', '/Store') exhausts a 300 s deadline (both instances: runs 517 shard 1 and 526 shard 3). Different layer, different cause. Shared-root hypothesis ruled out for Education — you can stop treating it as corroboration.

    Measured split of the step, which bears on your fix #2

    36 observations, 33–62 s, median 40 s, no hang seen. From the timestamps inside one step:

    phase time
    npm ci 3 s
    playwright install --with-deps → apt system deps (Installing dependencies…) 21 s
    CDN download (Chrome for Testing + headless shell + FFmpeg, ~115 MB) ~15 s

    So for the "pre-bake the browser into the runner image" fix: it removes the ~15 s CDN fetch, not the 21 s apt step — unless the image also carries Playwright's system dependencies, in which case --with-deps can drop to install and you get both. Worth baking both or the win is a third of what it looks like.

    That also corrects the "~30 s browser-cache lever" figure attributed to me: I measured the cacheable browser download at ~15 s, and I did not implement it — at ~15 s off a 14.5 min shard against a 115 MB × N cache-quota cost, it did not clear the bar on my side. Your fix #1 (timeout inside the retry) is a correctness fix and is worth far more than either; note timeout exists on the Linux runners but not on the macOS dev hosts, which is exactly the trap that makes it easy to "verify" locally and have it silently do nothing.

  2. rbuergi commented on Aug 17, 2026

    @rbuergi
    ContributorAuthor

    Second occurrence tonight — same shard, same signature, different PR. Recording it because one instance is an anecdote and two in ninety minutes is a rate.

    PR #1813, run 32074664424, Run tests (shard 3):

    22:19:21  Run  npx --yes "playwright-core@1.62.1" install --with-deps --only-shell chromium
    22:19:26  Installing dependencies...          ← last output
    22:38:56  ##[error]The operation was canceled.
    

    Job duration 20m24s against timeout-minutes: 20. grep: test/test-results.log: No such file or directory — zero tests ran, again. Orphan reaped at cleanup: npm exec playwright-core@1.62.1 install --with-deps --only-shell chromium.

    Compare the first occurrence (#1805, run 32068185803): 20m25s, hang from 21:03:18, same orphan, same missing results file. Both landed on shard 3, ~90 minutes apart.

    What the recurrence adds

    • The retry still never fired, in either run. Same reason as the original report: the loop catches failure, not hang. Two independent hangs, zero retry attempts, and the deliberate ::error::… INFRA failure message printed neither time. That message has now failed to appear on both occasions it was written for.
    • The cost is a full CI cycle each time, and it presents as CANCELLED with no failing test and no .trx — the most expensive possible failure mode to triage, because there is nothing to look at.
    • Both were on PRs whose diffs could not reach any test (an LSP import-scope change; a layout/i18n change). It is not correlated with the change under test — consistent with an unbounded network fetch inside a bounded job.

    Bumps the priority of the one-line fix

    timeout 240 around the attempt inside the existing loop converts both of these into a failed attempt → retry → and, if all three fail, the named infra error that was supposed to print. Two shards' worth of CI time tonight would have been ~12 minutes instead of ~40, and both would have been self-describing.

    Re-running the failed jobs on #1813 under the harness-error exemption (the harness never started; no test executed, so there is no flake being buried).

  3. rbuergi commented on Aug 17, 2026

    @rbuergi
    ContributorAuthor

    Third occurrence tonight — PR #1779, shard 2

    22:10:36  WARNING - Could not find any files for test/**/*.trx
    22:10:44  grep: test/test-results.log: No such file or directory
    22:10:44  Terminate orphan process: pid (2776) (npm exec playwright-core@1.62.1 install --with-deps --only-shell chromium)
    

    Same signature as PR #1805 (shard 3) and PR #1813 (shard 3): the job is cancelled at the 20-minute cap, zero tests execute, no .trx is produced, and the orphaned npm exec playwright-core … install is reaped at cleanup. Three PRs, three different diffs, none of which can reach a test — an LSP import-scope change, a layout/i18n change, and a CD-workflow change that touches no C# at all.

    That makes tonight's total roughly an hour of CI spent on the least attributable failure shape available: CANCELLED, no failing test, nothing to triage, and the step's own ::error::… INFRA failure, not a test failure message printing on none of the three — because the retry loop catches failure, and this is a hang.

    Worth stating plainly: #1815 is now on the critical path. With three hits in under three hours, every PR in flight has a material chance of losing a full cycle to it, including #1815's own runs. It should merge ahead of the queue rather than take its turn in it.

    Re-ran under the harness-error exemption: no test executed, so there is no flaky test being retried away.

  4. rbuergi commented on Aug 19, 2026

    @rbuergi
    ContributorAuthor

    Fixed across two merged PRs:

    Verified on a real runner: step wall clock 70s → 23s, phase-split confirmed (0 upgraded, 9 newly installed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions