157 行
5.2 KiB
JavaScript
157 行
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export class SourceProvenanceError extends Error {
|
|
constructor(code) {
|
|
super(code);
|
|
this.name = "SourceProvenanceError";
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
const ALLOWED_UNPUBLISHED_REASONS = new Set([
|
|
"offline-disaster-recovery",
|
|
"remote-outage-approved",
|
|
"controlled-local-recovery",
|
|
]);
|
|
|
|
export function deriveCandidateBuildEnv(commit) {
|
|
const normalized = String(commit ?? "").trim();
|
|
if (!/^[0-9a-f]{40,64}$/u.test(normalized)) {
|
|
throw new SourceProvenanceError("commit_invalid");
|
|
}
|
|
return Object.freeze({
|
|
BUILD_SHA: normalized,
|
|
VITE_BUILD_SHA: normalized.slice(0, 12),
|
|
});
|
|
}
|
|
|
|
function runGit(rootDir, args) {
|
|
return spawnSync("git", args, {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
}
|
|
|
|
function gitText(rootDir, args, failureCode) {
|
|
const result = runGit(rootDir, args);
|
|
if (result.status !== 0) {
|
|
throw new SourceProvenanceError(failureCode);
|
|
}
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
export function collectSourceProvenance({ rootDir, allowUnpublished = false }) {
|
|
const inside = gitText(rootDir, ["rev-parse", "--is-inside-work-tree"], "not_a_git_worktree");
|
|
if (inside !== "true") {
|
|
throw new SourceProvenanceError("not_a_git_worktree");
|
|
}
|
|
|
|
const dirty = gitText(
|
|
rootDir,
|
|
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
"git_status_failed",
|
|
);
|
|
if (dirty) {
|
|
throw new SourceProvenanceError("dirty_worktree");
|
|
}
|
|
|
|
const commit = gitText(rootDir, ["rev-parse", "--verify", "HEAD"], "commit_unavailable");
|
|
const buildEnv = deriveCandidateBuildEnv(commit);
|
|
const branch = gitText(rootDir, ["branch", "--show-current"], "branch_unavailable") || "detached";
|
|
const upstreamResult = runGit(rootDir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
const upstream = upstreamResult.status === 0 ? upstreamResult.stdout.trim() : null;
|
|
if (!upstream && !allowUnpublished) {
|
|
throw new SourceProvenanceError("upstream_missing");
|
|
}
|
|
|
|
const published = Boolean(upstream) && runGit(rootDir, ["merge-base", "--is-ancestor", commit, upstream]).status === 0;
|
|
if (!published && !allowUnpublished) {
|
|
throw new SourceProvenanceError("commit_not_in_upstream");
|
|
}
|
|
|
|
return {
|
|
status: "PASS",
|
|
commit: buildEnv.BUILD_SHA,
|
|
shortCommit: buildEnv.VITE_BUILD_SHA,
|
|
buildEnv,
|
|
branch,
|
|
upstream,
|
|
published,
|
|
overrideUsed: !published,
|
|
};
|
|
}
|
|
|
|
export function refreshUpstreamRef({ rootDir, allowFailure = false }) {
|
|
const branch = gitText(rootDir, ["branch", "--show-current"], "branch_unavailable");
|
|
const upstreamResult = runGit(rootDir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
const upstream = upstreamResult.status === 0 ? upstreamResult.stdout.trim() : null;
|
|
if (!upstream) {
|
|
if (allowFailure) {
|
|
return { refreshed: false, upstream: null, remote: null, code: "upstream_missing" };
|
|
}
|
|
throw new SourceProvenanceError("upstream_missing");
|
|
}
|
|
|
|
const remoteResult = runGit(rootDir, ["config", "--get", `branch.${branch}.remote`]);
|
|
const remote = remoteResult.status === 0 ? remoteResult.stdout.trim() : null;
|
|
if (!remote || remote === ".") {
|
|
if (allowFailure) {
|
|
return { refreshed: false, upstream, remote: null, code: "upstream_remote_missing" };
|
|
}
|
|
throw new SourceProvenanceError("upstream_remote_missing");
|
|
}
|
|
|
|
const fetchResult = runGit(rootDir, ["fetch", "--quiet", "--prune", remote]);
|
|
if (fetchResult.status !== 0) {
|
|
if (allowFailure) {
|
|
return { refreshed: false, upstream, remote, code: "upstream_refresh_failed" };
|
|
}
|
|
throw new SourceProvenanceError("upstream_refresh_failed");
|
|
}
|
|
return { refreshed: true, upstream, remote, code: null };
|
|
}
|
|
|
|
function parseRootArg(argv) {
|
|
const item = argv.find((value) => value.startsWith("--root="));
|
|
if (item) {
|
|
return path.resolve(item.slice("--root=".length));
|
|
}
|
|
return path.resolve(new URL("../..", import.meta.url).pathname);
|
|
}
|
|
|
|
function main() {
|
|
try {
|
|
const rootDir = parseRootArg(process.argv.slice(2));
|
|
const allowUnpublished = process.env.DEPLOY_ALLOW_UNPUBLISHED_COMMIT === "1";
|
|
const overrideReason = String(process.env.DEPLOY_UNPUBLISHED_REASON ?? "").trim();
|
|
if (allowUnpublished && !ALLOWED_UNPUBLISHED_REASONS.has(overrideReason)) {
|
|
throw new SourceProvenanceError("unpublished_override_reason_invalid");
|
|
}
|
|
|
|
collectSourceProvenance({ rootDir, allowUnpublished: true });
|
|
const refresh = refreshUpstreamRef({ rootDir, allowFailure: allowUnpublished });
|
|
const result = collectSourceProvenance({ rootDir, allowUnpublished });
|
|
console.log(JSON.stringify({
|
|
...result,
|
|
remoteVerified: refresh.refreshed,
|
|
upstreamRefreshCode: refresh.code,
|
|
overrideRequested: allowUnpublished,
|
|
overrideReason: allowUnpublished ? overrideReason : null,
|
|
}));
|
|
} catch (error) {
|
|
const code = error instanceof SourceProvenanceError ? error.code : "unexpected_error";
|
|
console.error(JSON.stringify({ status: "FAIL", code }));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (isMain) {
|
|
main();
|
|
}
|