200 lines
13 KiB
TypeScript
200 lines
13 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { test } from "node:test";
|
|
|
|
const root = resolve(".");
|
|
const sha = "a".repeat(40);
|
|
const fixture = "synthetic-workflow-credential";
|
|
function workflow(name: string): string { return readFileSync(join(root, `.gitea/workflows/${name}.yml`), "utf8"); }
|
|
function step(name: string, title: string): string {
|
|
const value = workflow(name).split(" - name: ").find(block => block.startsWith(title + "\n"));
|
|
assert.ok(value, `Missing workflow step: ${title}`);
|
|
return value;
|
|
}
|
|
function script(name: string, title: string): string {
|
|
const block = step(name, title);
|
|
const run = block.split(" run: ")[1];
|
|
assert.ok(run, `Missing run body: ${title}`);
|
|
return run.startsWith("|\n") ? run.slice(2).split("\n").map(line => line.replace(/^ /, "")).join("\n") : run.trim();
|
|
}
|
|
function sandbox(body: (dir: string) => void): void {
|
|
const dir = mkdtempSync(join(tmpdir(), "sentinel-workflow-"));
|
|
try { body(dir); } finally { rmSync(dir, { recursive: true, force: true }); }
|
|
}
|
|
function fake(dir: string, command: string, source: string): void {
|
|
mkdirSync(join(dir, "bin"), { recursive: true });
|
|
writeFileSync(join(dir, "bin", command), `#!${process.execPath}\n${source}`, { mode: 0o700 });
|
|
}
|
|
function shell(dir: string, source: string, env: NodeJS.ProcessEnv = {}) {
|
|
return spawnSync("/bin/sh", ["-eu", "-c", source], { cwd: dir, encoding: "utf8", env: {
|
|
PATH: `${join(dir, "bin")}:${process.env.PATH}`, HOME: dir, GIT_CONFIG_NOSYSTEM: "1",
|
|
GIT_CONFIG_GLOBAL: "/dev/null", GITHUB_WORKSPACE: root, GITHUB_OUTPUT: join(dir, "output"), ...env,
|
|
} });
|
|
}
|
|
|
|
test("CI runs the full check and commitlint with inert SHA arguments and SHA-named artifacts", () => {
|
|
sandbox(dir => {
|
|
writeFileSync(join(dir, "build-args"), "");
|
|
writeFileSync(join(dir, "gradlew"), '#!/bin/sh\nprintf "%s\\n" "$@" > build-args\n', { mode: 0o700 });
|
|
assert.equal(shell(dir, script("ci", "Build and test")).status, 0);
|
|
assert.equal(readFileSync(join(dir, "build-args"), "utf8"), "clean\ncheck\njar\n");
|
|
fake(dir, "npx", `require('node:fs').writeFileSync('lint-args', JSON.stringify(process.argv.slice(2)));`);
|
|
const child = shell(dir, script("ci", "Validate conventional commits"), { BASE_SHA: "b".repeat(40), HEAD_SHA: sha });
|
|
assert.equal(child.status, 0, child.stderr);
|
|
const args = JSON.parse(readFileSync(join(dir, "lint-args"), "utf8"));
|
|
assert.deepEqual(args.slice(-6), ["--from", "b".repeat(40), "--to", sha, "--extends", "@commitlint/config-conventional"]);
|
|
assert.match(step("ci", "Validate conventional commits"), /if: github.event_name == 'pull_request'/);
|
|
assert.match(step("ci", "Upload development artifact"), /name: purpur-sentinel-\$\{\{ github.sha \}\}/);
|
|
assert.match(step("ci", "Upload development artifact"), /path: build\/libs\/purpur-sentinel-\*\.jar/);
|
|
assert.match(step("ci", "Upload development artifact"), /if-no-files-found: error/);
|
|
assert.match(workflow("ci"), /java-version: 25/);
|
|
assert.match(workflow("ci"), /node-version: 20/);
|
|
});
|
|
});
|
|
|
|
test("release performs full snapshot and versioned checks and gates versioned artifacts on a new release", () => sandbox(dir => {
|
|
writeFileSync(join(dir, "build-args"), "");
|
|
writeFileSync(join(dir, "gradlew"), '#!/bin/sh\nprintf "%s\\n" "$@" > build-args\n', { mode: 0o700 });
|
|
assert.equal(shell(dir, script("release", "Build and test before release")).status, 0);
|
|
assert.equal(readFileSync(join(dir, "build-args"), "utf8"), "clean\ncheck\njar\n");
|
|
assert.equal(shell(dir, script("release", "Verify versioned plugin and release tooling"), { VERSION: "1.2.3" }).status, 0);
|
|
assert.equal(readFileSync(join(dir, "build-args"), "utf8"), "clean\ncheck\njar\n-PreleaseVersion=1.2.3\n");
|
|
for (const title of ["Verify versioned plugin and release tooling", "Upload release workflow artifact"]) {
|
|
assert.match(step("release", title), /if: steps.release.outputs.created == 'true'/);
|
|
}
|
|
const release = workflow("release");
|
|
assert.ok(release.indexOf("Build and test before release") < release.indexOf("Compute and push"));
|
|
assert.ok(release.indexOf("Capture previous tag") < release.indexOf("Compute and push"));
|
|
assert.ok(release.indexOf("Verify versioned") < release.indexOf("Upload release workflow artifact"));
|
|
assert.match(release, /branches: \[main\]/);
|
|
assert.match(release, /group: sentinel-release\n cancel-in-progress: false/);
|
|
assert.match(step("release", "Upload release workflow artifact"), /path: build\/libs\/purpur-sentinel-\$\{\{ steps.release.outputs.version \}\}\.jar/);
|
|
}));
|
|
|
|
for (const scenario of ["new", "none", "unchanged", "wrong-source", "invalid-tag"] as const) {
|
|
test(`release resolution proves exact source and does not claim no-release publication: ${scenario}`, () => sandbox(dir => {
|
|
const init = shell(dir, "git init -q; git -c user.name=Fixture -c user.email=fixture@example.invalid commit --allow-empty -qm 'feat: fixture'");
|
|
assert.equal(init.status, 0, init.stderr);
|
|
if (scenario === "unchanged") assert.equal(shell(dir, "git tag v1.2.3").status, 0);
|
|
writeFileSync(join(dir, "output"), "");
|
|
const previous = shell(dir, script("release", "Capture previous tag"));
|
|
assert.equal(previous.status, 0, previous.stderr);
|
|
const previousOutput = readFileSync(join(dir, "output"), "utf8");
|
|
assert.equal(previousOutput, scenario === "unchanged" ? "tag=v1.2.3\n" : "tag=\n");
|
|
writeFileSync(join(dir, "output"), "");
|
|
if (scenario === "new" || scenario === "wrong-source") assert.equal(shell(dir, "git tag v1.2.3").status, 0);
|
|
if (scenario === "invalid-tag") assert.equal(shell(dir, "git tag v01.2.3-unsupported").status, 0);
|
|
const head = shell(dir, "git rev-parse HEAD").stdout.trim();
|
|
const child = shell(dir, script("release", "Resolve newly created exact-source release"), {
|
|
PREVIOUS_TAG: scenario === "unchanged" ? "v1.2.3" : "", EXPECTED_COMMIT: scenario === "wrong-source" ? sha : head,
|
|
});
|
|
const output = readFileSync(join(dir, "output"), "utf8");
|
|
if (scenario === "wrong-source" || scenario === "invalid-tag") {
|
|
assert.notEqual(child.status, 0);
|
|
assert.ok(!output.includes("created=true"));
|
|
} else {
|
|
assert.equal(child.status, 0, child.stderr);
|
|
assert.equal(output, scenario === "new" ? "created=true\nversion=1.2.3\n" : "created=false\n");
|
|
}
|
|
}));
|
|
}
|
|
|
|
const forbiddenEnv = [
|
|
"GIT_CREDENTIALS", "GH_TOKEN", "GITHUB_TOKEN", "GL_TOKEN", "GITLAB_TOKEN", "BB_TOKEN", "BITBUCKET_TOKEN",
|
|
"BB_TOKEN_BASIC_AUTH", "BITBUCKET_TOKEN_BASIC_AUTH", "DEBUG", "NODE_DEBUG", "GIT_TRACE", "GIT_TRACE_CURL",
|
|
"GIT_CURL_VERBOSE", "GIT_TRACE_PACKET", "GIT_TRACE_SETUP", "GIT_TRACE2", "GIT_TRACE2_EVENT", "GIT_TRACE2_PERF",
|
|
];
|
|
|
|
test("publication invokes only the in-process publisher with no secret argv or tracing", () => sandbox(dir => {
|
|
assert.equal(shell(dir, "git init -q; git -c user.name=Fixture -c user.email=fixture@example.invalid commit --allow-empty -qm 'feat: fixture'").status, 0);
|
|
writeFileSync(join(dir, "publish-command"), "null");
|
|
fake(dir, "node", `
|
|
const assert = require('node:assert/strict');
|
|
for (const key of ${JSON.stringify(forbiddenEnv)}) assert.equal(process.env[key], undefined, key);
|
|
assert.equal(process.env.RELEASE_CREDENTIAL, ${JSON.stringify(fixture)});
|
|
assert.match(process.env.RELEASE_NOTES, /feat: fixture/);
|
|
require('node:fs').writeFileSync('publish-command', JSON.stringify(process.argv.slice(2)));
|
|
`);
|
|
const child = shell(dir, script("release", "Publish verified JAR through a draft release"), {
|
|
RELEASE_CREDENTIAL: fixture, PREVIOUS_TAG: "",
|
|
...Object.fromEntries(forbiddenEnv.map(key => [key, key.includes("TOKEN") ? fixture : "1"])),
|
|
});
|
|
assert.equal(child.status, 0, child.stderr);
|
|
assert.deepEqual(JSON.parse(readFileSync(join(dir, "publish-command"), "utf8")), ["build/release-tools/publish-release.js"]);
|
|
assert.ok(![child.stdout, child.stderr].join("").includes(fixture));
|
|
assert.match(step("release", "Publish verified JAR through a draft release"), /if: steps.release.outputs.created == 'true'/);
|
|
assert.ok(workflow("release").indexOf("Verify versioned") < workflow("release").indexOf("Publish verified"));
|
|
assert.doesNotMatch(workflow("release"), /curl|secrets\.RELEASE_TOKEN[^\n]*(?:curl|git|node)/);
|
|
}));
|
|
|
|
test("semantic tagging keeps the dummy credential only in helper environment, never argv, URLs or diagnostics", () => {
|
|
sandbox(dir => {
|
|
assert.equal(shell(dir, "git init -q").status, 0);
|
|
writeFileSync(join(dir, "tag-command"), "null");
|
|
fake(dir, "npx", `
|
|
const assert = require('node:assert/strict');
|
|
for (const key of ${JSON.stringify(forbiddenEnv)}) assert.equal(process.env[key], undefined, key);
|
|
assert.equal(process.env.RELEASE_CREDENTIAL, ${JSON.stringify(fixture)});
|
|
assert.equal(process.env.GIT_TERMINAL_PROMPT, '0');
|
|
require('node:fs').writeFileSync('tag-command', JSON.stringify(process.argv.slice(2)));
|
|
`);
|
|
const child = shell(dir, script("release", "Compute and push semantic release tag"), {
|
|
RELEASE_CREDENTIAL: fixture, REPOSITORY: "dmg/purpur-sentinel",
|
|
...Object.fromEntries(forbiddenEnv.map(key => [key, key.includes("TOKEN") ? fixture : "1"])),
|
|
});
|
|
assert.equal(child.status, 0, child.stderr);
|
|
const args = JSON.parse(readFileSync(join(dir, "tag-command"), "utf8"));
|
|
assert.ok(Array.isArray(args), "Must actually invoke semantic-release");
|
|
assert.ok(args.includes("semantic-release@24.2.9"));
|
|
assert.ok(args.includes("@semantic-release/commit-analyzer@13.0.1"));
|
|
assert.ok(args.includes("@semantic-release/release-notes-generator@14.1.0"));
|
|
assert.equal(args[args.indexOf("--repository-url") + 1], "https://git.garvis.dev/dmg/purpur-sentinel.git");
|
|
const config = shell(dir, "git config --local --list");
|
|
assert.match(config.stdout, /credential.usehttppath=true/);
|
|
assert.match(config.stdout, /http.followredirects=false/);
|
|
assert.ok(config.stdout.includes(`credential.helper=${root}/scripts/git-credential.sh`));
|
|
assert.ok(![JSON.stringify(args), config.stdout, child.stdout, child.stderr].join("").includes(fixture));
|
|
});
|
|
});
|
|
|
|
for (const env of [{ RELEASE_CREDENTIAL: "", REPOSITORY: "dmg/purpur-sentinel" },
|
|
{ RELEASE_CREDENTIAL: fixture, REPOSITORY: "evil/purpur-sentinel" }]) {
|
|
test(`tagging refuses missing credential or wrong repository: ${env.REPOSITORY}`, () => sandbox(dir => {
|
|
fake(dir, "npx", "process.exit(90);");
|
|
const child = shell(dir, script("release", "Compute and push semantic release tag"), env);
|
|
assert.notEqual(child.status, 0);
|
|
assert.notEqual(child.status, 90);
|
|
assert.ok(!child.stderr.includes(fixture));
|
|
}));
|
|
}
|
|
|
|
for (const name of ["ci", "release"]) {
|
|
test(`${name} rejects multiline source SHAs before invoking Git`, () => sandbox(dir => {
|
|
writeFileSync(join(dir, "calls"), "");
|
|
fake(dir, "git", `require('node:fs').appendFileSync('calls', 'called');`);
|
|
const child = shell(dir, script(name, "Checkout public source without a credential"), { SOURCE_SHA: `${sha}\nmalformed` });
|
|
assert.notEqual(child.status, 0);
|
|
assert.equal(readFileSync(join(dir, "calls"), "utf8"), "");
|
|
}));
|
|
}
|
|
|
|
for (const name of ["ci", "release"]) test(`${name} anonymously fetches full history and exact source without credentials or redirects`, () => {
|
|
sandbox(dir => {
|
|
writeFileSync(join(dir, "calls"), "");
|
|
fake(dir, "git", `require('node:fs').appendFileSync('calls', JSON.stringify(process.argv.slice(2))+'\\n');`);
|
|
const child = shell(dir, script(name, "Checkout public source without a credential"), { SOURCE_SHA: sha });
|
|
assert.equal(child.status, 0, child.stderr);
|
|
const calls = readFileSync(join(dir, "calls"), "utf8").trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
|
|
assert.ok(calls.some(args => JSON.stringify(args) === JSON.stringify(["remote", "add", "origin", "https://git.garvis.dev/dmg/purpur-sentinel.git"])));
|
|
for (const [key, value] of [["credential.helper", ""], ["http.extraHeader", ""], ["http.followRedirects", "false"]]) {
|
|
assert.ok(calls.some(args => JSON.stringify(args) === JSON.stringify(["config", "--local", key, value])));
|
|
}
|
|
assert.ok(calls.some(args => JSON.stringify(args) === JSON.stringify(["fetch", "--force", "--tags", "origin", "+refs/heads/*:refs/remotes/origin/*"])));
|
|
assert.deepEqual(calls.at(-1), name === "ci" ? ["checkout", "--detach", sha] : ["checkout", "-B", "main", sha]);
|
|
assert.doesNotMatch(step(name, "Checkout public source without a credential"), /secrets\.|token:/);
|
|
});
|
|
});
|