paperclip/server/scripts/write-build-stamp.mjs

88 lines
3.1 KiB
JavaScript

// Write the build stamp for the server.
//
// The server `build` script runs this after `tsc`. It writes the commit SHA
// into `dist/build-info.json`. The instrumentation module reads that stamp to
// report `service.version`, so the value tracks the true built commit.
//
// The build resolves the commit in two steps:
// 1. `git rev-parse --short HEAD` in the server directory.
// 2. The `PAPERCLIP_BUILD_COMMIT` environment variable.
// A Docker image build excludes `.git`, so the git lookup fails there. The
// image build passes the commit in `PAPERCLIP_BUILD_COMMIT` instead, so the
// stamp still records the true built commit.
//
// The build must not fail when no commit is available. A missing `git`, a
// checkout with no `.git`, and an unset `PAPERCLIP_BUILD_COMMIT` together write
// no stamp and exit 0.
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const serverDir = join(scriptDir, "..");
const distDir = join(serverDir, "dist");
const outFile = join(distDir, "build-info.json");
/**
* Resolve the commit for the build stamp. Prefer the git commit. Fall back to
* the supplied commit — the value a Docker image build passes in
* `PAPERCLIP_BUILD_COMMIT` when `.git` is absent. Return null when neither
* source gives a non-empty value.
*
* @param {unknown} gitCommit The `git rev-parse` result, or null on failure.
* @param {unknown} suppliedCommit The `PAPERCLIP_BUILD_COMMIT` value.
* @returns {string | null}
*/
export function resolveBuildCommit(gitCommit, suppliedCommit) {
const git = typeof gitCommit === "string" ? gitCommit.trim() : "";
if (git) return git;
const supplied = typeof suppliedCommit === "string" ? suppliedCommit.trim() : "";
if (supplied) return supplied;
return null;
}
/**
* Read the short commit SHA with `git rev-parse --short HEAD` in the server
* directory. Return the SHA, or null on any failure.
*
* @returns {string | null}
*/
function readGitCommit() {
try {
const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
cwd: serverDir,
stdio: ["ignore", "pipe", "ignore"],
})
.toString()
.trim();
return out.length > 0 ? out : null;
} catch {
return null;
}
}
/**
* Resolve the commit and write the build stamp. Write no stamp and return when
* no commit is available, so the build continues.
*/
function main() {
const commit = resolveBuildCommit(readGitCommit(), process.env.PAPERCLIP_BUILD_COMMIT);
if (!commit) {
console.log("[build-stamp] no commit available; wrote no build stamp");
return;
}
mkdirSync(distDir, { recursive: true });
writeFileSync(outFile, `${JSON.stringify({ commit }, null, 2)}\n`);
console.log(`[build-stamp] wrote ${outFile} commit=${commit}`);
}
// Run only when node invokes this file directly (the `build` script). A test
// that imports `resolveBuildCommit` does not run `main`.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}