name: CI on: push: branches: - main pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest timeout-minutes: 45 strategy: matrix: node-version: [22] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache npm directory uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - name: Cache node_modules id: node-modules-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - name: Install ripgrep run: sudo apt-get update && sudo apt-get install -y ripgrep - name: Run lint suite run: npm run lint - name: Build assets run: npm run build - name: Generate dist hash state if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | mkdir -p artifacts node scripts/hash-dist.mjs --dist dist --output artifacts/dist-hashes-current.json - name: Fetch prior dist hash state if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: DIST_HASH_STATE_URL: ${{ secrets.DIST_HASH_STATE_URL }} run: | if [ -n "$DIST_HASH_STATE_URL" ]; then curl --fail --silent --show-error "$DIST_HASH_STATE_URL" -o artifacts/dist-hashes-previous.json echo "Loaded prior dist hash state from authoritative store." else echo '{"combined":"","files":[]}' > artifacts/dist-hashes-previous.json echo "No DIST_HASH_STATE_URL configured; using empty baseline." fi - name: Diff dist hash states if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | node scripts/diff-dist-hashes.mjs \ --current artifacts/dist-hashes-current.json \ --previous artifacts/dist-hashes-previous.json > artifacts/dist-hashes-changed.txt echo "Changed dist paths:" cat artifacts/dist-hashes-changed.txt || true - name: Plan cache purge scope if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | node scripts/plan-cache-purge.mjs \ --changed-file artifacts/dist-hashes-changed.txt \ --output-file artifacts/dist-purge-targets.txt > artifacts/dist-purge-plan.json cat artifacts/dist-purge-plan.json - name: Purge changed Cloudflare URLs if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }} run: | if [ ! -s artifacts/dist-purge-targets.txt ]; then echo "No purge targets detected. Skipping purge." exit 0 fi if [ -z "$CLOUDFLARE_ZONE_ID" ] || [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then echo "Cloudflare secrets not configured; skipping targeted purge." exit 0 fi node scripts/purge-cloudflare-changed.mjs \ --changed-file artifacts/dist-purge-targets.txt \ --base-url "$CLOUDFLARE_PURGE_BASE_URL" - name: Record authoritative dist hash state (post-deploy) if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: DIST_HASH_STATE_UPLOAD_URL: ${{ secrets.DIST_HASH_STATE_UPLOAD_URL }} run: | if [ -z "$DIST_HASH_STATE_UPLOAD_URL" ]; then echo "No DIST_HASH_STATE_UPLOAD_URL configured; skipping authoritative hash-state update." exit 0 fi node scripts/store-dist-hash-state.mjs \ --input artifacts/dist-hashes-current.json \ --upload-url "$DIST_HASH_STATE_UPLOAD_URL" - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run visual regression tests id: visual run: npm run test:visual continue-on-error: true - name: Upload Playwright artifacts if: steps.visual.outcome == 'failure' uses: actions/upload-artifact@v4 with: name: playwright-visual-diffs path: | playwright-report test-results if-no-files-found: ignore - name: Fail if visual regressions detected if: steps.visual.outcome == 'failure' run: exit 1 unit-tests: runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: node-version: [22] shard: [1, 2, 3] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache npm directory uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - name: Cache node_modules id: node-modules-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - name: Run unit tests (shard ${{ matrix.shard }}/3) env: UNIT_TEST_TIMEOUT_MS: 300000 run: npm run test:unit:shard${{ matrix.shard }} dm-unit-tests: runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: node-version: [22] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache npm directory uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - name: Cache node_modules id: node-modules-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - name: Run DM unit tests run: npm run test:dm:unit dm-integration-tests: runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: node-version: [22] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache npm directory uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - name: Cache node_modules id: node-modules-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - name: Run DM integration tests run: npm run test:dm:integration e2e-headless: runs-on: ubuntu-latest timeout-minutes: 45 strategy: matrix: node-version: [22] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache npm directory uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - name: Cache node_modules id: node-modules-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - name: Install dependencies if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run headless E2E tests run: npm run test:e2e