diff --git a/package.json b/package.json index d0940c86a6..03c72b2184 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "build": "node scripts/build/build-next-isolated.mjs", "build:secure": "OMNIROUTE_BUILD_PROFILE=minimal node scripts/build/build-next-isolated.mjs", "build:cli": "node --import tsx scripts/build/prepublish.ts", + "build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs", "start": "node scripts/dev/run-next.mjs start", "lint": "eslint .", "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"", diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index c166d3482c..3a89534572 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -29,6 +29,7 @@ export const APP_STAGING_REMOVAL_PATHS: string[] = [ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ ".env.example", + "BUILD_SHA", "docs/reference/openapi.yaml", "open-sse/mcp-server/server.js", "package.json", diff --git a/scripts/build/write-build-sha.mjs b/scripts/build/write-build-sha.mjs new file mode 100644 index 0000000000..a740b472c0 --- /dev/null +++ b/scripts/build/write-build-sha.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +/** + * write-build-sha.mjs — HEAD sentinel guard for OmniRoute release builds. + * + * Writes OMNIROUTE_BUILD_SHA (or git rev-parse --short HEAD) into: + * - dist/BUILD_SHA + * - .build/next/standalone/BUILD_SHA (present after `npm run build`) + * + * Exits 1 if the standalone directory does not exist, which guards against + * stale-cache shipping where a previous build artifact is accidentally published + * without a fresh `next build`. + * + * Usage: node scripts/build/write-build-sha.mjs + * Called by: npm run build:release (after npm run build) + */ + +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, "..", ".."); + +// Resolve the build SHA: prefer the env var (set by build:release script), fall +// back to running git rev-parse. +function resolveBuildSha() { + if (process.env.OMNIROUTE_BUILD_SHA) { + return process.env.OMNIROUTE_BUILD_SHA.trim(); + } + try { + return execFileSync("git", ["rev-parse", "--short", "HEAD"], { + cwd: ROOT, + encoding: "utf8", + }).trim(); + } catch (err) { + console.error("[write-build-sha] Could not determine build SHA:", err.message); + process.exit(1); + } +} + +const sha = resolveBuildSha(); +const NEXT_DIST = process.env.NEXT_DIST_DIR || ".build/next"; +const standaloneDir = path.join(ROOT, NEXT_DIST, "standalone"); +const distDir = path.join(ROOT, "dist"); + +// Guard: standalone must exist (ensures build:release runs after npm run build) +if (!fs.existsSync(standaloneDir)) { + console.error( + `[write-build-sha] FATAL: standalone dir not found: ${standaloneDir}\n` + + ` Run \`npm run build\` before \`npm run build:release\`.` + ); + process.exit(1); +} + +// Write sentinel to the standalone dir (Docker/dev path) +const standaloneSentinel = path.join(standaloneDir, "BUILD_SHA"); +fs.writeFileSync(standaloneSentinel, sha + "\n"); +console.log(`[write-build-sha] Written ${sha} -> ${path.relative(ROOT, standaloneSentinel)}`); + +// Write sentinel to dist/ (npm publish path) if it exists +if (fs.existsSync(distDir)) { + const distSentinel = path.join(distDir, "BUILD_SHA"); + fs.writeFileSync(distSentinel, sha + "\n"); + console.log(`[write-build-sha] Written ${sha} -> ${path.relative(ROOT, distSentinel)}`); +} + +console.log(`[write-build-sha] Build SHA: ${sha}`);