179 行
7.4 KiB
JavaScript
179 行
7.4 KiB
JavaScript
const { execFile } = require("node:child_process");
|
|
const { mkdtemp, rm, writeFile } = require("node:fs/promises");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { promisify } = require("node:util");
|
|
const { pathToFileURL } = require("node:url");
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const ROOT_DIR = path.resolve(__dirname, "../../../..");
|
|
let provenance;
|
|
let repoDir;
|
|
|
|
async function git(args, cwd = repoDir) {
|
|
return execFileAsync("git", args, { cwd });
|
|
}
|
|
|
|
async function createCommit(message = "initial") {
|
|
await git(["add", "tracked.txt"]);
|
|
await git(["commit", "-m", message]);
|
|
}
|
|
|
|
describe("deployment source provenance", () => {
|
|
beforeAll(async () => {
|
|
const scriptPath = path.join(ROOT_DIR, "deploy/trialrun/check-source-provenance.mjs");
|
|
provenance = await import(pathToFileURL(scriptPath).href);
|
|
});
|
|
|
|
it("derives the complete candidate build environment as one immutable contract", () => {
|
|
const commit = "a".repeat(40);
|
|
const buildEnv = provenance.deriveCandidateBuildEnv(commit);
|
|
expect(Object.keys(buildEnv)).toEqual(["BUILD_SHA", "VITE_BUILD_SHA"]);
|
|
expect(buildEnv).toEqual({
|
|
BUILD_SHA: commit,
|
|
VITE_BUILD_SHA: commit.slice(0, 12),
|
|
});
|
|
expect(Object.isFrozen(buildEnv)).toBe(true);
|
|
expect(() => provenance.deriveCandidateBuildEnv("a".repeat(39))).toThrowError(
|
|
expect.objectContaining({ code: "commit_invalid" }),
|
|
);
|
|
expect(() => provenance.deriveCandidateBuildEnv("A".repeat(40))).toThrowError(
|
|
expect.objectContaining({ code: "commit_invalid" }),
|
|
);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
repoDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-test-"));
|
|
await git(["init", "--initial-branch=main"]);
|
|
await git(["config", "user.name", "Test User"]);
|
|
await git(["config", "user.email", "[email protected]"]);
|
|
await writeFile(path.join(repoDir, "tracked.txt"), "initial\n", "utf8");
|
|
await createCommit();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(repoDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("requires an upstream by default and permits an explicit unpublished override", () => {
|
|
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
|
expect.objectContaining({ code: "upstream_missing" }),
|
|
);
|
|
|
|
expect(provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toMatchObject({
|
|
status: "PASS",
|
|
published: false,
|
|
overrideUsed: true,
|
|
branch: "main",
|
|
});
|
|
});
|
|
|
|
it("rejects tracked and untracked working-tree changes before any deployment", async () => {
|
|
await writeFile(path.join(repoDir, "tracked.txt"), "changed\n", "utf8");
|
|
await writeFile(path.join(repoDir, "untracked.txt"), "untracked\n", "utf8");
|
|
|
|
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toThrowError(
|
|
expect.objectContaining({ code: "dirty_worktree" }),
|
|
);
|
|
});
|
|
|
|
it("accepts a clean commit that is contained in its upstream", async () => {
|
|
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
|
try {
|
|
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
|
await git(["remote", "add", "origin", remoteDir]);
|
|
await git(["push", "--set-upstream", "origin", "main"]);
|
|
|
|
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({
|
|
status: "PASS",
|
|
published: true,
|
|
overrideUsed: false,
|
|
upstream: "origin/main",
|
|
buildEnv: {
|
|
BUILD_SHA: expect.stringMatching(/^[0-9a-f]{40,64}$/u),
|
|
VITE_BUILD_SHA: expect.stringMatching(/^[0-9a-f]{12}$/u),
|
|
},
|
|
});
|
|
} finally {
|
|
await rm(remoteDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("rejects a clean local commit that has not reached upstream", async () => {
|
|
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
|
try {
|
|
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
|
await git(["remote", "add", "origin", remoteDir]);
|
|
await git(["push", "--set-upstream", "origin", "main"]);
|
|
await writeFile(path.join(repoDir, "tracked.txt"), "ahead\n", "utf8");
|
|
await createCommit("ahead");
|
|
|
|
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
|
expect.objectContaining({ code: "commit_not_in_upstream" }),
|
|
);
|
|
expect(provenance.collectSourceProvenance({ rootDir: repoDir, allowUnpublished: true })).toMatchObject({
|
|
published: false,
|
|
overrideUsed: true,
|
|
});
|
|
} finally {
|
|
await rm(remoteDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("refreshes the remote-tracking ref before deciding whether a commit is still published", async () => {
|
|
const remoteDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-remote-"));
|
|
const replacementDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-replacement-"));
|
|
try {
|
|
await execFileAsync("git", ["init", "--bare", remoteDir]);
|
|
await git(["remote", "add", "origin", remoteDir]);
|
|
await git(["push", "--set-upstream", "origin", "main"]);
|
|
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({ published: true });
|
|
|
|
await execFileAsync("git", ["init", "--initial-branch=main"], { cwd: replacementDir });
|
|
await execFileAsync("git", ["config", "user.name", "Replacement"], { cwd: replacementDir });
|
|
await execFileAsync("git", ["config", "user.email", "[email protected]"], { cwd: replacementDir });
|
|
await writeFile(path.join(replacementDir, "replacement.txt"), "replacement\n", "utf8");
|
|
await execFileAsync("git", ["add", "replacement.txt"], { cwd: replacementDir });
|
|
await execFileAsync("git", ["commit", "-m", "replacement"], { cwd: replacementDir });
|
|
await execFileAsync("git", ["remote", "add", "origin", remoteDir], { cwd: replacementDir });
|
|
await execFileAsync("git", ["push", "--force", "origin", "main"], { cwd: replacementDir });
|
|
|
|
expect(provenance.collectSourceProvenance({ rootDir: repoDir })).toMatchObject({ published: true });
|
|
expect(provenance.refreshUpstreamRef({ rootDir: repoDir })).toMatchObject({
|
|
refreshed: true,
|
|
upstream: "origin/main",
|
|
remote: "origin",
|
|
});
|
|
expect(() => provenance.collectSourceProvenance({ rootDir: repoDir })).toThrowError(
|
|
expect.objectContaining({ code: "commit_not_in_upstream" }),
|
|
);
|
|
} finally {
|
|
await rm(remoteDir, { recursive: true, force: true });
|
|
await rm(replacementDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("requires an allowlisted audit reason for the unpublished override", async () => {
|
|
const script = path.join(ROOT_DIR, "deploy/trialrun/check-source-provenance.mjs");
|
|
|
|
await expect(execFileAsync("node", [script, `--root=${repoDir}`], {
|
|
env: { ...process.env, DEPLOY_ALLOW_UNPUBLISHED_COMMIT: "1" },
|
|
})).rejects.toMatchObject({ code: 1 });
|
|
|
|
const { stdout } = await execFileAsync("node", [script, `--root=${repoDir}`], {
|
|
env: {
|
|
...process.env,
|
|
DEPLOY_ALLOW_UNPUBLISHED_COMMIT: "1",
|
|
DEPLOY_UNPUBLISHED_REASON: "offline-disaster-recovery",
|
|
},
|
|
});
|
|
expect(JSON.parse(stdout)).toMatchObject({
|
|
published: false,
|
|
remoteVerified: false,
|
|
overrideUsed: true,
|
|
overrideRequested: true,
|
|
overrideReason: "offline-disaster-recovery",
|
|
});
|
|
});
|
|
});
|