5minutes_rule #1014
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Simple workflow for deploying static content to GitHub Pages | |
| name: Deploy static content to Pages | |
| on: | |
| # Runs on pushes targeting the default branch. Homepage/UI-only pushes are | |
| # ignored here so the fast UI workflow can deploy them without waiting behind | |
| # this full Worker/release/static validation stack. Mixed or risky pushes | |
| # still run this workflow because paths-ignore only skips when every changed | |
| # file matches the safe UI list. | |
| push: | |
| branches: ["main"] | |
| paths-ignore: | |
| - "index.html" | |
| - "de/index.html" | |
| - "css/pages/index.css" | |
| - "css/base/**" | |
| - "css/components/**" | |
| - "js/pages/index/**" | |
| - "assets/images/**" | |
| - "assets/favicons/**" | |
| - "fonts/**" | |
| - "tests/smoke.spec.js" | |
| # Allows you to run this workflow manually from the Actions tab | |
| workflow_dispatch: | |
| inputs: | |
| release_plan_base_ref: | |
| description: "Git base ref/SHA for release-plan comparison. Defaults to HEAD^ for manual reruns." | |
| required: false | |
| default: "HEAD^" | |
| release_plan_dependency_acknowledgement: | |
| description: "Optional manual static deploy acknowledgement after release-plan Worker/schema dependencies are handled. Must equal I_CONFIRM_RELEASE_PLAN_DEPENDENCIES_HANDLED." | |
| required: false | |
| default: "" | |
| # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. | |
| # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. | |
| concurrency: | |
| group: "pages" | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| release-compatibility: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Audit root dependencies | |
| run: npm audit --audit-level=low | |
| - name: Validate worker package dependencies | |
| run: | | |
| for worker in workers/auth workers/contact workers/ai; do | |
| npm --prefix "$worker" ci | |
| npm --prefix "$worker" ls --depth=0 | |
| done | |
| npm run check:worker-dependency-audits | |
| - name: Check toolchain pinning | |
| run: npm run check:toolchain | |
| - name: Run quality gate tests | |
| run: npm run test:quality-gates | |
| - name: Check committed secrets | |
| run: npm run check:secrets | |
| - name: Check DOM sink baseline | |
| run: npm run check:dom-sinks | |
| - name: Check auth route policy coverage | |
| run: npm run check:route-policies | |
| - name: Test operational readiness checks | |
| run: npm run test:operational-readiness | |
| - name: Check operational readiness docs | |
| run: npm run check:operational-readiness | |
| - name: Run live health check in skipped mode | |
| run: npm run check:live-health | |
| - name: Run live security header check in skipped mode | |
| run: npm run check:live-security-headers | |
| - name: Test live runtime canary skipped/safe mode | |
| run: npm run test:live-canary | |
| - name: Test release cutover evidence generator | |
| run: npm run test:release-cutover-evidence | |
| - name: Check targeted JavaScript syntax | |
| run: npm run check:js | |
| - name: Run release compatibility tests | |
| run: npm run test:release-compat | |
| - name: Run release planner tests | |
| run: npm run test:release-plan | |
| - name: Run static deploy safety tests | |
| run: npm run test:static-deploy-safety | |
| - name: Run Cloudflare deploy prerequisite tests | |
| run: npm run test:cloudflare-prereqs | |
| - name: Run Cloudflare resource model tests | |
| run: npm run test:cloudflare-resource-model | |
| - name: Run readiness dossier tests | |
| run: npm run test:readiness-dossier | |
| - name: Run rollback drill tests | |
| run: npm run test:rollback-drill | |
| - name: Run Release Candidate manifest tests | |
| run: npm run test:release-rc | |
| - name: Run Release Candidate matrix tests | |
| run: npm run test:rc-check | |
| - name: Check Worker body parser guardrails | |
| run: npm run check:worker-body-parsers | |
| - name: Check admin activity query shape | |
| run: npm run check:admin-activity-query-shape | |
| - name: Check data lifecycle guardrails | |
| run: npm run check:data-lifecycle | |
| - name: Run doc currentness tests | |
| run: npm run test:doc-currentness | |
| - name: Check documentation currentness | |
| run: npm run check:doc-currentness | |
| - name: Run asset version tests | |
| run: npm run test:asset-version | |
| - name: Validate release compatibility | |
| run: npm run validate:release | |
| - name: Validate Cloudflare deploy prerequisites | |
| run: npm run validate:cloudflare-prereqs | |
| - name: Validate asset version sources | |
| run: npm run validate:asset-version | |
| worker-validation: | |
| needs: release-compatibility | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run worker route tests | |
| run: npm run test:workers | |
| # Single deploy job since we're just deploying | |
| deploy: | |
| needs: [release-compatibility, worker-validation] | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Check static deploy release-plan safety | |
| id: static_safety | |
| env: | |
| STATIC_DEPLOY_EVENT: ${{ github.event_name }} | |
| STATIC_DEPLOY_ACK: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_plan_dependency_acknowledgement || '' }} | |
| STATIC_DEPLOY_BASE_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_plan_base_ref || github.event.before }} | |
| STATIC_DEPLOY_HEAD_REF: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| npm run check:static-deploy-safety:github -- --base "${STATIC_DEPLOY_BASE_REF:-}" --head "${STATIC_DEPLOY_HEAD_REF:-}" --event-name "$STATIC_DEPLOY_EVENT" --acknowledgement "$STATIC_DEPLOY_ACK" | |
| - name: Report skipped static deploy | |
| if: steps.static_safety.outputs.static_deploy_skipped == 'true' | |
| run: | | |
| echo "Static deploy skipped because release plan requires non-static deploy steps first." | |
| echo "Workers, D1 migrations, and processor services are not deployed by the GitHub Pages workflow." | |
| - name: Setup Pages | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| uses: actions/configure-pages@v6 | |
| - name: Validate JS import paths | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: | | |
| errors=0 | |
| grep -rn "from ['\"]\.\.*/.*['\"]" --include="*.js" --include="*.html" --exclude-dir=node_modules --exclude-dir=workers --exclude-dir=.wrangler . | while IFS= read -r line; do | |
| file=$(echo "$line" | cut -d: -f1) | |
| # Extract the path between quotes after "from" | |
| import_path=$(echo "$line" | sed "s/.*from ['\"]//;s/['\"].*//" ) | |
| # Strip query params (?v=...) | |
| clean_path=$(echo "$import_path" | sed 's/?.*//') | |
| # Resolve relative to the importing file's directory | |
| dir=$(dirname "$file") | |
| resolved=$(cd "$dir" && realpath -m "$clean_path" 2>/dev/null || echo "$dir/$clean_path") | |
| if [ ! -f "$resolved" ]; then | |
| echo "ERROR: Broken import in $file" | |
| echo " import '$import_path' -> $resolved (not found)" | |
| echo "FAIL" >> /tmp/import_errors | |
| fi | |
| done | |
| if [ -f /tmp/import_errors ]; then | |
| count=$(wc -l < /tmp/import_errors) | |
| rm /tmp/import_errors | |
| echo "" | |
| echo "FAILED: $count broken import path(s) found" | |
| exit 1 | |
| fi | |
| echo "All JS import paths validated successfully" | |
| - name: Check target="_blank" links have rel="noopener" | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: | | |
| violations=$(grep -rn 'target="_blank"' --include="*.html" --include="*.js" --exclude-dir=node_modules --exclude-dir=workers --exclude-dir=_site --exclude-dir=.wrangler --exclude="*.min.js" . | grep -v 'noopener' || true) | |
| if [ -n "$violations" ]; then | |
| echo "ERROR: Found target=\"_blank\" without rel=\"noopener noreferrer\":" | |
| echo "$violations" | |
| echo "" | |
| echo "FAILED: Add rel=\"noopener noreferrer\" to all target=\"_blank\" links" | |
| exit 1 | |
| fi | |
| echo "All target=\"_blank\" links include rel=\"noopener\" — OK" | |
| - name: Validate local CSS/JS references | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: | | |
| # Check that CSS stylesheet and JS script references point to existing files | |
| # (Complements the JS import path validator above) | |
| grep -rn 'href="[^"]*\.css' --include="*.html" --exclude-dir=node_modules --exclude-dir=workers --exclude-dir=_site --exclude-dir=.wrangler . | while IFS= read -r line; do | |
| file=$(echo "$line" | cut -d: -f1) | |
| echo "$line" | grep -oE 'href="[^"]*\.css[^"]*"' | sed 's/href="//;s/"//' | while IFS= read -r ref; do | |
| case "$ref" in http://*|https://*|//*) continue ;; esac | |
| [ -z "$ref" ] && continue | |
| clean=$(echo "$ref" | sed 's/[?#].*//') | |
| dir=$(dirname "$file") | |
| resolved=$(cd "$dir" && realpath -m "$clean" 2>/dev/null || echo "$dir/$clean") | |
| if [ ! -f "$resolved" ]; then | |
| echo "ERROR: Missing CSS in $file" | |
| echo " href='$ref' -> $resolved (not found)" | |
| echo "FAIL" >> /tmp/asset_errors | |
| fi | |
| done | |
| done | |
| grep -rn 'src="[^"]*\.js' --include="*.html" --exclude-dir=node_modules --exclude-dir=workers --exclude-dir=_site --exclude-dir=.wrangler . | while IFS= read -r line; do | |
| file=$(echo "$line" | cut -d: -f1) | |
| echo "$line" | grep -oE 'src="[^"]*\.js[^"]*"' | sed 's/src="//;s/"//' | while IFS= read -r ref; do | |
| case "$ref" in http://*|https://*|//*) continue ;; esac | |
| [ -z "$ref" ] && continue | |
| clean=$(echo "$ref" | sed 's/[?#].*//') | |
| dir=$(dirname "$file") | |
| resolved=$(cd "$dir" && realpath -m "$clean" 2>/dev/null || echo "$dir/$clean") | |
| if [ ! -f "$resolved" ]; then | |
| echo "ERROR: Missing JS in $file" | |
| echo " src='$ref' -> $resolved (not found)" | |
| echo "FAIL" >> /tmp/asset_errors | |
| fi | |
| done | |
| done | |
| if [ -f /tmp/asset_errors ]; then | |
| count=$(wc -l < /tmp/asset_errors) | |
| rm /tmp/asset_errors | |
| echo "" | |
| echo "FAILED: $count broken CSS/JS reference(s) found" | |
| exit 1 | |
| fi | |
| echo "All local CSS/JS references validated successfully" | |
| - name: Validate page metadata | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: | | |
| errors=0 | |
| for f in index.html; do | |
| [ ! -f "$f" ] && continue | |
| if ! grep -q '<meta name="description"' "$f"; then | |
| echo "ERROR: Missing meta description in $f" | |
| errors=$((errors + 1)) | |
| fi | |
| if ! grep -q '<link rel="canonical"' "$f"; then | |
| echo "ERROR: Missing canonical link in $f" | |
| errors=$((errors + 1)) | |
| fi | |
| if ! grep -q 'og:title' "$f"; then | |
| echo "ERROR: Missing og:title in $f" | |
| errors=$((errors + 1)) | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "FAILED: $errors metadata error(s) found" | |
| exit 1 | |
| fi | |
| echo "Page metadata validated successfully" | |
| - name: Install Playwright Chromium | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: npx playwright install --with-deps chromium | |
| - name: Run smoke tests | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: npm run test:static | |
| - name: Upload Playwright report | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' && failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 7 | |
| - name: Prepare deployment | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| run: npm run build:static | |
| - name: Upload artifact | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| uses: actions/upload-pages-artifact@v4 | |
| with: | |
| path: '_site' | |
| - name: Deploy to GitHub Pages | |
| if: steps.static_safety.outputs.static_deploy_skipped != 'true' | |
| id: deployment | |
| uses: actions/deploy-pages@v5 |