Skip to content
Merged
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
42 changes: 42 additions & 0 deletions .github/workflows/deploy-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Deploy verify

# Vercel's GitHub integration posts deployment_status events with no secrets needed to read
# them. This repo's own deployments (checked via
# `gh api repos/HomenShum/NodeProof/deployments?per_page=10` and each deployment's
# `/statuses`) show every production deploy's environment as the bare string "Production" —
# no sibling Vercel project shares this repo, so no name-contains filter is needed here
# (unlike NodeVoice's deploy-verify.yml, which does share a repo with local-collab-mvp).
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://proofloop.live/
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="proofloop-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::proofloop-build-sha never matched the deployed commit ($EXPECTED_SHA); last seen: '${found:-<none>}'" >&2
exit 1
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"doctor": "npm run proofloop:doctor",
"check": "npm test && npm audit --omit=dev",
"proof": "npm run proofloop:maturity",
"build": "tsc -p tsconfig.json",
"build": "tsc -p tsconfig.json && node scripts/stamp-build-sha.mjs",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"test": "vitest run",
Expand Down
60 changes: 60 additions & 0 deletions scripts/stamp-build-sha.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Stamps exactly one <meta name="proofloop-build-sha" ...> into public/index.html at build
* time, adapted from node-foyer's vite.config.ts foyer-build-sha plugin (there is no bundler
* here — public/ is a static Vercel deploy — so this runs as a `npm run build` step instead
* of a Vite transform).
*
* Precedence: VERCEL_GIT_COMMIT_SHA, then GITHUB_SHA, then `git rev-parse HEAD`, else
* "unavailable" (non-strict, matching NodeVoice's build-sha plugin: no gate here depends on
* this tag existing, so a git-less build environment ships "unavailable" rather than failing).
*
* Idempotent: strips any previously stamped tag before inserting the new one, so re-running
* `npm run build` (as `pretest` does) never produces a duplicate.
*/
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
const INDEX_HTML = resolve(ROOT, "public", "index.html");
const BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/u;
const META_TAG_RE = /\n?\s*<meta name="proofloop-build-sha"[^>]*>\r?\n?/gu;

export function resolveBuildSha() {
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,
cwd: ROOT,
}).trim();
if (BUILD_SHA_PATTERN.test(sha)) return sha;
} catch {
// fall through to unavailable
}
return "unavailable";
}

export function stamp(sha, html) {
const provenance = sha === "unavailable" ? "unavailable" : "commit";
const tag = ` <meta name="proofloop-build-sha" content="${sha}" data-provenance="${provenance}" />\n`;
const stripped = html.replace(META_TAG_RE, "\n");
return stripped.replace("</head>", `${tag} </head>`);
}

function main() {
const sha = resolveBuildSha();
const html = readFileSync(INDEX_HTML, "utf8");
writeFileSync(INDEX_HTML, stamp(sha, html));
console.log(`stamp-build-sha: wrote proofloop-build-sha=${sha} to public/index.html`);
}

const invokedDirectly = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
if (invokedDirectly) {
main();
}
28 changes: 28 additions & 0 deletions tests/stampBuildSha.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { stamp } from "../scripts/stamp-build-sha.mjs";

const HEAD = "<!doctype html>\n<html>\n <head>\n <title>x</title>\n </head>\n <body></body>\n</html>\n";

describe("stamp-build-sha", () => {
it("injects exactly one meta tag with the given sha", () => {
const sha = "e45f90f692c59f4f86dd8a4343d42b9e1c03bd0d";
const out = stamp(sha, HEAD);
const matches = out.match(/<meta name="proofloop-build-sha"[^>]*>/g) ?? [];
expect(matches).toHaveLength(1);
expect(matches[0]).toContain(`content="${sha}"`);
expect(matches[0]).toContain('data-provenance="commit"');
});

it("marks an unresolved sha as unavailable provenance", () => {
const out = stamp("unavailable", HEAD);
expect(out).toContain('content="unavailable" data-provenance="unavailable"');
});

it("is idempotent: re-stamping never duplicates the tag", () => {
const once = stamp("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", HEAD);
const twice = stamp("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", once);
const matches = twice.match(/<meta name="proofloop-build-sha"[^>]*>/g) ?? [];
expect(matches).toHaveLength(1);
expect(matches[0]).toContain('content="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"');
});
});
Loading