name: Viewer.js vendor and compatibility on: pull_request: branches: [main] push: branches: [main] workflow_dispatch: permissions: contents: read # Superseded PR pushes cancel their older run. Every main push gets its own # group (run_id), so no main commit's run is ever cancelled or dropped as pending. concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Timeouts are the measured job time plus at least 50%, and never under 20 minutes for a browser job, # because runner speed varies. Before sharding (main 2cecfcf, one worker): frontend coverage 32m10s, # page workflows firefox 18m59s, webkit 19m26s. First sharded run (PR #274 run 39712909047, 3 workers, # no npm cache, where npm ci stalled for 5m in six of eight jobs): coverage shards 11m19s/10m47s/10m49s # (tests 5m20s/4m42s/4m46s), merge 5m52s (report and check 4s), firefox shards 7m53s/9m27s (tests # 4m41s/3m29s), webkit shards 12m25s+ (cancelled at the old 12-minute limit after its 267 tests passed in # 6m29s) and 4m53s (tests 3m08s). With the npm cache hit (run 39725176320 at cbc8a61, npm ci 0-2s): # coverage shards 7m34s/6m04s/5m22s (tests 6m38s/4m53s/4m04s), merge 54s, firefox 7m57s/5m21s (tests # 7m08s/4m34s), webkit 8m16s/5m03s (tests 7m26s/3m58s); a cache miss adds up to 5m to npm ci, which # the 20-minute limits absorb. jobs: frontend-coverage: name: frontend coverage (shard ${{ matrix.shard }}/3) runs-on: ubuntu-latest # Layout comparisons depend on the fonts each browser falls back to, so browser jobs run in the # same pinned Playwright image as local runs (tag = @playwright/test in tools/vendor/package-lock.json; # tests/vendor/browser-image.test.mjs checks it). Browsers and fonts come from the image. container: image: mcr.microsoft.com/playwright:v1.64.1-noble options: --user 1001 timeout-minutes: 20 # Public-repo ubuntu-latest runners have 4 vCPUs: three Playwright workers, each with its own test # server, leave one CPU for the servers and the runner. env: PM_TEST_WORKERS: 3 strategy: fail-fast: false matrix: shard: [1, 2, 3] steps: - uses: actions/checkout@v7 # The image's own Python 3.12 and Node 24: setup-python/setup-node install the runner's toolcache # builds, whose entry points name host paths that do not exist in the container. The image has no # ensurepip and the job has no root, so a checksum-pinned pip wheel bootstraps a user-space venv. - name: Python environment env: PIP_WHEEL: https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl PIP_SHA256: 71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e run: | venv="$RUNNER_TEMP/venv" wheel="$RUNNER_TEMP/pip-26.2.1-py3-none-any.whl" python3 -m venv --without-pip "$venv" curl -fsSL --retry 3 -o "$wheel" "$PIP_WHEEL" echo "$PIP_SHA256 $wheel" | sha256sum -c - "$venv/bin/python" "$wheel/pip" install --no-index --no-cache-dir "$wheel" "$venv/bin/python" -m pip install --no-cache-dir -r requirements-dev.txt echo "$venv/bin" >> "$GITHUB_PATH" - run: python --version && node --version && node --version | grep -q '^v24\.' # npm's download cache, keyed on the lockfile; inside the container ~ is the job's HOME (/github/home), # which npm also uses. node_modules itself is still built fresh by npm ci. - name: Cache npm downloads uses: actions/cache@v6 with: path: ~/.npm key: npm-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('tools/vendor/package-lock.json') }} - run: npm ci --prefix tools/vendor --ignore-scripts # Shard 1 also runs the node units and the viewer suite, so the vendor pins and the tooling tests # run on the lightest shard instead. - if: matrix.shard == 3 run: npm run --prefix tools/vendor check - if: matrix.shard == 3 run: npm run --prefix tools/vendor test - name: Page workflows with coverage, shard ${{ matrix.shard }}/3 run: npm run --prefix tools/vendor test:coverage:shard -- ${{ matrix.shard }}/3 - name: Upload raw coverage uses: actions/upload-artifact@v7 with: name: coverage-raw-${{ matrix.shard }} path: coverage/js/raw/ if-no-files-found: error retention-days: 3 - name: Upload Chromium and touch traces if: always() uses: actions/upload-artifact@v7 with: name: frontend-traces-shard-${{ matrix.shard }} path: tools/vendor/test-results/ retention-days: 14 frontend-coverage-merge: name: frontend coverage (merge) needs: frontend-coverage runs-on: ubuntu-latest # Same image as the shards: the raw files name sources by the container's workspace path. container: image: mcr.microsoft.com/playwright:v1.64.1-noble options: --user 1001 timeout-minutes: 10 steps: - uses: actions/checkout@v7 - run: node --version && node --version | grep -q '^v24\.' # npm's download cache, keyed on the lockfile; inside the container ~ is the job's HOME (/github/home), # which npm also uses. node_modules itself is still built fresh by npm ci. - name: Cache npm downloads uses: actions/cache@v6 with: path: ~/.npm key: npm-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('tools/vendor/package-lock.json') }} - run: npm ci --prefix tools/vendor --ignore-scripts # Each artifact lands in its own coverage-raw-N directory, so the next step can count them. - uses: actions/download-artifact@v8 with: pattern: coverage-raw-* path: ${{ runner.temp }}/coverage-raw # coverage:merge only checks that raw files exist; merging two of three shards would report # partial coverage, so a missing or empty shard artifact fails here. - name: Verify every shard's raw coverage arrived shell: bash env: STAGED_RAW: ${{ runner.temp }}/coverage-raw EXPECTED_SHARDS: 3 run: | found=0 for shard in $(seq 1 "$EXPECTED_SHARDS"); do directory="$STAGED_RAW/coverage-raw-$shard" count=0 if [ -d "$directory" ]; then count=$(find "$directory" -maxdepth 1 -type f -name '*.json' | wc -l); fi echo "coverage-raw-$shard: $count raw files" if [ "$count" -gt 0 ]; then found=$((found + 1)); fi done echo "shard artifacts with raw coverage: $found of $EXPECTED_SHARDS" if [ "$found" -ne "$EXPECTED_SHARDS" ]; then echo "::error::Expected raw coverage from $EXPECTED_SHARDS shards, got $found of $EXPECTED_SHARDS" exit 1 fi staged=$(find "$STAGED_RAW" -mindepth 2 -maxdepth 2 -type f -name '*.json' | wc -l) mkdir -p coverage/js/raw # cp -n never overwrites; a clash shows up as fewer merged files than staged ones. find "$STAGED_RAW" -mindepth 2 -maxdepth 2 -type f -name '*.json' -exec cp -n -t coverage/js/raw {} + || true merged=$(find coverage/js/raw -maxdepth 1 -type f -name '*.json' | wc -l) if [ "$merged" -ne "$staged" ]; then echo "::error::Shards wrote clashing raw file names: $staged staged, $merged merged" exit 1 fi echo "merged $merged raw files" - name: Frontend coverage report and baseline check run: npm run --prefix tools/vendor coverage:merge - name: Upload frontend coverage report if: always() uses: actions/upload-artifact@v7 with: name: frontend-test-reports path: | coverage/js/ !coverage/js/raw/** retention-days: 14 browser-workflows: name: page workflows (${{ matrix.browser }}, shard ${{ matrix.shard }}/2) runs-on: ubuntu-latest # Layout comparisons depend on the fonts each browser falls back to, so browser jobs run in the # same pinned Playwright image as local runs (tag = @playwright/test in tools/vendor/package-lock.json; # tests/vendor/browser-image.test.mjs checks it). Browsers and fonts come from the image. container: image: mcr.microsoft.com/playwright:v1.64.1-noble options: --user 1001 timeout-minutes: 20 # Public-repo ubuntu-latest runners have 4 vCPUs: three Playwright workers, each with its own test # server, leave one CPU for the servers and the runner. env: PM_TEST_WORKERS: 3 strategy: fail-fast: false matrix: browser: [firefox, webkit] shard: [1, 2] steps: - uses: actions/checkout@v7 # The image's own Python 3.12 and Node 24: setup-python/setup-node install the runner's toolcache # builds, whose entry points name host paths that do not exist in the container. The image has no # ensurepip and the job has no root, so a checksum-pinned pip wheel bootstraps a user-space venv. - name: Python environment env: PIP_WHEEL: https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl PIP_SHA256: 71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e run: | venv="$RUNNER_TEMP/venv" wheel="$RUNNER_TEMP/pip-26.2.1-py3-none-any.whl" python3 -m venv --without-pip "$venv" curl -fsSL --retry 3 -o "$wheel" "$PIP_WHEEL" echo "$PIP_SHA256 $wheel" | sha256sum -c - "$venv/bin/python" "$wheel/pip" install --no-index --no-cache-dir "$wheel" "$venv/bin/python" -m pip install --no-cache-dir -r requirements-dev.txt echo "$venv/bin" >> "$GITHUB_PATH" - run: python --version && node --version && node --version | grep -q '^v24\.' # npm's download cache, keyed on the lockfile; inside the container ~ is the job's HOME (/github/home), # which npm also uses. node_modules itself is still built fresh by npm ci. - name: Cache npm downloads uses: actions/cache@v6 with: path: ~/.npm key: npm-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('tools/vendor/package-lock.json') }} - run: npm ci --prefix tools/vendor --ignore-scripts - name: Page workflows in ${{ matrix.browser }}, shard ${{ matrix.shard }}/2 run: npm run --prefix tools/vendor test:workflows -- --project ${{ matrix.browser }} --shard ${{ matrix.shard }}/2 - name: Upload browser traces if: always() uses: actions/upload-artifact@v7 with: name: browser-traces-${{ matrix.browser }}-shard-${{ matrix.shard }} path: tools/vendor/test-results/ retention-days: 14 # Branch protection requires the exact context "viewerjs". This job keeps that # name and fails unless every frontend job above succeeded (every shard, the # coverage merge and every browser shard), including when one was cancelled or skipped. viewerjs: needs: [frontend-coverage, frontend-coverage-merge, browser-workflows] if: always() runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Require every frontend job to succeed env: COVERAGE_RESULT: ${{ needs.frontend-coverage.result }} MERGE_RESULT: ${{ needs.frontend-coverage-merge.result }} WORKFLOWS_RESULT: ${{ needs.browser-workflows.result }} run: | echo "frontend-coverage: $COVERAGE_RESULT" echo "frontend-coverage-merge: $MERGE_RESULT" echo "browser-workflows: $WORKFLOWS_RESULT" test "$COVERAGE_RESULT" = success && test "$MERGE_RESULT" = success && test "$WORKFLOWS_RESULT" = success