From 11fe24e1d2593d90288efe63009768c24a23c781 Mon Sep 17 00:00:00 2001 From: hshum Date: Sat, 12 Sep 2026 18:08:20 -0700 Subject: [PATCH] feat(build): nodebench-build-sha meta + post-deploy readback Copies NodeVoice PR #10's pattern for the Node Foyer identity lane (FOYER-V3, worker identity-nodebench): a Vite plugin stamps exactly one into the built apps/web/index.html, with precedence VERCEL_GIT_COMMIT_SHA -> GITHUB_SHA -> `git rev-parse HEAD`, falling back to content="unavailable" data-provenance="unavailable" rather than throwing. Adds .github/workflows/deploy-verify.yml (on: deployment_status only, no schedule, no workflow_dispatch, no rollback step) that polls https://www.nodebenchai.com/ for up to 3 minutes after a Production deployment_status success and fails if the live meta never equals github.event.deployment.sha. KNOWN HAZARD: this repo's existing .github/workflows/post-deploy-verify.yml runs `npx vercel rollback --yes` on a failed Production deployment_status event and also runs on a 6-hourly schedule. deploy-verify.yml is a separate workflow file triggered only by deployment_status (never the schedule) and carries no rollback step, so it cannot reach that path in either direction. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/deploy-verify.yml | 51 +++++++++++++++++++++++++++++ vite.config.ts | 45 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 .github/workflows/deploy-verify.yml diff --git a/.github/workflows/deploy-verify.yml b/.github/workflows/deploy-verify.yml new file mode 100644 index 000000000..a49c08f71 --- /dev/null +++ b/.github/workflows/deploy-verify.yml @@ -0,0 +1,51 @@ +name: Deploy verify + +# Node Foyer identity lane (copies NodeVoice PR #10's pattern). Vercel's GitHub +# integration posts deployment_status events with no secrets needed to read them. +# `gh api repos/HomenShum/NodeBenchAI/deployments?per_page=5` shows this repo's +# production environment name is the bare string "Production" (a single Vercel +# project, nodebench-ai, deploys from this repo — unlike NodeVoice's two-project +# case, no environment-name disambiguation is needed here). +# +# KNOWN HAZARD (FOYER-V3 council ruling): .github/workflows/post-deploy-verify.yml +# in this same repo runs `npx vercel rollback --yes` on a failed PRODUCTION +# deployment_status event, and also runs on a 6-hourly schedule. This workflow is +# a separate file with `on: deployment_status` only (no `schedule:`, no +# `workflow_dispatch:`) and has no rollback step at all, so it can never reach +# post-deploy-verify.yml's rollback path in either direction: this workflow never +# fires from the schedule (it isn't triggered by one), and a failure here has no +# rollback step to run. The two workflows run independently on the same event. +on: + deployment_status: + +permissions: + contents: read + deployments: read + +jobs: + verify-live-identity: + if: >- + github.event.deployment_status.state == 'success' && + github.event.deployment_status.environment == 'Production' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Poll the live meta tag for the deployed commit sha + env: + EXPECTED_SHA: ${{ github.event.deployment.sha }} + LIVE_URL: https://www.nodebenchai.com/ + run: | + set -euo pipefail + deadline=$((SECONDS + 180)) + found="" + while [ "$SECONDS" -lt "$deadline" ]; do + body="$(curl -fsS --max-time 10 "$LIVE_URL" || true)" + found="$(printf '%s' "$body" | grep -oE '}'" >&2 + exit 1 diff --git a/vite.config.ts b/vite.config.ts index ada9664f4..3678c7e28 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,52 @@ import { visualizer } from "rollup-plugin-visualizer"; import { imagetools } from "vite-imagetools"; import { VitePWA } from "vite-plugin-pwa"; import Critters from "critters"; +import { execFileSync } from "node:child_process"; /// +// Build identity, adapted from node-foyer's vite.config.ts (foyer-build-sha) via the +// NodeVoice PR #10 pattern. Non-strict: falls back to "unavailable" rather than throwing +// when no signal is available. +const BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/u; + +function resolveBuildSha(): string { + for (const value of [process.env.VERCEL_GIT_COMMIT_SHA, process.env.GITHUB_SHA]) { + const sha = value?.trim().toLowerCase(); + if (sha && BUILD_SHA_PATTERN.test(sha)) return sha; + } + try { + const sha = execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", + timeout: 5_000, + windowsHide: true, + }).trim(); + if (BUILD_SHA_PATTERN.test(sha)) return sha; + } catch { + // fall through to unavailable + } + return "unavailable"; +} + +function buildIdentityMeta(): Plugin { + return { + name: "nodebench-build-identity", + transformIndexHtml() { + const sha = resolveBuildSha(); + return [ + { + tag: "meta", + attrs: { + name: "nodebench-build-sha", + content: sha, + "data-provenance": sha === "unavailable" ? "unavailable" : "commit", + }, + injectTo: "head" as const, + }, + ]; + }, + }; +} + // Critical CSS plugin using Critters function criticalCSSPlugin(): Plugin { return { @@ -177,6 +221,7 @@ export default defineConfig(({ mode }) => { envDir: __dirname, plugins: [ react(), + buildIdentityMeta(), // Service Worker + PWA for aggressive caching VitePWA({ registerType: 'autoUpdate',