Skip to content

chore(ci): add timeout and retry to Playwright browser installs - #1259

Merged
Wikid82 merged 4 commits into
mainfrom
fix/e2e-playwright-install-retry
Aug 18, 2026
Merged

chore(ci): add timeout and retry to Playwright browser installs#1259
Wikid82 merged 4 commits into
mainfrom
fix/e2e-playwright-install-retry

Conversation

@Wikid82

@Wikid82 Wikid82 commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • apt-get inside npx playwright install --with-deps <browser> can silently hang against a flaky Azure apt mirror with no timeout of its own, burning the job's full 60-minute timeout-minutes and surfacing as a misleading cancelled run instead of a clear, actionable failure (observed on run 32068948141E2E Chromium (Security Enforcement), triggered on release-please's PR chore(main): release 0.37.0 #1258 against main).
  • Wraps all 10 Playwright browser install steps in e2e-tests-split.yml with a bash timeout-based retry loop (10 min/attempt, 3 attempts, 15s backoff) — an earlier attempt used nick-fields/retry, but that action crashes with Error: kill EPERM when it tries to kill a process tree that playwright install --with-deps has partially elevated to root via sudo, so it never actually retried. The bash timeout command signals its own direct child and avoids that cross-UID kill problem entirely.
  • Job-level timeout-minutes: 60 and the concurrency: cancel-in-progress: true block are left untouched — those are correct as-is.
  • Fixes a real, systemic accessibility defect in frontend/src/components/ui/Input.tsx: its <label htmlFor> was never programmatically associated with its <input> unless the caller passed an explicit id — 115 of 118 call sites app-wide never do. Input.tsx now auto-generates a stable id via React.useId() when one isn't supplied. Note: generated ids contain colons (e.g. :r0:), so a naive page.locator('#' + id) against one of these auto-generated ids won't work as a raw CSS selector — no current call site does this, but worth knowing.
  • Fixes the actual root cause of the "E2E Firefox (Shard 4/4)" CI failure on tests/settings/user-lifecycle.spec.ts (this is the seventh fix attempt at this exact test's Firefox flakiness — see commit message for full trace-level RCA): a 2026-07-28 refactor (7503c01a) silently downgraded gotoTolerant/reloadTolerant (tests/utils/wait-helpers.ts) from waitUntil: 'domcontentloaded' to waitUntil: 'commit'. Firefox can fail to emit a trackable commit event at all, which leaves Playwright's frame-navigation tracker believing a navigation is still pending — so the next locator-based assertion blocks for a second full timeout without ever querying the DOM, even though the page had already rendered correctly. Restored to domcontentloaded, the value proven correct by the original RCA two days earlier and already used by 100+ other call sites in this suite.

Test plan

  • YAML validated, actionlint clean, lefthook run pre-commit passes (incl. semgrep, 0 findings)
  • e2e-tests-split.yml has an unfiltered pull_request: trigger, so this PR runs the full "E2E Tests" workflow directly
  • Input.tsx: 2 new unit tests (label associates via auto-id; explicit id still respected), all 18 existing Input.test.tsx cases pass, npm run type-check clean, npm run build clean
  • wait-helpers.ts: existing 30-case real-browser wait-helpers.spec.ts suite passes unmodified under Firefox
  • tests/settings/user-lifecycle.spec.ts --project=firefox --repeat-each=3 — 3/3 clean passes, zero flakes (was 3/3 failures before)
  • tests/settings/navigation-settle-regression.spec.ts --project=firefox — passes
  • Blast-radius check: tests/settings/user-management.spec.ts (full file, 32 passed/1 pre-existing skip) and tests/tasks/long-running-operations.spec.ts (grep-scoped) — both pass, zero regressions
  • Frontend coverage gate: scripts/frontend-test-coverage.sh → PASS (90.81% lines vs 87% minimum)
  • scripts/local-patch-report.sh → 100% patch coverage, all scopes pass
  • cd backend && go build ./... clean (unaffected, gated per DoD anyway)

…event job-timeout hangs

apt-get inside `playwright install --with-deps` can silently hang against
a flaky Azure apt mirror with no timeout of its own, burning the full
60-minute job timeout and surfacing as a misleading cancelled run instead
of a clear failure. Wrap all 10 Playwright browser install steps in
nick-fields/retry (10min/attempt, 3 attempts, 15s backoff), matching the
existing pattern already used in docker-build.yml.
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

✅ Supply Chain Verification Results

PASSED

📦 SBOM Summary

  • Components: 1753

🔍 Vulnerability Scan

Severity Count
🔴 Critical 0
🟠 High 0
🟡 Medium 5
🟢 Low 2
Total 11

📎 Artifacts

  • SBOM (CycloneDX JSON) and Grype results available in workflow artifacts

Generated by Supply Chain Verification workflow • View Details

…right installs

nick-fields/retry's timeout handling calls process.kill() on the child
process tree from its own Node process. `playwright install --with-deps`
internally elevates part of that tree to root via sudo to run apt-get, so
when the action's 10-minute timeout fires, the kill fails with EPERM
(cross-UID kill) and throws unhandled instead of retrying. The step died
on attempt 1 and never reached attempt 2 or 3.

Replaced all 10 occurrences with a plain bash retry loop using coreutils
`timeout`, which signals its own direct child and isn't subject to the
EPERM bug class. Matches the existing bash retry-loop idiom already used
in codecov-upload.yml and quality-checks.yml.
…nerated id

frontend/src/components/ui/Input.tsx rendered <label htmlFor={props.id}>
next to <input {...props} />, but 115 of 118 call sites never pass an id
prop, so the label was never programmatically associated with its
control (WCAG 1.3.1/3.3.2). Browsers fall back to the placeholder text
as the accessible name instead, which also broke Playwright's
getByLabel() matching against these fields.

Auto-generate a stable id via React.useId() when the caller doesn't
supply one, so every <Input label=.../> usage gets a properly
associated label/control pair without changing behavior for call sites
that already pass an explicit id.
…07-28 refactor

gotoTolerant/reloadTolerant in tests/utils/wait-helpers.ts used
waitUntil: 'commit', silently downgraded from waitUntil: 'domcontentloaded'
during a 2026-07-28 refactor (7503c01) that extracted these helpers from
navigateToLogin()'s previously RCA-verified-working inline implementation
(d537476). 'commit' fires on Firefox's earliest possible navigation
signal, which Firefox can fail to emit at all for some goto()/reload()
calls -- leaving Playwright's frame-navigation tracker believing a
navigation is still pending and blocking the *next* locator-based wait
on the page for a second full timeout, even though the page had already
rendered correctly. Five subsequent fix attempts patched around this
symptom without tracing it back to the regression.

Trace-level evidence (docs/plans/current_spec.md) confirms this exact
mechanism caused PR #1259's "E2E Firefox (Shard 4/4)" job to fail on
tests/settings/user-lifecycle.spec.ts STEP 4: reloadTolerant() burned
its full 15s timeout with a swallowed TimeoutError, then the following
expect(emailInput).toBeVisible() burned another full 15s blocked on the
phantom pending navigation without ever querying the DOM.

Restores 'domcontentloaded', the value proven correct by the original
2026-07-26 RCA and already used by 100+ other call sites in this suite.
Verified with 3x repeat-each on the target spec (zero flakes, was 3/3
failures before) plus blast-radius checks on every other consumer of
these helpers (user-management.spec.ts full file, long-running-operations.spec.ts
grep-scoped) -- all pass.
@Wikid82
Wikid82 merged commit fe3d58d into main Aug 18, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants