Skip to content

chore(ci): Use Playwright Docker images instead of install-playwright action#20270

Draft
mydea wants to merge 53 commits intodevelopfrom
fn/playwright-docker-images
Draft

chore(ci): Use Playwright Docker images instead of install-playwright action#20270
mydea wants to merge 53 commits intodevelopfrom
fn/playwright-docker-images

Conversation

@mydea
Copy link
Copy Markdown
Member

@mydea mydea commented Apr 14, 2026

Summary

  • Delete the custom install-playwright composite action
  • Build a custom Docker image (official Playwright + yarn) and cache in GHCR
  • Use the GHCR image as a container for all jobs that need Playwright
  • Verify @playwright/test version consistency across the entire repo

Architecture

.github/docker/playwright.Dockerfile                    → extends official Playwright image with yarn
.github/actions/ensure-playwright-image/action.yml      → composite action: check GHCR, build if missing, verify versions

Each workflow has a lightweight job_playwright_image prerequisite job that:

  1. Verifies all @playwright/test versions match across packages and e2e test apps
  2. Checks if ghcr.io/getsentry/sentry-javascript/playwright:v<version> already exists
  3. If the image exists → noop (takes ~5s)
  4. If missing → builds and pushes (first run after a Playwright bump)

Downstream jobs reference the image via needs.job_playwright_image.outputs.image.

Jobs updated

Job Before After
Browser Playwright tests install-playwright composite action GHCR container
Browser Loader tests install-playwright composite action GHCR container
Remix integration tests install-playwright composite action GHCR container
E2E tests (required) install-playwright composite action GHCR container
E2E tests (optional) install-playwright composite action GHCR container
Canary E2E install-playwright composite action GHCR container
Flaky test detector install-playwright composite action GHCR container

Benefits

  • No more downloading/caching Playwright browsers per job — they're pre-installed in the image
  • Removes actions/cache@v4 usage from the deleted composite action (Node.js 20 deprecation warning)
  • Single Dockerfile to customize if extra tools are needed
  • Version consistency enforced — mismatched @playwright/test versions fail CI with clear error annotations
  • No hardcoded image tags in workflow files — the version is derived from package.json automatically
  • After the image is built once, subsequent runs are a noop (~5s docker manifest inspect)

Maintenance

When bumping @playwright/test, the ensure-playwright-image action automatically detects the new version and builds a new image on the first CI run. No manual workflow file updates needed.

Test plan

  • job_playwright_image is a noop when the image already exists
  • Image is built and pushed when it doesn't exist
  • Version consistency check catches mismatched versions
  • Browser Playwright tests pass with GHCR container
  • Browser Loader tests pass with GHCR container
  • Remix integration tests pass with GHCR container
  • E2E tests pass with GHCR container
  • Canary workflow still works
  • Flaky test detector still works

🤖 Generated with Claude Code

@mydea mydea marked this pull request as draft April 14, 2026 08:12
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Cloudflare

  • Split alarms into multiple traces and link them by JPeer264 in #19373
  • Propagate traceparent to RPC calls - via fetch by JPeer264 in #19991

Core

  • Automatically disable truncation when span streaming is enabled in LangGraph integration by andreiborza in #20231
  • Automatically disable truncation when span streaming is enabled in LangChain integration by andreiborza in #20230
  • Automatically disable truncation when span streaming is enabled in Google GenAI integration by andreiborza in #20229
  • Automatically disable truncation when span streaming is enabled in Anthropic AI integration by andreiborza in #20228
  • Automatically disable truncation when span streaming is enabled in Vercel AI integration by andreiborza in #20232
  • Automatically disable truncation when span streaming is enabled in OpenAI integration by andreiborza in #20227
  • Add enableTruncation option to Vercel AI integration by nicohrubec in #20195
  • Add enableTruncation option to Google GenAI integration by andreiborza in #20184
  • Add enableTruncation option to Anthropic AI integration by andreiborza in #20181
  • Add enableTruncation option to LangGraph integration by andreiborza in #20183
  • Add enableTruncation option to LangChain integration by andreiborza in #20182
  • Add enableTruncation option to OpenAI integration by andreiborza in #20167
  • Export a reusable function to add tracing headers by JPeer264 in #20076

Deps

  • Bump axios from 1.13.5 to 1.15.0 by dependabot in #20180
  • Bump hono from 4.12.7 to 4.12.12 by dependabot in #20118
  • Bump defu from 6.1.4 to 6.1.6 by dependabot in #20104

