Remove the self-grantable FUSION_RELEASE_AUTHORIZED env signal and replace it with an interactive prompt: a real release now requires a live human to type "authorized" at a TTY. Releases can no longer run non-interactively (no TTY is blocked outright), and --yes does not bypass the typed phrase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.4 KiB
JavaScript
73 lines
3.4 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { URL } from "node:url";
|
|
|
|
import {
|
|
evaluateReleaseAuthorization,
|
|
isReleaseAuthorizationPhrase,
|
|
RELEASE_AUTHORIZATION_PHRASE,
|
|
} from "../lib/release-authorization-gate.mjs";
|
|
|
|
test("gate blocks a real release run non-interactively (no TTY)", () => {
|
|
const result = evaluateReleaseAuthorization({ dryRun: false, stdinIsTTY: false });
|
|
|
|
assert.equal(result.authorized, false);
|
|
assert.equal(result.mode, "blocked");
|
|
assert.match(result.reason ?? "", /non-interactively/);
|
|
assert.match(result.reason ?? "", /aborted before version bump, publish, push, or tag/);
|
|
});
|
|
|
|
test("interactive real release requires the typed authorization phrase", () => {
|
|
const result = evaluateReleaseAuthorization({ dryRun: false, stdinIsTTY: true });
|
|
|
|
assert.deepEqual(result, { authorized: false, mode: "requires-confirmation" });
|
|
});
|
|
|
|
test("dry-run bypasses authorization because it publishes nothing", () => {
|
|
const result = evaluateReleaseAuthorization({ dryRun: true, stdinIsTTY: false });
|
|
|
|
assert.deepEqual(result, { authorized: true, mode: "dry-run-bypass" });
|
|
});
|
|
|
|
test("only the exact authorization phrase passes; anything else fails closed", () => {
|
|
assert.equal(isReleaseAuthorizationPhrase(RELEASE_AUTHORIZATION_PHRASE), true);
|
|
assert.equal(isReleaseAuthorizationPhrase(" Authorized "), true);
|
|
assert.equal(isReleaseAuthorizationPhrase("AUTHORIZED\n"), true);
|
|
|
|
for (const value of ["", " ", "yes", "y", "authorize", "authorized now", undefined, null]) {
|
|
assert.equal(
|
|
isReleaseAuthorizationPhrase(value),
|
|
false,
|
|
`expected ${JSON.stringify(value)} to be rejected`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("release script prompts for the authorization phrase before the first mutation", () => {
|
|
const source = readFileSync(new URL("../release.mjs", import.meta.url), "utf8");
|
|
const importIndex = source.indexOf("./lib/release-authorization-gate.mjs");
|
|
const dryRunExitIndex = source.indexOf("if (DRY_RUN) {");
|
|
const gateIndex = source.indexOf("evaluateReleaseAuthorization({");
|
|
const phraseCheckIndex = source.indexOf("isReleaseAuthorizationPhrase(");
|
|
const versionBumpIndex = source.indexOf("run(\"pnpm release:version\")");
|
|
|
|
assert.notEqual(importIndex, -1, "release.mjs should import the authorization helper");
|
|
assert.notEqual(dryRunExitIndex, -1, "release.mjs should retain the dry-run early exit");
|
|
assert.notEqual(gateIndex, -1, "release.mjs should call evaluateReleaseAuthorization()");
|
|
assert.notEqual(phraseCheckIndex, -1, "release.mjs should validate the typed authorization phrase");
|
|
assert.notEqual(versionBumpIndex, -1, "release.mjs should still run the version bump after gates");
|
|
assert.ok(dryRunExitIndex < gateIndex, "dry-run must exit before the authorization gate call site");
|
|
assert.ok(gateIndex < phraseCheckIndex, "the gate decision must precede the typed-phrase check");
|
|
assert.ok(phraseCheckIndex < versionBumpIndex, "authorization must be checked before the first mutation");
|
|
});
|
|
|
|
test("env vars no longer influence release authorization", () => {
|
|
const source = readFileSync(
|
|
new URL("../lib/release-authorization-gate.mjs", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.ok(!/FUSION_RELEASE_AUTHORIZED/.test(source), "the env signal must be fully removed");
|
|
assert.ok(!/process\.env/.test(source), "the gate must not read process env");
|
|
});
|