Skip to content

Commit 061e4b0

Browse files
authored
🎞️ ci: Fix Playwright ffmpeg Install Hang and Cache the Download (#15065)
Every Playwright job spent a flat 90s on `npx playwright install ffmpeg`, and none of them ended up with a usable ffmpeg. The 2.3MB download finishes in under a second; extraction then hangs until `timeout -k 10 90` reaps it (exit 124, masked by `continue-on-error`). That is a Node 24.16.0 readable-stream change (nodejs/node#62557) colliding with yauzl/fd-slicer never firing `close` after EOF, which hangs extract-zip. It leaves a truncated `ffmpeg-linux` — 5,055,201 bytes against the zip's declared 5,101,056, segfaulting on exec — and no INSTALLATION_COMPLETE marker, so Playwright treated ffmpeg as uninstalled. `video: 'on-first-retry'` has therefore never worked in CI, and every first retry of a flaky test died in browserContext.newPage: exactly the failure the step existed to prevent. Upstream fixed it in Playwright 1.60.0 (microsoft/playwright#40747) and Node reverted it in 24.18.0 (nodejs/node#63834). Node 24.16.0 is pinned in 17 places including the Dockerfiles, so bump Playwright instead — it is a dev dependency, and `^1.56.1` already permitted 1.62.1; only the lockfile pinned it. Staying at or above 1.62.1 also avoids the tsconfig-resolution regressions in 1.62.0. Caching alone could not have fixed this: a cold cache still hangs, and what would have been cached is the corrupt binary. So the ffmpeg download is now restored from cache keyed on the resolved playwright-core version, the install is skipped outright on a hit, and the cache is only saved once the binary is verified to actually execute — a partial extraction can never be promoted into a cache that every later job restores. Per job: 90s to ~0s on a hit, ~2s on a miss.
1 parent d6d6b04 commit 061e4b0

5 files changed

Lines changed: 153 additions & 23 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Verifies that Playwright's ffmpeg download produced a usable binary.
4+
#
5+
# `playwright install` is not trustworthy on its own here: under the Node 24.16.0
6+
# yauzl/extract-zip regression (Playwright < 1.60.0) it would hang mid-extraction
7+
# and leave a truncated `ffmpeg-linux` behind with no INSTALLATION_COMPLETE marker,
8+
# so the exit code said nothing about whether ffmpeg actually worked.
9+
#
10+
# CI caches the download, so this runs before the cache is saved: checking both the
11+
# marker and that the binary actually executes is what keeps a partial extraction
12+
# from being promoted into a cache that every later job would restore. The install
13+
# directory is read back from Playwright so this stays correct across version bumps
14+
# and never lets a stale revision vouch for the one actually required.
15+
16+
set -uo pipefail
17+
18+
install_dir=$(npx playwright install --dry-run ffmpeg 2>/dev/null |
19+
sed -n 's/^[[:space:]]*Install location:[[:space:]]*//p' | head -1)
20+
21+
if [ -z "${install_dir}" ]; then
22+
echo "::warning::Could not determine Playwright's ffmpeg install location; skipping cache save."
23+
exit 1
24+
fi
25+
26+
if [ ! -f "${install_dir}/INSTALLATION_COMPLETE" ]; then
27+
echo "::warning::${install_dir} has no INSTALLATION_COMPLETE marker; the download did not finish."
28+
exit 1
29+
fi
30+
31+
binary="${install_dir}/ffmpeg-linux"
32+
33+
if [ ! -x "${binary}" ]; then
34+
echo "::warning::${binary} is missing or not executable."
35+
exit 1
36+
fi
37+
38+
if ! "${binary}" -version >/dev/null 2>&1; then
39+
echo "::warning::${binary} is present but does not execute; treating it as a partial extraction."
40+
exit 1
41+
fi
42+
43+
echo "Verified Playwright ffmpeg at ${binary}"

.github/workflows/codegraph-e2e-votes.yml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,37 @@ jobs:
172172
run: google-chrome --version
173173

174174
# ffmpeg for retry video — see the note in playwright-mock.yml.
175-
- name: Install Playwright ffmpeg (best effort)
175+
- name: Resolve Playwright version
176+
id: playwright-version
177+
if: steps.tiers.outputs.count != '0'
178+
run: |
179+
version=$(node -p "require('./package-lock.json').packages['node_modules/playwright-core'].version")
180+
echo "version=${version}" >> "$GITHUB_OUTPUT"
181+
182+
- name: Restore Playwright ffmpeg cache
183+
id: cache-ffmpeg
176184
if: steps.tiers.outputs.count != '0'
185+
uses: actions/cache/restore@v5
186+
with:
187+
path: ~/.cache/ms-playwright
188+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
189+
190+
- name: Install Playwright ffmpeg (best effort)
191+
id: install-ffmpeg
192+
if: steps.tiers.outputs.count != '0' && steps.cache-ffmpeg.outputs.cache-hit != 'true'
177193
timeout-minutes: 3
178194
continue-on-error: true
179-
run: timeout -k 10 90 npx playwright install ffmpeg
195+
run: |
196+
timeout -k 10 60 npx playwright install ffmpeg
197+
.github/scripts/verify-playwright-ffmpeg.sh
198+
199+
- name: Save Playwright ffmpeg cache
200+
if: steps.tiers.outputs.count != '0' && steps.install-ffmpeg.outcome == 'success'
201+
continue-on-error: true
202+
uses: actions/cache/save@v5
203+
with:
204+
path: ~/.cache/ms-playwright
205+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
180206

181207
# Optional fonts only — see the note in playwright-mock.yml.
182208
- name: Install optional Playwright font dependencies (best effort)

.github/workflows/playwright-mock.yml

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,48 @@ jobs:
146146

147147
# `video: 'on-first-retry'` needs ffmpeg; without it the first retry dies in
148148
# browserContext.newPage before the test body runs, so a flaky test loses the
149-
# retry that would have recovered it. The CLI can hang after the download
150-
# finishes on these runners, so bound it and keep it non-fatal — worst case is
151-
# today's behaviour of retrying without video.
149+
# retry that would have recovered it.
150+
#
151+
# This step used to burn its full 90s bound on every job. Playwright's bundled
152+
# extractor hangs on Node 24.16.0 (a yauzl/extract-zip regression fixed in
153+
# Playwright 1.60.0): the 2.3MB download finished in under a second, then
154+
# extraction stalled and the timeout reaped it, leaving a truncated binary and
155+
# no INSTALLATION_COMPLETE marker — so ffmpeg was never actually installed and
156+
# retries never got video. With Playwright bumped past the fix the install
157+
# takes about a second, and a restored cache skips it outright.
158+
#
159+
# The cache is only saved once the binary is verified to run, so a partial
160+
# extraction can never be promoted into a cache every later job restores.
161+
# Kept non-fatal: retry video is a debugging aid, not something CI asserts on.
162+
- name: Resolve Playwright version
163+
id: playwright-version
164+
run: |
165+
version=$(node -p "require('./package-lock.json').packages['node_modules/playwright-core'].version")
166+
echo "version=${version}" >> "$GITHUB_OUTPUT"
167+
168+
- name: Restore Playwright ffmpeg cache
169+
id: cache-ffmpeg
170+
uses: actions/cache/restore@v5
171+
with:
172+
path: ~/.cache/ms-playwright
173+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
174+
152175
- name: Install Playwright ffmpeg (best effort)
176+
id: install-ffmpeg
177+
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
153178
timeout-minutes: 3
154179
continue-on-error: true
155-
run: timeout -k 10 90 npx playwright install ffmpeg
180+
run: |
181+
timeout -k 10 60 npx playwright install ffmpeg
182+
.github/scripts/verify-playwright-ffmpeg.sh
183+
184+
- name: Save Playwright ffmpeg cache
185+
if: steps.install-ffmpeg.outcome == 'success'
186+
continue-on-error: true
187+
uses: actions/cache/save@v5
188+
with:
189+
path: ~/.cache/ms-playwright
190+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
156191

157192
# The runner's Chrome is an apt package, so its real library dependencies are
158193
# already satisfied; all `install-deps` adds here are optional CJK/Thai/Cyrillic
@@ -246,10 +281,35 @@ jobs:
246281
run: google-chrome --version
247282

248283
# ffmpeg for retry video — see the note in the e2e_shards job.
284+
- name: Resolve Playwright version
285+
id: playwright-version
286+
run: |
287+
version=$(node -p "require('./package-lock.json').packages['node_modules/playwright-core'].version")
288+
echo "version=${version}" >> "$GITHUB_OUTPUT"
289+
290+
- name: Restore Playwright ffmpeg cache
291+
id: cache-ffmpeg
292+
uses: actions/cache/restore@v5
293+
with:
294+
path: ~/.cache/ms-playwright
295+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
296+
249297
- name: Install Playwright ffmpeg (best effort)
298+
id: install-ffmpeg
299+
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
250300
timeout-minutes: 3
251301
continue-on-error: true
252-
run: timeout -k 10 90 npx playwright install ffmpeg
302+
run: |
303+
timeout -k 10 60 npx playwright install ffmpeg
304+
.github/scripts/verify-playwright-ffmpeg.sh
305+
306+
- name: Save Playwright ffmpeg cache
307+
if: steps.install-ffmpeg.outcome == 'success'
308+
continue-on-error: true
309+
uses: actions/cache/save@v5
310+
with:
311+
path: ~/.cache/ms-playwright
312+
key: playwright-ffmpeg-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
253313

254314
# This job deliberately skips the optional font install: its bounded
255315
# Playwright apt process can outlive the wrapper on a slow mirror and

package-lock.json

Lines changed: 16 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
"@eslint/compat": "^1.2.6",
153153
"@eslint/eslintrc": "^3.3.4",
154154
"@eslint/js": "^9.20.0",
155-
"@playwright/test": "^1.56.1",
155+
"@playwright/test": "^1.62.1",
156156
"@types/react-virtualized": "^9.22.0",
157157
"brace-expansion": "^2.1.2",
158158
"caniuse-lite": "^1.0.30001741",

0 commit comments

Comments
 (0)