Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/deploy-verify.yml
Original file line number Diff line number Diff line change
@@ -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 '<meta name="nodebench-build-sha" content="[0-9a-f]{40}"' | grep -oE '[0-9a-f]{40}' || true)"
if [ "$found" = "$EXPECTED_SHA" ]; then
echo "Live meta matches deployed commit: $found"
exit 0
fi
sleep 10
done
echo "::error::nodebench-build-sha never matched the deployed commit ($EXPECTED_SHA); last seen: '${found:-<none>}'" >&2
exit 1
45 changes: 45 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
/// <reference types="vitest" />

// 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 {
Expand Down Expand Up @@ -177,6 +221,7 @@ export default defineConfig(({ mode }) => {
envDir: __dirname,
plugins: [
react(),
buildIdentityMeta(),
// Service Worker + PWA for aggressive caching
VitePWA({
registerType: 'autoUpdate',
Expand Down
Loading