Other

  • (browser) Add View Hierarchy integration by timfish in #14981
  • (cloudflare,deno,vercel-edge) Add span streaming support by Lms24 in #20127
  • (node) Include global scope for eventLoopBlockIntegration by timfish in #20108
  • (node-native) Add support for V8 v14 (Node v25+) by timfish in #20125
  • (opentelemetry) Vendor AsyncLocalStorageContextManager by mydea in #20243

Bug Fixes 🐛

Deno

  • Handle reader.closed rejection from releaseLock() in streaming by andreiborza in #20187
  • Avoid inferring invalid span op from Deno tracer by Lms24 in #20128

Other

  • (ci) Prevent command injection in ci-metadata workflow by fix-it-felix-sentry in #19899
  • (core, node) Support loading Express options lazily by isaacs in #20211
  • (e2e) Add op check to waitForTransaction in React Router e2e tests by copilot-swe-agent in #20193
  • (e2e-tests) Remove flaky navigation breadcrumb assertions from parameterized-routes tests by copilot-swe-agent in #20202
  • (node-integration-tests) Fix flaky kafkajs test race condition by copilot-swe-agent in #20189

Internal Changes 🔧

Ci

  • Use Playwright Docker images instead of install-playwright action by mydea in #20270
  • Remove node-overhead GitHub Action by mydea in #20246
  • Bump dorny/paths-filter from v3.0.1 to v4.0.1 by mydea in #20251
  • Remove codecov steps from jobs that produce no coverage/JUnit data by mydea in #20244

Deps

  • Bump hono from 4.12.7 to 4.12.12 in /dev-packages/e2e-tests/test-applications/cloudflare-hono by dependabot in #20119
  • Bump axios from 1.13.5 to 1.15.0 in /dev-packages/e2e-tests/test-applications/nestjs-basic by dependabot in #20179

Deps Dev

  • Bump @sveltejs/kit from 2.53.3 to 2.57.1 by dependabot in #20216
  • Bump vite from 7.2.0 to 7.3.2 in /dev-packages/e2e-tests/test-applications/tanstackstart-react by dependabot in #20107

Other

  • (bugbot) Add rules to flag test-flake-provoking patterns by Lms24 in #20192
  • (react) Remove duplicated test mock by s1gr1d in #20200
  • (size-limit) Bump failing size limit scenario by Lms24 in #20186
  • Add PR review reminder workflow by copilot-swe-agent in #20175
  • Fix lint warnings by mydea in #20250
  • Fix flaky ANR test by increasing blocking duration by JPeer264 in #20239
  • Add automatic flaky test detector by nicohrubec in #18684

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.78 kB - -
@sentry/browser - with treeshaking flags 24.27 kB - -
@sentry/browser (incl. Tracing) 43.61 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 45.32 kB - -
@sentry/browser (incl. Tracing, Profiling) 48.51 kB - -
@sentry/browser (incl. Tracing, Replay) 82.74 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.25 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 87.43 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 99.67 kB - -
@sentry/browser (incl. Feedback) 42.59 kB - -
@sentry/browser (incl. sendFeedback) 30.45 kB - -
@sentry/browser (incl. FeedbackAsync) 35.45 kB - -
@sentry/browser (incl. Metrics) 27.07 kB - -
@sentry/browser (incl. Logs) 27.2 kB - -
@sentry/browser (incl. Metrics & Logs) 27.89 kB - -
@sentry/react 27.53 kB - -
@sentry/react (incl. Tracing) 45.88 kB - -
@sentry/vue 30.61 kB - -
@sentry/vue (incl. Tracing) 45.45 kB - -
@sentry/svelte 25.8 kB - -
CDN Bundle 28.46 kB - -
CDN Bundle (incl. Tracing) 44.69 kB - -
CDN Bundle (incl. Logs, Metrics) 29.83 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 45.78 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.73 kB - -
CDN Bundle (incl. Tracing, Replay) 81.65 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 82.73 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 87.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 88.23 kB - -
CDN Bundle - uncompressed 83.12 kB - -
CDN Bundle (incl. Tracing) - uncompressed 133.64 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.27 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 137.05 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.63 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 250.87 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 254.27 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 263.78 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 267.17 kB - -
@sentry/nextjs (client) 48.42 kB - -
@sentry/sveltekit (client) 44.06 kB - -
@sentry/node-core 57.94 kB +0.02% +7 B 🔺
@sentry/node 174.77 kB -0.01% -9 B 🔽
@sentry/node - without tracing 97.89 kB +0.03% +21 B 🔺
@sentry/aws-serverless 115.12 kB +0.01% +8 B 🔺

View base workflow run

@mydea mydea self-assigned this Apr 15, 2026
@mydea mydea force-pushed the fn/playwright-docker-images branch 2 times, most recently from f051caa to 7696d34 Compare April 15, 2026 13:55
mydea and others added 22 commits April 16, 2026 10:05
… action

