Files
fusion/scripts/lib/release-authorization-gate.mjs
gsxdsm 81188f1996 FN-6480: require operator authorization for releases
Require an out-of-repository operator signal before the release script can mutate version, publish, push, or tag.

- Add a reusable release authorization gate that allows dry-runs and blocks real releases without FUSION_RELEASE_AUTHORIZED.
- Invoke the gate in scripts/release.mjs before the first release mutation while preserving dry-run behavior.
- Cover blocked, authorized, dry-run, whitespace, TTY, and call-order behavior with script tests.

Files changed:
 .../__tests__/release-authorization-gate.test.mjs  | 70 ++++++++++++++++++++++
 scripts/lib/release-authorization-gate.mjs         | 30 ++++++++++
 scripts/release.mjs                                | 26 +++++++-
 3 files changed, 123 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-6480

Fusion-Task-Lineage: 5347552c-e395-4852-b389-6bbdba0e044e
2026-06-16 15:08:24 -07:00

31 lines
1.5 KiB
JavaScript

export const RELEASE_AUTHORIZATION_ENV = "FUSION_RELEASE_AUTHORIZED";
/**
* FNXC:ReleaseScript 2026-06-15-02:41:
* FN-6469 proved that branch and working-tree preflight checks are not an authorization boundary because an agent can clone `main` into a fresh directory and rerun `pnpm release --yes`.
* Real releases are not agent-initiable: the publish path requires an explicit operator-held environment signal that is outside repo state and cannot be self-granted by reproducing `main`; dry-runs bypass this gate because they publish nothing.
*
* @param {{ dryRun: boolean, env?: Record<string, string | undefined>, stdinIsTTY?: boolean }} options
* @returns {{ authorized: boolean, mode: "dry-run-bypass" | "env-signal" | "blocked", reason?: string }}
*/
export function evaluateReleaseAuthorization({ dryRun, env = {}, stdinIsTTY = false }) {
if (dryRun === true) {
return { authorized: true, mode: "dry-run-bypass" };
}
const signal = env[RELEASE_AUTHORIZATION_ENV];
if (typeof signal === "string" && signal.trim() !== "") {
return { authorized: true, mode: "env-signal" };
}
const shellContext = stdinIsTTY
? "No operator authorization signal was present in this interactive shell."
: "No operator authorization signal was present in this non-interactive shell.";
return {
authorized: false,
mode: "blocked",
reason: `${shellContext} Real releases require explicit operator authorization via ${RELEASE_AUTHORIZATION_ENV}; aborted before version bump, publish, push, or tag.`,
};
}