Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,9 @@ jobs:
# per-shard names need no fan-in job.
web-integration:
name: web-integration (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
# Pinned, not ubuntu-latest: Playwright 1.60 refuses ubuntu26.04 (nr-7sn1). Move it together
# with a Playwright that supports the new image.
runs-on: ubuntu-24.04
# The tests take about seven minutes on a shard (nr-y7yu, one worker). The limit was raised to 30 when installing
# Playwright's system packages from a slow apt mirror took up to eleven; that step no longer
# touches apt unless a library is missing (nr-2e2l).
Expand Down Expand Up @@ -439,7 +441,7 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ hashFiles('pnpm-lock.yaml') }}
key: playwright-chromium-ubuntu-24.04-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
Expand Down Expand Up @@ -488,7 +490,9 @@ jobs:
retention-days: 7

web-container:
runs-on: ubuntu-latest
# Pinned, not ubuntu-latest: Playwright 1.60 refuses ubuntu26.04 (nr-7sn1). Move it together
# with a Playwright that supports the new image.
runs-on: ubuntu-24.04
timeout-minutes: 25
needs: [changes, python-tests, rust-test-build, rust-test-run, rust-lint, wasm, web, web-integration]
if: >-
Expand Down
11 changes: 11 additions & 0 deletions docs/retrospectives/nr-7sn1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# nr-7sn1: ubuntu-latest moved to 26.04 under us, and a cache hid it

**What happened:** The web-container job failed on PR #3395 with "Playwright does not support chromium on ubuntu26.04-x64". It failed before any test ran, and with nothing in the repository having changed. GitHub had moved `ubuntu-latest` to Ubuntu 26.04, and our lockfile's Playwright (1.60.0) refuses that image. web-integration ran on the same image and stayed green, but only because its Chromium came from an Actions cache keyed on the lockfile alone, so its install step never ran. That was luck, not health.

**Why:** Jobs that download a host-specific browser followed a floating runner label. A cache keyed without the image skipped the one step that checks host support.

**Cost:** Every pull request that ran web-container was blocked (PR #3395 failed twice; nr-ri8n and nr-1wbx queued behind it). One P0 bug pass.

**Prevent by:** `scripts/test_playwright_runner_image.py` requires every job that runs `playwright install` to name `ubuntu-24.04`, and every Playwright cache key to carry that image. Moving the image is therefore a deliberate change, made together with a Playwright upgrade (1.64.1 is the first release seen to list ubuntu26.04).

**Seen before:** No.
58 changes: 58 additions & 0 deletions scripts/test_playwright_runner_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Every CI job that installs Playwright's Chromium runs on a pinned Ubuntu image (nr-7sn1).

``ubuntu-latest`` moved to Ubuntu 26.04 in October 2026, and the Playwright the lockfile carries
(1.60) refuses it: ``playwright install chromium`` failed with "Playwright does not support
chromium on ubuntu26.04-x64" before any test ran, and web-container blocked every pull request.
A job that installs the browser therefore names the image it was proven on rather than following
``ubuntu-latest``; moving it is a deliberate change made together with a Playwright that supports
the new image. The workflow is read as text, as ``test_web_integration_ci.py`` does.
"""

import re
import unittest
from pathlib import Path

CI_YML = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "ci.yml"
PINNED_IMAGE = "ubuntu-24.04"


def jobs() -> dict[str, str]:
"""Each job of ci.yml by name, as the text of its body."""

ci = CI_YML.read_text(encoding="utf-8")
jobs_text = ci.split("\njobs:\n", 1)[1]
return {
match.group(1): match.group(2)
for match in re.finditer(r"^ ([\w-]+):\n((?:(?: {4}.*)?\n)+)", jobs_text, re.MULTILINE)
}


class PlaywrightRunnerImageTest(unittest.TestCase):
def test_jobs_installing_playwright_are_found(self) -> None:
installing = [name for name, body in jobs().items() if "playwright install" in body]
self.assertEqual(sorted(installing), ["web-container", "web-integration"])

def test_jobs_installing_playwright_pin_their_image(self) -> None:
for name, body in jobs().items():
if "playwright install" not in body:
continue
with self.subTest(job=name):
runs_on = re.search(r"^ runs-on: (.+)$", body, re.MULTILINE)
assert runs_on is not None, f"{name} has no runs-on"
self.assertEqual(runs_on.group(1), PINNED_IMAGE)

def test_cached_browsers_are_keyed_by_the_pinned_image(self) -> None:
"""A cached Chromium skips the install, the one step that checks the image (review of #3396).

The cache once hid this very bug in web-integration, so moving the pin must miss the cache.
"""

keys = [(name, key) for name, body in jobs().items() for key in re.findall(r"key: (playwright-\S+)", body)]
self.assertTrue(keys, "no Playwright browser cache found in ci.yml")
for name, key in keys:
with self.subTest(job=name):
self.assertIn(PINNED_IMAGE, key)


if __name__ == "__main__":
unittest.main()
Loading