Replace the custom `install-playwright` composite action with official
Playwright Docker images for browser integration tests, and inline
`npx playwright install` for other jobs.

**Browser integration tests** (playwright + loader):
- Use `mcr.microsoft.com/playwright:v1.56.1-noble` container image
- Browsers are pre-installed, no download or caching needed

**Remix, E2E, canary, flaky-test-detector**:
- Replace composite action with `npx playwright install --with-deps chromium`
- These jobs have complex setups (Node version matrix, pnpm, Verdaccio)
  where a container adds unnecessary complexity

This also removes the `actions/cache@v4` usage from the composite action,
eliminating another source of Node.js 20 deprecation warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build a custom Docker image extending the official Playwright image with
yarn pre-installed, push it to GHCR, and use it across all CI jobs that
need Playwright browsers.

- Add `.github/docker/playwright.Dockerfile` with yarn@1.22.22
- Add build workflow that pushes to GHCR on Dockerfile or Playwright
  version changes
- All Playwright jobs (browser tests, loader tests, Remix, E2E, canary,
  flaky-test-detector) now use the GHCR container image
- No more `npx playwright install` or browser caching logic anywhere

When bumping @playwright/test, also update the PLAYWRIGHT_IMAGE env var
in build.yml, canary.yml, and flaky-test-detector.yml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Trigger the build workflow on PRs that change the Dockerfile, Playwright
  version, or any e2e test application package.json
- Add a version consistency check that verifies all packages and e2e test
  applications use the same @playwright/test version as the canonical
  source (dev-packages/browser-integration-tests)
- On PRs, the image is built (to verify the Dockerfile) but not pushed
- On push to develop and workflow_dispatch, the image is built and pushed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tandalone workflow

Replace the standalone build-playwright-docker-image workflow with a
composite action that runs as a prerequisite job in each workflow:

- Verifies all @playwright/test versions are consistent across the repo
- Checks if the GHCR image already exists (noop if so)
- Builds and pushes only when the image is missing

Each workflow (build, canary, flaky-test-detector) now has a small
`job_playwright_image` prerequisite job that runs the composite action
and outputs the image reference for downstream container jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the default value for PLAYWRIGHT_VERSION ARG so the build
fails if the arg isn't passed. The ensure-playwright-image action
reads the version from browser-integration-tests/package.json and
passes it as a build arg — that is the single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplify the checkout steps — full checkout is fine here and avoids
potential issues with missing files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@playwright/test is in `dependencies` (not `devDependencies`) in
browser-integration-tests/package.json. Check both fields so the
version is found regardless of which section it's declared in.

