Build & Test workflow (cure53/DOMPurify)
The Build & Test workflow from cure53/DOMPurify, explained and optimized by Latchkey.
CI health: A - excellent
Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build & Test workflow from the cure53/DOMPurify repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Build & Test
on:
push:
branches:
- main
- 3.x
- 2.x
# Skip on docs-only / metadata-only pushes. Keep this list in SYNC with
# build-and-test-skip.yml - that companion workflow reports success for
# the same required-check names when this one is skipped, otherwise PRs
# that only touch docs would hang forever on "Waiting for status to be
# reported" (GitHub's well-known paths-filter + required-check gotcha).
paths-ignore:
- '**/*.md'
- 'demos/**'
- 'website/**'
- 'LICENSE'
- '.gitattributes'
- '.editorconfig'
- '.prettierrc'
- '.nvmrc'
- '.gitignore'
- '.husky/**'
- 'osv-scanner.toml'
- '.github/ISSUE_TEMPLATE.md'
- '.github/PULL_REQUEST_TEMPLATE.md'
- '.github/FUNDING.yml'
- '.github/dependabot.yml'
- '.github/workflows/codeql-analysis.yml'
- '.github/workflows/dependency-review.yml'
- '.github/workflows/fuzz.yml'
- '.github/workflows/scorecard.yml'
- '.github/workflows/sign-release.yml'
- '.github/workflows/slsa-provenance.yml'
- '.github/workflows/legacy-browsers.yml'
pull_request:
paths-ignore:
- '**/*.md'
- 'demos/**'
- 'website/**'
- 'LICENSE'
- '.gitattributes'
- '.editorconfig'
- '.prettierrc'
- '.nvmrc'
- '.gitignore'
- '.husky/**'
- 'osv-scanner.toml'
- '.github/ISSUE_TEMPLATE.md'
- '.github/PULL_REQUEST_TEMPLATE.md'
- '.github/FUNDING.yml'
- '.github/dependabot.yml'
- '.github/workflows/codeql-analysis.yml'
- '.github/workflows/dependency-review.yml'
- '.github/workflows/fuzz.yml'
- '.github/workflows/scorecard.yml'
- '.github/workflows/sign-release.yml'
- '.github/workflows/slsa-provenance.yml'
- '.github/workflows/legacy-browsers.yml'
permissions:
contents: read
# Cancel superseded runs on the same PR to save minutes on rapid pushes.
# Pushes to protected branches are NOT cancelled - we want full CI history
# for every merged commit.
concurrency:
group: build-test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Fast feedback: lint, build, dist-sync and jsdom across all supported
# Node versions. The full three-engine browser suite runs once, on the
# primary Node version only - browser behaviour is Node-independent, so
# repeating it per Node entry added cost without coverage.
# Runs on every push and every PR.
install:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x, 24.x, 25.x, 26.x]
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install Dependencies
run: npm ci --ignore-scripts
# All three engines, but only on the primary Node version (the same
# one the browser-matrix job uses). DOMPurify's security posture
# depends on per-engine HTML parser behaviour being exercised on
# every PR - but the browsers don't care which Node drives them, so
# installing and running 3 engines x 5 Node versions repeated the
# same suite 15 times. This gate keeps the full per-engine coverage
# once per PR and drops the 12 redundant runs.
- name: Install Playwright Browsers
if: matrix.node-version == '25.x'
run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit
- name: Build
run: npm run build
- name: Verify dist/ matches src/
run: |
if ! git diff --quiet dist/; then
echo "::error::dist/ is out of sync with src/. The husky pre-commit hook should have rebuilt dist/ before commit. Did you commit without running 'npm install' first (which wires up the hook), or bypass hooks with --no-verify?"
echo "--- dist/ diff ---"
git diff --stat dist/
git diff dist/ | head -100
exit 1
fi
echo "dist/ matches src/ ✓"
- name: Lint
run: npm run lint
- name: Test (jsdom + full browser matrix)
if: matrix.node-version == '25.x'
run: npm run test:ci
- name: Test (jsdom)
if: matrix.node-version != '25.x'
run: npm run test:jsdom
- name: Upload Playwright report
if: failure() && matrix.node-version == '25.x'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-node-${{ matrix.node-version }}
path: playwright-report/
retention-days: 7
- name: Verify TypeScript
run: npm run verify-typescript
# Browser diversity: chromium + firefox + webkit across Ubuntu, macOS, and
# Windows. Catches OS-specific rendering quirks (e.g. macOS WebKit ≈ real
# Safari engine, Windows font shaping, Linux-specific parser paths).
#
# Only runs on release branches (main, 2.x, 3.x) to conserve runner minutes.
# PRs get full three-engine coverage on a single Node version from the
# "install" job above, which is sufficient for catching regressions.
browser-matrix:
if: github.event_name == 'push'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node.js 25.x
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 25.x
cache: npm
- name: Install Dependencies
run: npm ci --ignore-scripts
- name: Install Playwright Browsers (Linux)
if: runner.os == 'Linux'
run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit
- name: Install Playwright Browsers (macOS / Windows)
if: runner.os != 'Linux'
run: ./node_modules/.bin/playwright install chromium firefox webkit
- name: Build
run: npm run build
- name: Run browser tests (all engines)
run: npm run test:browser
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-${{ matrix.os }}
path: playwright-report/
retention-days: 7
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Build & Test on: push: branches: - main - 3.x - 2.x # Skip on docs-only / metadata-only pushes. Keep this list in SYNC with # build-and-test-skip.yml - that companion workflow reports success for # the same required-check names when this one is skipped, otherwise PRs # that only touch docs would hang forever on "Waiting for status to be # reported" (GitHub's well-known paths-filter + required-check gotcha). paths-ignore: - '**/*.md' - 'demos/**' - 'website/**' - 'LICENSE' - '.gitattributes' - '.editorconfig' - '.prettierrc' - '.nvmrc' - '.gitignore' - '.husky/**' - 'osv-scanner.toml' - '.github/ISSUE_TEMPLATE.md' - '.github/PULL_REQUEST_TEMPLATE.md' - '.github/FUNDING.yml' - '.github/dependabot.yml' - '.github/workflows/codeql-analysis.yml' - '.github/workflows/dependency-review.yml' - '.github/workflows/fuzz.yml' - '.github/workflows/scorecard.yml' - '.github/workflows/sign-release.yml' - '.github/workflows/slsa-provenance.yml' - '.github/workflows/legacy-browsers.yml' pull_request: paths-ignore: - '**/*.md' - 'demos/**' - 'website/**' - 'LICENSE' - '.gitattributes' - '.editorconfig' - '.prettierrc' - '.nvmrc' - '.gitignore' - '.husky/**' - 'osv-scanner.toml' - '.github/ISSUE_TEMPLATE.md' - '.github/PULL_REQUEST_TEMPLATE.md' - '.github/FUNDING.yml' - '.github/dependabot.yml' - '.github/workflows/codeql-analysis.yml' - '.github/workflows/dependency-review.yml' - '.github/workflows/fuzz.yml' - '.github/workflows/scorecard.yml' - '.github/workflows/sign-release.yml' - '.github/workflows/slsa-provenance.yml' - '.github/workflows/legacy-browsers.yml' permissions: contents: read # Cancel superseded runs on the same PR to save minutes on rapid pushes. # Pushes to protected branches are NOT cancelled - we want full CI history # for every merged commit. concurrency: group: build-test-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: # Fast feedback: lint, build, dist-sync and jsdom across all supported # Node versions. The full three-engine browser suite runs once, on the # primary Node version only - browser behaviour is Node-independent, so # repeating it per Node entry added cost without coverage. # Runs on every push and every PR. install: timeout-minutes: 30 runs-on: latchkey-small strategy: matrix: node-version: [20.x, 22.x, 24.x, 25.x, 26.x] steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node-version }} cache: npm - name: Install Dependencies run: npm ci --ignore-scripts # All three engines, but only on the primary Node version (the same # one the browser-matrix job uses). DOMPurify's security posture # depends on per-engine HTML parser behaviour being exercised on # every PR - but the browsers don't care which Node drives them, so # installing and running 3 engines x 5 Node versions repeated the # same suite 15 times. This gate keeps the full per-engine coverage # once per PR and drops the 12 redundant runs. - name: Install Playwright Browsers if: matrix.node-version == '25.x' run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit - name: Build run: npm run build - name: Verify dist/ matches src/ run: | if ! git diff --quiet dist/; then echo "::error::dist/ is out of sync with src/. The husky pre-commit hook should have rebuilt dist/ before commit. Did you commit without running 'npm install' first (which wires up the hook), or bypass hooks with --no-verify?" echo "--- dist/ diff ---" git diff --stat dist/ git diff dist/ | head -100 exit 1 fi echo "dist/ matches src/ ✓" - name: Lint run: npm run lint - name: Test (jsdom + full browser matrix) if: matrix.node-version == '25.x' run: npm run test:ci - name: Test (jsdom) if: matrix.node-version != '25.x' run: npm run test:jsdom - name: Upload Playwright report if: failure() && matrix.node-version == '25.x' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: playwright-report-node-${{ matrix.node-version }} path: playwright-report/ retention-days: 7 - name: Verify TypeScript run: npm run verify-typescript # Browser diversity: chromium + firefox + webkit across Ubuntu, macOS, and # Windows. Catches OS-specific rendering quirks (e.g. macOS WebKit ≈ real # Safari engine, Windows font shaping, Linux-specific parser paths). # # Only runs on release branches (main, 2.x, 3.x) to conserve runner minutes. # PRs get full three-engine coverage on a single Node version from the # "install" job above, which is sufficient for catching regressions. browser-matrix: timeout-minutes: 30 if: github.event_name == 'push' runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Node.js 25.x uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 25.x cache: npm - name: Install Dependencies run: npm ci --ignore-scripts - name: Install Playwright Browsers (Linux) if: runner.os == 'Linux' run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit - name: Install Playwright Browsers (macOS / Windows) if: runner.os != 'Linux' run: ./node_modules/.bin/playwright install chromium firefox webkit - name: Build run: npm run build - name: Run browser tests (all engines) run: npm run test:browser - name: Upload Playwright report if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: playwright-report-${{ matrix.os }} path: playwright-report/ retention-days: 7
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
- End-to-end and browser tests
This workflow runs 2 jobs (8 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.