Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 136 additions & 32 deletions scripts/actions-runner-assignment-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@ import {
collectRunnerAssignmentEvidence,
parseSelectedRunIds,
} from "./lib/actions-runner-assignment-source.mjs";
import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evidence.mjs";

const AUDITED_REPOSITORY = "ContextualWisdomLab/noema";
const GITHUB_API_VERSION = "2026-03-10";
const GH_API_TIMEOUT_MILLISECONDS = 20_000;
const GH_API_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
const REPORT_PATH = "artifacts/operations/actions-runner-assignment-audit.json";
const canonicalShaPattern = /^[0-9a-f]{40}$/;
const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });

const defaultGhRuntime = {
spawn_sync: spawnSync,
environment: process.env,
};

const defaultWriteIo = {
mkdirSync,
openSync,
writeFileSync,
closeSync,
renameSync,
unlinkSync,
randomUUID,
};

function boundedErrorText(value) {
const text = typeof value === "string" ? value : String(value ?? "");
Expand Down Expand Up @@ -66,20 +83,69 @@ export function createGhSubprocessEnvironment(environment) {
};
}

/**
* Decode and parse bounded GitHub API bytes without normalizing ambiguous input.
*
* Malformed UTF-8 and duplicate decoded object keys fail before `JSON.parse`, so
* runner-assignment evidence cannot inherit replacement-character or
* last-key-wins semantics from the JavaScript runtime.
*
* @param {Uint8Array} bytes Raw stdout bytes returned by the GitHub CLI.
* @returns {unknown} Parsed JSON evidence.
*/
export function parseGhJsonEvidence(bytes) {
if (!(bytes instanceof Uint8Array)) {
throw new TypeError("GitHub Actions evidence must be supplied as raw bytes.");
}

let text;
try {
text = fatalUtf8Decoder.decode(bytes);
} catch {
throw new Error("GitHub Actions evidence read returned invalid UTF-8.");
}

let duplicateKeys;
try {
duplicateKeys = hasDuplicateJsonObjectKeys(text);
} catch {
throw new Error("GitHub Actions evidence read returned malformed JSON.");
}
if (duplicateKeys) {
throw new Error("GitHub Actions evidence read returned duplicate decoded object keys.");
}

try {
return JSON.parse(text);
} catch {
throw new Error("GitHub Actions evidence read returned malformed JSON.");
}
}