Also cleaned up the version consistency check to use a shared function
and log the canonical version for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Firefox fails with "Sandbox: CanCreateUserNamespace() clone() failure:
EPERM" when running in a Docker container on GitHub Actions because user
namespaces are restricted. Adding --ipc=host allows the browser sandbox
to work correctly for all browsers (chromium, firefox, webkit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The image tag was only based on the Playwright version, so changes to
the Dockerfile (e.g. adding yarn, system deps) didn't trigger a rebuild.

Tag is now `v<playwright-version>-<8-char-dockerfile-hash>`, e.g.
`v1.56.0-a1b2c3d4`. Any change to the Dockerfile produces a new tag,
which misses the GHCR cache and triggers a fresh build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHub Actions runs containers as root but sets HOME=/github/home
(owned by pwuser). Firefox refuses to launch as root in another
user's home directory. Setting HOME=/root fixes this.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The -e flag in container options gets overridden by GitHub Actions.
Use container.env which is the proper way to set environment variables
inside job containers and takes precedence over the Actions default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
actions/cache computes an internal version hash from the cache paths
and runner environment. When a cache is saved on the host runner but
restored inside a Docker container, the paths resolve differently
(e.g. ~ expands to /home/runner vs /root), causing a version mismatch
and cache miss.

Adding `enableCrossOsArchive: true` to all cache save/restore steps
makes the version hash environment-independent, allowing caches to be
shared between host runners and container jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mydea and others added 28 commits April 16, 2026 15:21
Use ubuntu:24.04 as the base image instead of the official Playwright
Docker image to avoid missing system packages and config differences.
Install Node/Yarn via Volta to match the repo's version management.
Since the container now uses the same OS as GHA runners, remove the
enableCrossOsArchive workaround from all cache steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend ghcr.io/actions/actions-runner:2.333.1 instead of plain ubuntu.
Remove Volta — actions/setup-node in workflow steps handles Node/Yarn.
Update HOME env to /home/runner to match the runner user in the image.
Remove enableCrossOsArchive since the container OS matches the host.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ions

Read node and yarn versions from the root package.json volta config and
pass them as build args to the Dockerfile. This ensures the Docker image
always has Node and Yarn installed at the same versions the repo pins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required by actions/cache for compression. The actions-runner base
image does not include it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
actions/cache needs this flag when caches are saved on the host but
restored inside Docker containers, even when both run the same OS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GHA overrides HOME=/github/home inside containers, so browsers installed
under /home/runner/.cache weren't found at runtime. Fix by:
- Setting PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers (fixed, HOME-independent)
- Using system-wide git config for safe.directory (works with any HOME)
- Removing HOME env overrides from all workflow container configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E tests need Docker-in-Docker (Verdaccio registry). The actions-runner
base image runs as non-root 'runner' user which cannot access the
Docker socket mounted from the host. Run as root to fix permissions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both host (ubuntu-24.04) and container (actions-runner, also Ubuntu 24.04)
are the same OS, so enableCrossOsArchive is not needed. Removing it
fixes cache misses caused by version hash differences between caches
saved without the flag and restores attempted with it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add v2 prefix to dependency cache key to bust stale caches that were
saved without enableCrossOsArchive. Re-add the flag to all cache steps
so new caches are saved in cross-OS format from the start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sudo resets environment variables by default, so the ENV set in the
Dockerfile wasn't visible to the npx playwright install command.
Explicitly pass the variable so browsers are installed to /opt/pw-browsers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Firefox refuses to run as root when HOME points to a directory owned by
a different user. GHA sets HOME=/github/home (owned by runner) but we
run as --user root. Setting HOME=/root fixes the ownership mismatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
github.workspace resolves to different absolute paths on the host
(/home/runner/work/...) vs inside a container (/__w/...), causing
cache version hash mismatches. Use relative paths instead, which
actions/cache resolves from the working directory.

Bust cache key to v3 since the path format changed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GHA hard-codes HOME=/github/home when creating the container, which
overrides our container.env setting. Using -e HOME=/root in the
container options flag takes precedence over GHA's injected value.
Firefox requires HOME to be owned by the current user (root).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When running inside a GHA container, paths resolve to /__w/... which
only exists inside the container. Docker-in-Docker volume mounts need
host paths (/home/runner/work/...). Convert paths in registrySetup.ts.

Also move HOME=/root to docker -e flag since GHA overrides container
env settings with its own HOME=/github/home.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
runner.temp resolves to the host path (/home/runner/work/_temp) but
inside a container the equivalent path is /__w/_temp. The copy step
writes to the host path which doesn't exist inside the container.

Use a workspace-relative path (dev-packages/tmp-test-application)
instead, which is inside the mounted workspace volume and accessible
from both host and container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GHA strips -e flags from container options, so HOME can't be overridden
that way. Instead, add a 'Fix HOME for root user' step that writes
HOME=/root to GITHUB_ENV at the start of every container job.

Firefox requires HOME to be owned by the current user (root), and GHA
sets HOME=/github/home (owned by runner).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only the browser Playwright tests job runs Firefox (via matrix project).
All other container jobs (loader tests, Remix, E2E, canary, flaky
detector) only run chromium and don't need the HOME override.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E tests use Docker-in-Docker (Verdaccio registry) which doesn't work
inside a GHA container due to network isolation. On develop these jobs
run directly on the host. Only browser Playwright, loader, and Remix
tests need the Playwright container image.

Revert registrySetup.ts path conversion and temp directory changes for
E2E jobs since they now run on the host again.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Dockerfile.publish-packages container only provided a pinned Node.js
version for running `npm publish` of pre-built tarballs. The host already
has the correct Node.js version via Volta, so the Docker build+run overhead
is unnecessary. Run the publish script directly on the host instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the tarball publishing logic into an importable function in
lib/publishPackages.ts and call it directly from registrySetup instead
of spawning a subprocess. Remove the old publish-packages.ts script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR #20329 removed Docker-based package publishing, so E2E jobs can now
run in the Playwright container without Docker-in-Docker issues.

Re-add the container block to E2E jobs and use workspace-relative temp
paths (runner.temp doesn't work inside containers).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mydea mydea force-pushed the fn/playwright-docker-images branch from 29281a3 to 213b16a Compare April 16, 2026 13:21
Docker containers don't have IPv6 enabled on the loopback interface by
default. Node.js fetch resolves localhost to ::1 (IPv6) first, which
fails when IPv6 is disabled. This breaks E2E tests that do server-side
self-referencing fetch (e.g., Astro SSR fetching its own API endpoints).

Enable IPv6 via --sysctl net.ipv6.conf.all.disable_ipv6=0 on all
container jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant