diff --git a/.github/actions/ensure-playwright-image/action.yml b/.github/actions/ensure-playwright-image/action.yml new file mode 100644 index 000000000000..647bd933e9e4 --- /dev/null +++ b/.github/actions/ensure-playwright-image/action.yml @@ -0,0 +1,113 @@ +name: 'Ensure Playwright Docker Image' +description: 'Checks if the custom Playwright Docker image exists in GHCR. Builds and pushes it if missing.' + +inputs: + github_token: + description: 'GitHub token with packages:write permission' + required: true + +outputs: + image: + description: 'The full Docker image reference (e.g. ghcr.io/owner/repo/playwright:v1.56.0)' + value: ${{ steps.image.outputs.ref }} + +runs: + using: 'composite' + steps: + - name: Get versions + id: versions + run: | + PLAYWRIGHT_VERSION=$(node -p " + const pkg = require('./dev-packages/browser-integration-tests/package.json'); + const v = (pkg.dependencies || {})['@playwright/test'] || (pkg.devDependencies || {})['@playwright/test']; + v.replace(/[~^]/, '') + ") + ROOT_PKG=$(node -p "JSON.stringify(require('./package.json').volta)") + NODE_VERSION=$(echo "$ROOT_PKG" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).node") + YARN_VERSION=$(echo "$ROOT_PKG" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).yarn") + echo "playwright=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT + echo "node=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "yarn=$YARN_VERSION" >> $GITHUB_OUTPUT + shell: bash + + - name: Verify @playwright/test version consistency + shell: bash + run: | + CANONICAL=$(node -p " + const pkg = require('./dev-packages/browser-integration-tests/package.json'); + (pkg.dependencies || {})['@playwright/test'] || (pkg.devDependencies || {})['@playwright/test'] + ") + echo "Canonical @playwright/test version: $CANONICAL" + MISMATCHES=0 + + check_version() { + local file="$1" + local version + version=$(node -p " + const pkg = require('./$file'); + (pkg.dependencies || {})['@playwright/test'] || + (pkg.devDependencies || {})['@playwright/test'] || + (pkg.peerDependencies || {})['@playwright/test'] || + 'not found' + ") + if [ "$version" != "not found" ] && [ "$version" != "$CANONICAL" ]; then + echo "::error file=$file::@playwright/test version mismatch: $version (expected $CANONICAL)" + MISMATCHES=$((MISMATCHES + 1)) + fi + } + + check_version "dev-packages/test-utils/package.json" + + for pkg in dev-packages/e2e-tests/test-applications/*/package.json; do + check_version "$pkg" + done + + if [ "$MISMATCHES" -gt 0 ]; then + echo "Found $MISMATCHES package(s) with mismatched @playwright/test versions." + echo "All packages must use the same version as dev-packages/browser-integration-tests ($CANONICAL)." + exit 1 + fi + echo "All @playwright/test versions are consistent ($CANONICAL)" + + - name: Set image reference + id: image + shell: bash + run: | + DOCKERFILE_HASH=$(sha256sum .github/docker/playwright.Dockerfile | cut -c1-8) + TAG="v${{ steps.versions.outputs.playwright }}-${DOCKERFILE_HASH}" + echo "ref=ghcr.io/${{ github.repository }}/playwright:${TAG}" >> $GITHUB_OUTPUT + + - name: Check if image already exists + id: check + shell: bash + run: | + if docker manifest inspect "${{ steps.image.outputs.ref }}" > /dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Image ${{ steps.image.outputs.ref }} already exists, skipping build." + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Image ${{ steps.image.outputs.ref }} not found, will build." + fi + env: + DOCKER_CLI_EXPERIMENTAL: enabled + + - name: Log in to GHCR + if: steps.check.outputs.exists == 'false' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.github_token }} + + - name: Build and push + if: steps.check.outputs.exists == 'false' + uses: docker/build-push-action@v6 + with: + file: .github/docker/playwright.Dockerfile + push: true + build-args: | + PLAYWRIGHT_VERSION=${{ steps.versions.outputs.playwright }} + NODE_VERSION=${{ steps.versions.outputs.node }} + YARN_VERSION=${{ steps.versions.outputs.yarn }} + tags: | + ${{ steps.image.outputs.ref }} diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index a862cd355af2..88bd96548b24 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -20,6 +20,7 @@ runs: with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ steps.compute_lockfile_hash.outputs.hash }} + enableCrossOsArchive: true - name: Install dependencies if: steps.cache_dependencies.outputs.cache-hit != 'true' diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml deleted file mode 100644 index ec6ae171e925..000000000000 --- a/.github/actions/install-playwright/action.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: 'Install Playwright dependencies' -description: 'Installs Playwright dependencies and caches them.' -inputs: - browsers: - description: 'What browsers to install.' - default: 'chromium webkit firefox' - cwd: - description: 'The working directory to run Playwright in.' - default: '.' - -runs: - using: 'composite' - steps: - - name: Get Playwright version - id: playwright-version - run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - shell: bash - working-directory: ${{ inputs.cwd }} - - - name: Restore cached playwright binaries - uses: actions/cache/restore@v5 - id: playwright-cache - with: - path: | - ~/.cache/ms-playwright - # Bump the iteration when bumping runner images to use a new cache - key: playwright-${{ runner.os }}-iteration-1-${{ steps.playwright-version.outputs.version }} - - # We always install all browsers, if uncached - - name: Install Playwright dependencies (uncached) - run: npx playwright install chromium webkit firefox --with-deps - if: steps.playwright-cache.outputs.cache-hit != 'true' - shell: bash - working-directory: ${{ inputs.cwd }} - - - name: Install Playwright system dependencies only (cached) - env: - PLAYWRIGHT_BROWSERS: ${{ inputs.browsers || 'chromium webkit firefox' }} - run: npx playwright install-deps "$PLAYWRIGHT_BROWSERS" - if: steps.playwright-cache.outputs.cache-hit == 'true' - shell: bash - working-directory: ${{ inputs.cwd }} - - # Only store cache on develop branch - - name: Store cached playwright binaries - uses: actions/cache/save@v5 - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' - with: - path: | - ~/.cache/ms-playwright - # Bump the iteration when bumping runner images to use a new cache - key: playwright-${{ runner.os }}-iteration-1-${{ steps.playwright-version.outputs.version }} diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index 1d5126fbe952..08fb18f94f37 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -15,6 +15,7 @@ runs: with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ inputs.dependency_cache_key }} + enableCrossOsArchive: true - name: Restore build artifacts uses: actions/download-artifact@v7 diff --git a/.github/docker/playwright.Dockerfile b/.github/docker/playwright.Dockerfile new file mode 100644 index 000000000000..8fbcaa2ec9f9 --- /dev/null +++ b/.github/docker/playwright.Dockerfile @@ -0,0 +1,50 @@ +# Extend the GitHub Actions runner image (Ubuntu 24.04 based) to get an +# environment close to what GHA hosted runners provide. +FROM ghcr.io/actions/actions-runner:2.333.1 + +ARG PLAYWRIGHT_VERSION +ARG NODE_VERSION +ARG YARN_VERSION + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install zstd (required by actions/cache for compression), a JRE for Firebase +# emulators (Firestore, etc. spawn `java -version`), and Node.js + Yarn at the +# versions pinned in the repo's package.json. +RUN sudo apt-get update && \ + sudo apt-get install -y --no-install-recommends zstd openjdk-17-jre-headless && \ + sudo rm -rf /var/lib/apt/lists/* +RUN sudo curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION%%.*}.x | sudo bash - && \ + sudo apt-get install -y --no-install-recommends nodejs && \ + sudo rm -rf /var/lib/apt/lists/* && \ + sudo npm install -g yarn@${YARN_VERSION} + +# Install Playwright browsers and their OS-level dependencies. +# Use a fixed path so browsers are found regardless of HOME at runtime +# (GHA sets HOME=/github/home inside containers, not /home/runner). +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers +RUN sudo mkdir -p /opt/pw-browsers && \ + sudo PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers npx playwright@${PLAYWRIGHT_VERSION} install chromium webkit firefox --with-deps + +# Mark GitHub Actions workspace as safe for git (system-wide config so it +# works regardless of HOME, which GHA overrides to /github/home). +RUN sudo git config --system --add safe.directory '*' + +# Ensure /root exists and is owned by root. When running as --user root, +# Firefox requires HOME to be owned by the current user. GHA sets +# HOME=/github/home (owned by runner), but we can't override it. +# The workflow sets HOME=/root via a step, and this ensures /root is ready. +RUN mkdir -p /root + +# Docker CLI + Compose v2 for E2E apps that run `docker compose` against the +# host daemon (workflows mount /var/run/docker.sock). Image is built amd64 on GHA. +ARG DOCKER_VERSION=27.4.1 +ARG DOCKER_COMPOSE_VERSION=2.32.1 +RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" | sudo tar -xz -C /tmp && \ + sudo mv /tmp/docker/docker /usr/local/bin/docker && \ + sudo rm -rf /tmp/docker && \ + sudo mkdir -p /usr/local/lib/docker/cli-plugins && \ + sudo curl -fsSL "https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose && \ + sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 544bb7900008..89e9b6dc7733 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,25 +28,20 @@ concurrency: env: HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} - # WARNING: this disables cross os caching as ~ and - # github.workspace evaluate to differents paths CACHED_DEPENDENCY_PATHS: | - ${{ github.workspace }}/node_modules - ${{ github.workspace }}/packages/*/node_modules - ${{ github.workspace }}/dev-packages/*/node_modules + node_modules + packages/*/node_modules + dev-packages/*/node_modules ~/.cache/mongodb-binaries/ # DEPENDENCY_CACHE_KEY: can't be set here because we don't have access to yarn.lock - # WARNING: this disables cross os caching as ~ and - # github.workspace evaluate to differents paths - # packages/utils/cjs and packages/utils/esm: Symlinks to the folders inside of `build`, needed for tests CACHED_BUILD_PATHS: | - ${{ github.workspace }}/dev-packages/*/build - ${{ github.workspace }}/packages/*/build - ${{ github.workspace }}/packages/*/lib - ${{ github.workspace }}/packages/ember/*.d.ts - ${{ github.workspace }}/packages/gatsby/*.d.ts + dev-packages/*/build + packages/*/build + packages/*/lib + packages/ember/*.d.ts + packages/gatsby/*.d.ts BUILD_CACHE_TARBALL_KEY: tarball-${{ github.event.inputs.commit || github.sha }} @@ -65,12 +60,31 @@ env: jobs: job_get_metadata: + name: Get Metadata uses: ./.github/workflows/ci-metadata.yml with: head_commit: ${{ github.event.inputs.commit || github.sha }} permissions: pull-requests: read + job_playwright_image: + name: Ensure Playwright Image + runs-on: ubuntu-24.04 + permissions: + packages: write + outputs: + image: ${{ steps.ensure.outputs.image }} + steps: + - name: Check out current commit + uses: actions/checkout@v6 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Ensure Playwright image + id: ensure + uses: ./.github/actions/ensure-playwright-image + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + job_build: name: Build needs: job_get_metadata @@ -478,9 +492,15 @@ jobs: name: Playwright ${{ matrix.bundle }}${{ matrix.project && matrix.project != 'chromium' && format(' {0}', matrix.project) || ''}}${{ matrix.shard && format(' ({0}/{1})', matrix.shard, matrix.shards) || ''}} Tests - needs: [job_get_metadata, job_build] + needs: [job_get_metadata, job_build, job_playwright_image] if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04-large-js + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 timeout-minutes: 25 strategy: fail-fast: false @@ -530,6 +550,8 @@ jobs: project: 'chromium' steps: + - name: Fix HOME for root user + run: echo "HOME=/root" >> $GITHUB_ENV - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v6 with: @@ -543,11 +565,6 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: ${{ matrix.project }} - - name: Run Playwright tests env: PW_BUNDLE: ${{ matrix.bundle }} @@ -581,9 +598,15 @@ jobs: job_browser_loader_tests: name: PW ${{ matrix.bundle }} Tests - needs: [job_get_metadata, job_build] + needs: [job_get_metadata, job_build, job_playwright_image] if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 timeout-minutes: 15 strategy: fail-fast: false @@ -611,11 +634,6 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - - name: Run Playwright Loader tests env: PW_BUNDLE: ${{ matrix.bundle }} @@ -800,9 +818,15 @@ jobs: job_remix_integration_tests: name: Remix (Node ${{ matrix.node }}) Tests - needs: [job_get_metadata, job_build] + needs: [job_get_metadata, job_build, job_playwright_image] if: needs.job_build.outputs.changed_remix == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 timeout-minutes: 15 strategy: fail-fast: false @@ -822,11 +846,6 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - - name: Run integration tests env: NODE_VERSION: ${{ matrix.node }} @@ -879,8 +898,9 @@ jobs: - name: Stores tarballs in cache uses: actions/cache/save@v5 with: - path: ${{ github.workspace }}/packages/*/*.tgz + path: packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + enableCrossOsArchive: true - name: Determine which E2E test applications should be run id: matrix @@ -902,8 +922,15 @@ jobs: # See: https://github.com/actions/runner/issues/2205 if: always() && needs.job_e2e_prepare.result == 'success' && needs.job_e2e_prepare.outputs.matrix != '{"include":[]}' - needs: [job_get_metadata, job_build, job_e2e_prepare] + && needs.job_playwright_image.result == 'success' + needs: [job_get_metadata, job_build, job_e2e_prepare, job_playwright_image] runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 -v /var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway timeout-minutes: 15 env: # We just use a dummy DSN here, only send to the tunnel anyhow @@ -952,8 +979,9 @@ jobs: uses: actions/cache/restore@v5 id: restore-tarball-cache with: - path: ${{ github.workspace }}/packages/*/*.tgz + path: packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + enableCrossOsArchive: true - name: Build tarballs if not cached if: steps.restore-tarball-cache.outputs.cache-hit != 'true' @@ -968,24 +996,22 @@ jobs: working-directory: dev-packages/e2e-tests - name: Copy to temp - run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application + run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ../tmp-test-application working-directory: dev-packages/e2e-tests - name: Build E2E app - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 7 run: ${{ matrix.build-command || 'pnpm test:build' }} env: SENTRY_E2E_WORKSPACE_ROOT: ${{ github.workspace }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - cwd: ${{ runner.temp }}/test-application + - name: Point BullMQ at host Redis (Docker-out-of-Docker) + if: matrix.test-application == 'nestjs-bullmq' + run: echo "REDIS_HOST=host.docker.internal" >> "$GITHUB_ENV" - name: Run E2E test - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 10 run: ${{ matrix.assert-command || 'pnpm test:assert' }} env: @@ -996,7 +1022,7 @@ jobs: if: failure() with: name: playwright-traces-job_e2e_playwright_tests-${{ matrix.test-application}} - path: ${{ runner.temp }}/test-application/test-results + path: dev-packages/tmp-test-application/test-results overwrite: true retention-days: 7 @@ -1010,7 +1036,7 @@ jobs: if: failure() with: name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) - path: ${{ runner.temp }}/test-application/event-dumps + path: dev-packages/tmp-test-application/event-dumps overwrite: true retention-days: 7 if-no-files-found: ignore @@ -1024,9 +1050,16 @@ jobs: if: always() && needs.job_get_metadata.outputs.is_release != 'true' && needs.job_e2e_prepare.result == 'success' && needs.job_e2e_prepare.outputs.matrix-optional != '{"include":[]}' && (github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' - needs: [job_get_metadata, job_build, job_e2e_prepare] + github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' && + needs.job_playwright_image.result == 'success' + needs: [job_get_metadata, job_build, job_e2e_prepare, job_playwright_image] runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 -v /var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway timeout-minutes: 15 env: E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} @@ -1062,8 +1095,9 @@ jobs: uses: actions/cache/restore@v5 id: restore-tarball-cache with: - path: ${{ github.workspace }}/packages/*/*.tgz + path: packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + enableCrossOsArchive: true - name: Build tarballs if not cached if: steps.restore-tarball-cache.outputs.cache-hit != 'true' @@ -1078,22 +1112,16 @@ jobs: working-directory: dev-packages/e2e-tests - name: Copy to temp - run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application + run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ../tmp-test-application working-directory: dev-packages/e2e-tests - name: Build E2E app - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 7 run: ${{ matrix.build-command || 'pnpm test:build' }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - cwd: ${{ runner.temp }}/test-application - - name: Run E2E test - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 10 run: ${{ matrix.assert-command || 'pnpm test:assert' }} @@ -1107,7 +1135,7 @@ jobs: if: failure() with: name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) - path: ${{ runner.temp }}/test-application/event-dumps + path: dev-packages/tmp-test-application/event-dumps overwrite: true retention-days: 7 if-no-files-found: ignore diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index ac4e1df08841..8e2125ba9698 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -13,18 +13,34 @@ env: HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} CACHED_BUILD_PATHS: | - ${{ github.workspace }}/packages/*/*.tgz - ${{ github.workspace }}/node_modules - ${{ github.workspace }}/packages/*/node_modules - ${{ github.workspace }}/dev-packages/*/node_modules - ${{ github.workspace }}/dev-packages/*/build - ${{ github.workspace }}/packages/*/build + packages/*/*.tgz + node_modules + packages/*/node_modules + dev-packages/*/node_modules + dev-packages/*/build + packages/*/build permissions: contents: read issues: write + packages: write jobs: + job_playwright_image: + name: Ensure Playwright Image + runs-on: ubuntu-24.04 + outputs: + image: ${{ steps.ensure.outputs.image }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Ensure Playwright image + id: ensure + uses: ./.github/actions/ensure-playwright-image + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + job_e2e_prepare: name: Prepare E2E Canary tests runs-on: ubuntu-24.04 @@ -43,6 +59,7 @@ jobs: with: path: ${{ env.CACHED_BUILD_PATHS }} key: canary-${{ env.HEAD_COMMIT }} + enableCrossOsArchive: true - name: Install dependencies run: yarn install - name: Build packages @@ -53,8 +70,14 @@ jobs: job_e2e_tests: name: E2E ${{ matrix.label }} Test - needs: [job_e2e_prepare] + needs: [job_e2e_prepare, job_playwright_image] runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 -v /var/run/docker.sock:/var/run/docker.sock --add-host=host.docker.internal:host-gateway timeout-minutes: 20 env: # We just use a dummy DSN here, only send to the tunnel anyhow @@ -139,6 +162,7 @@ jobs: with: path: ${{ env.CACHED_BUILD_PATHS }} key: canary-${{ env.HEAD_COMMIT }} + enableCrossOsArchive: true - name: Validate Verdaccio run: yarn test:validate @@ -149,22 +173,16 @@ jobs: working-directory: dev-packages/e2e-tests - name: Copy to temp - run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application + run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ../tmp-test-application working-directory: dev-packages/e2e-tests - name: Build E2E app - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 7 run: yarn ${{ matrix.build-command }} - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - cwd: ${{ runner.temp }}/test-application - - name: Run E2E test - working-directory: ${{ runner.temp }}/test-application + working-directory: dev-packages/tmp-test-application timeout-minutes: 15 run: yarn test:assert diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index c0a8f1f720b1..3ce7c6f29368 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -22,8 +22,30 @@ concurrency: cancel-in-progress: true jobs: + job_playwright_image: + name: Ensure Playwright Image + runs-on: ubuntu-24.04 + permissions: + packages: write + outputs: + image: ${{ steps.ensure.outputs.image }} + steps: + - uses: actions/checkout@v6 + - name: Ensure Playwright image + id: ensure + uses: ./.github/actions/ensure-playwright-image + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + flaky-detector: + needs: [job_playwright_image] runs-on: ubuntu-24.04 + container: + image: ${{ needs.job_playwright_image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --ipc=host --user root --sysctl net.ipv6.conf.all.disable_ipv6=0 timeout-minutes: 60 name: 'Check tests for flakiness' # Also skip if PR is from master -> develop @@ -49,11 +71,6 @@ jobs: - name: Build packages run: yarn build - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: 'chromium' - - name: Determine changed tests uses: dorny/paths-filter@v4.0.1 id: changed diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts index 090142714d5b..ed9a3d949879 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts @@ -221,7 +221,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa }, 'http.request.header.cf_connecting_ip': { type: 'string', - value: '::1', + value: expect.stringMatching(/^(127\.0\.0\.1|::1)$/), }, 'http.request.header.host': { type: 'string', diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json index 400ab6144248..24ff5ca9762e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "astro dev", "build": "astro build", - "preview": "wrangler dev --port 3030", + "preview": "wrangler dev --ip 0.0.0.0 --port 3030", "test:build": "pnpm install && pnpm build", "test:assert": "TEST_ENV=production playwright test" }, diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json index 2e7cfa8e62dd..86bba3c1cdb3 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json @@ -7,7 +7,7 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "start": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030", + "start": "wrangler dev --ip 0.0.0.0 --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030", "test:build": "pnpm install && pnpm build", "test:assert": "TEST_ENV=production playwright test", "generate-types": "wrangler types" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json index 3d536e6fbabe..ce61fc79ed09 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -1,7 +1,7 @@ { "name": "cloudflare-hono", "scripts": { - "dev": "wrangler dev", + "dev": "wrangler dev --ip 0.0.0.0", "build": "wrangler deploy --dry-run --var E2E_TEST_DSN=$E2E_TEST_DSN", "test": "vitest", "typecheck": "tsc --noEmit", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json index 160b8a9cdc03..1c2a620c86b1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "dev": "wrangler dev --ip 0.0.0.0 --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", "build": "wrangler deploy --dry-run", "test": "vitest --run", "typecheck": "tsc --noEmit", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json index 37b3352bdcfc..01b1a895a0e2 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "dev": "wrangler dev --ip 0.0.0.0 --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", "build": "wrangler deploy --dry-run", "test": "vitest --run", "typecheck": "tsc --noEmit", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index 344337612165..3c526d5c9990 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "dev": "wrangler dev --ip 0.0.0.0 --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", "build": "wrangler deploy --dry-run", "test": "vitest --run", "typecheck": "tsc --noEmit", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json index a0a1b020df86..5b6d76ae0270 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "dev": "wrangler dev --ip 0.0.0.0 --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", "build": "wrangler deploy --dry-run", "test": "vitest --run", "typecheck": "tsc --noEmit", diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/wrangler.toml b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/wrangler.toml index b2de8e7d1321..81f734464407 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/wrangler.toml @@ -1,3 +1,6 @@ name = "hydrogen-react-router-7" main = "server.ts" compatibility_flags = ["transformstream_enable_standard_constructor"] + +[dev] +ip = "0.0.0.0" diff --git a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/app.module.ts index be5fd107e4cb..9dfb0417025e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/app.module.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/app.module.ts @@ -9,7 +9,8 @@ import { TestProcessor } from './jobs/test.processor'; imports: [ SentryModule.forRoot(), BullModule.forRoot({ - connection: { host: 'localhost', port: 6379 }, + // CI: Redis runs on the GHA host via docker compose; REDIS_HOST is set in the workflow. + connection: { host: process.env.REDIS_HOST || 'localhost', port: 6379 }, }), BullModule.registerQueue({ name: 'test-queue' }), ], diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs index 0f15639161dd..3a35d4c3e8ba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs @@ -7,7 +7,8 @@ if (!testEnv) { const getStartCommand = () => { if (testEnv === 'production') { - return 'pnpm cf:preview --port 3030'; + // --ip: CI Playwright container; fetch uses 127.0.0.1 while default bind can be ::1-only. + return 'pnpm cf:preview --ip 0.0.0.0 --port 3030'; } throw new Error(`Unknown test env: ${testEnv}`); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc index 4bf131c387e9..2b5a1a6e82e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc @@ -32,6 +32,10 @@ "observability": { "enabled": true, }, + // Listen on IPv4 in CI (Playwright job container); avoids ::1-only bind vs fetch to 127.0.0.1. + "dev": { + "ip": "0.0.0.0", + }, /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts index 0bb957bbc466..aae461371773 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts @@ -104,7 +104,8 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru 'http.request.method': 'GET', 'http.request.method_original': 'GET', 'http.response.status_code': 200, - 'network.peer.address': '::1', + // localhost resolves to IPv4 or IPv6 depending on OS / container + 'network.peer.address': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'network.peer.port': 3030, 'otel.kind': 'CLIENT', 'sentry.op': 'http.client', diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js index 8b68e8412aba..73f0b2e52db3 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js @@ -14,7 +14,8 @@ const Boom = require('@hapi/boom'); const server = Hapi.server({ port: 3030, - host: 'localhost', + // Avoid IPv6-only ::1 bind for `localhost` on Linux CI (fetch uses 127.0.0.1). + host: '0.0.0.0', }); const init = async () => { diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/wrangler.toml b/dev-packages/e2e-tests/test-applications/remix-hydrogen/wrangler.toml new file mode 100644 index 000000000000..b01f7ccf2b2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/wrangler.toml @@ -0,0 +1,6 @@ +name = "remix-hydrogen-e2e" +main = "server.ts" +compatibility_flags = ["transformstream_enable_standard_constructor"] + +[dev] +ip = "0.0.0.0" diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json index e17a9b2bdabd..9f8c368720f8 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "wrangler pages dev ./.svelte-kit/cloudflare --port 4173", + "preview": "wrangler pages dev ./.svelte-kit/cloudflare --ip 0.0.0.0 --port 4173", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:e2e": "playwright test", diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts index 2020cfdd09b0..487574b2d6f9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts @@ -29,7 +29,7 @@ test('strips and handles query params in spans of outgoing fetch requests', asyn 'http.request.method': 'GET', 'http.request.method_original': 'GET', 'http.response.status_code': 200, - 'network.peer.address': '::1', + 'network.peer.address': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'network.peer.port': expect.any(Number), 'otel.kind': 'CLIENT', 'server.port': expect.any(Number), diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts index 94ccd6c9702a..11a27a411d8a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts @@ -31,7 +31,7 @@ test('strips and handles query params in spans of outgoing http requests', async 'http.response_content_length_uncompressed': 0, 'http.status_code': 200, 'http.status_text': 'OK', - 'net.peer.ip': '::1', + 'net.peer.ip': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'net.peer.name': 'localhost', 'net.peer.port': expect.any(Number), 'net.transport': 'ip_tcp', diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index ac0ac3780a38..7d48487d1f60 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -118,10 +118,10 @@ describe('httpIntegration', () => { 'http.target': '/test?a=1&b=2', 'http.url': `http://localhost:${port}/test?a=1&b=2`, 'http.user_agent': 'node', - 'net.host.ip': '::1', + 'net.host.ip': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'net.host.name': 'localhost', 'net.host.port': port, - 'net.peer.ip': '::1', + 'net.peer.ip': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'net.peer.port': expect.any(Number), 'net.transport': 'ip_tcp', 'otel.kind': 'SERVER', @@ -160,10 +160,10 @@ describe('httpIntegration', () => { 'http.target': '/test?a=1&b=2', 'http.url': `http://localhost:${port}/test?a=1&b=2`, 'http.user_agent': 'node', - 'net.host.ip': '::1', + 'net.host.ip': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'net.host.name': 'localhost', 'net.host.port': port, - 'net.peer.ip': '::1', + 'net.peer.ip': expect.stringMatching(/^(127\.0\.0\.1|::1)$/), 'net.peer.port': expect.any(Number), 'net.transport': 'ip_tcp', 'otel.kind': 'SERVER', diff --git a/packages/remix/package.json b/packages/remix/package.json index 6241c3ec11d7..74dd7c774d98 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -110,7 +110,7 @@ "test:integration:ci": "run-s test:integration:clean test:integration:prepare test:integration:client:ci test:integration:server", "test:integration:prepare": "(cd test/integration && yarn install)", "test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)", - "test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/ --project='chromium'", + "test:integration:client": "yarn playwright test test/integration/test/client/ --project='chromium'", "test:integration:client:ci": "yarn test:integration:client", "test:integration:server": "export NODE_OPTIONS='--stack-trace-limit=25' && vitest run", "test:unit": "vitest run --config vitest.config.unit.ts", diff --git a/scripts/dependency-hash-key.js b/scripts/dependency-hash-key.js index 55e38e4a385e..7fddbc101003 100644 --- a/scripts/dependency-hash-key.js +++ b/scripts/dependency-hash-key.js @@ -36,7 +36,7 @@ function outputDependencyCacheKey() { // We log the output in a way that the GitHub Actions can append it to the output // We prefix it with `dependencies-` so it is easier to identify in the logs // eslint-disable-next-line no-console - console.log(`hash=dependencies-${hash}`); + console.log(`hash=v3-dependencies-${hash}`); } function getNormalizedDependencies(packageJson, workspacePackageNames) { diff --git a/yarn.lock b/yarn.lock index cc45a89c5703..c7b8ed9f8eaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12554,7 +12554,14 @@ brace-expansion@^2.0.1, brace-expansion@^2.0.2: dependencies: balanced-match "^1.0.0" -brace-expansion@^5.0.2, brace-expansion@^5.0.5: +brace-expansion@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + +brace-expansion@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==