/**
* Read one GitHub REST resource through the authenticated `gh` CLI.
*
* The caller supplies only repository-relative API paths. Pagination uses
* `--slurp` so every returned page remains explicit to the bounded source
* collector instead of being silently collapsed or truncated.
* collector instead of being silently collapsed or truncated. The optional
* runtime is an explicit test seam only; production callers use the pinned
* shell-free `spawnSync` runtime and least-authority environment.
*
* @param {string} path Repository-relative GitHub REST path.
* @param {{paginate?: boolean}} options Read options.
* @param {{spawn_sync?: Function, environment?: object}} runtime Process runtime.
* @returns {unknown} Parsed GitHub JSON evidence.
*/
export function ghApi(path, options = {}) {
export function ghApi(path, options = {}, runtime = defaultGhRuntime) {
if (typeof path !== "string" || path.length === 0 || path.length > 1000) {
throw new Error("GitHub API path is invalid.");
}
if (path.startsWith("/") || path.includes("..") || /[\u0000-\u001f\u007f]/.test(path)) {
throw new Error("GitHub API path is outside the bounded relative-path contract.");
}
if (!runtime || typeof runtime.spawn_sync !== "function") {
throw new Error("A shell-free GitHub CLI spawn runtime is required.");
}

const args = [
"api",
Expand All @@ -93,11 +159,10 @@ export function ghApi(path, options = {}) {
}
args.push(path);

const result = spawnSync("gh", args, {
encoding: "utf8",
const result = runtime.spawn_sync("gh", args, {
timeout: GH_API_TIMEOUT_MILLISECONDS,
maxBuffer: GH_API_MAX_BUFFER_BYTES,
env: createGhSubprocessEnvironment(process.env),
env: createGhSubprocessEnvironment(runtime.environment ?? process.env),
stdio: ["ignore", "pipe", "pipe"],
});

Expand All @@ -110,11 +175,7 @@ export function ghApi(path, options = {}) {
);
}

try {
return JSON.parse(result.stdout);
} catch {
throw new Error("GitHub Actions evidence read returned malformed JSON.");
}
return parseGhJsonEvidence(result.stdout);
}

/**
Expand Down Expand Up @@ -163,33 +224,43 @@ function parseQueueGrace(value) {
return parsed;
}

/** Write the fixed audit report atomically with owner-only temporary permissions. */
export function writeReportAtomically(report) {
/**
* Write the fixed audit report atomically with owner-only temporary permissions.
*
* The optional I/O seam permits deterministic failure testing without changing
* the production report path, file mode, atomic rename, or cleanup semantics.
*
* @param {unknown} report Bounded report value.
* @param {object} io File-system and UUID operations.
* @returns {string} Absolute report path.
*/
export function writeReportAtomically(report, io = defaultWriteIo) {
const reportPath = resolve(REPORT_PATH);
const reportDirectory = dirname(reportPath);
mkdirSync(reportDirectory, { recursive: true, mode: 0o700 });
const temporaryPath = `${reportPath}.tmp-${process.pid}-${randomUUID()}`;
io.mkdirSync(reportDirectory, { recursive: true, mode: 0o700 });
const temporaryPath = `${reportPath}.tmp-${process.pid}-${io.randomUUID()}`;
let descriptor;
try {
descriptor = openSync(temporaryPath, "wx", 0o600);
writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`, "utf8");
closeSync(descriptor);
descriptor = io.openSync(temporaryPath, "wx", 0o600);
io.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`, "utf8");
io.closeSync(descriptor);
descriptor = undefined;
renameSync(temporaryPath, reportPath);
io.renameSync(temporaryPath, reportPath);
} finally {
if (descriptor !== undefined) {
try {
closeSync(descriptor);
io.closeSync(descriptor);
} catch {
// Cleanup failures must not replace the original report-write failure.
}
}
try {
unlinkSync(temporaryPath);
io.unlinkSync(temporaryPath);
} catch {
// Cleanup failures must not replace the original report-write result.
}
}
return reportPath;
}

/**
Expand Down Expand Up @@ -270,21 +341,54 @@ export async function runActionsRunnerAssignmentAudit(input) {
};
}

async function main() {
/**
* Execute the CLI with injectable boundaries while preserving production defaults.
*
* @param {object} options Runtime overrides used only by tests/operators.
* @returns {Promise<{exit_code: number, report: object}>} Audit result.
*/
export async function main(options = {}) {
const result = await runActionsRunnerAssignmentAudit({
env: process.env,
observed_at: new Date().toISOString(),
gh_api: ghApi,
write_report: writeReportAtomically,
env: options.env ?? process.env,
observed_at: options.observed_at ?? new Date().toISOString(),
gh_api: options.gh_api ?? ghApi,
write_report: options.write_report ?? writeReportAtomically,
});
process.stdout.write(`${result.report.status}\n`);
process.exitCode = result.exit_code;
const writeOutput = options.write_output ?? ((value) => process.stdout.write(value));
const setExitCode = options.set_exit_code ?? ((code) => {
process.exitCode = code;
});
writeOutput(`${result.report.status}\n`);
setExitCode(result.exit_code);
return result;
}

const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
if (invokedPath === import.meta.url) {
main().catch((error) => {
process.stderr.write(`runner-assignment audit failed: ${boundedErrorText(error?.message)}\n`);
process.exitCode = 2;
/**
* Run a CLI promise with bounded error output and a distinct internal-error exit.
*
* @param {object} options Execution/output overrides.
* @returns {Promise<unknown>} Execution result, or undefined after a bounded error.
*/
export async function startCli(options = {}) {
const execute = options.execute ?? main;
const writeError = options.write_error ?? ((value) => process.stderr.write(value));
const setExitCode = options.set_exit_code ?? ((code) => {
process.exitCode = code;
});
try {
return await execute();
} catch (error) {
writeError(`runner-assignment audit failed: ${boundedErrorText(error?.message)}\n`);
setExitCode(2);
return undefined;
}
}

/** Execute a supplied CLI only when the module is the process entry point. */
export function runIfDirect(metaUrl, argv, execute) {
if (!argv[1] || metaUrl !== pathToFileURL(resolve(argv[1])).href) return false;
void execute();
return true;
}

runIfDirect(import.meta.url, process.argv, startCli);
Loading
Loading