redline exempt
Decides whether a pull request carries a valid exemption for a failing process check — a reason, a scope and an expiry, not a bare label.
What this is
Decides whether a pull request carries a valid exemption for a failing process check. The gate calls it; you rarely will. It exists because `redline-exempt` was a bare label that recorded nothing — not who accepted the failing check, not why, not until when.
How to onboard it
Nothing to install. The gate invokes it when a soft-fail label is present and a process check has failed, and the pull request template carries the `## Redline exemption` section an author fills in. A repository moves from `warn` to `require` one standards version after the block was introduced, so nobody's open pull request is failed by a rule that did not exist when they opened it.
How to use it
$ npx redlinegate@latest exempt --body-file pr-body.md # is there a valid exemption at all? $ npx redlinegate@latest exempt --body-file pr-body.md --scope adr # does it cover this check?
The flags that change behaviour materially:
--body-file <path>— The pull request body, as a file. A file rather than an argument on purpose: a pull request body is attacker-controlled text full of backticks and $(...), and anyone who can open a pull request can write it — interpolating that into a command is how a body becomes a command.--scope <check>— The failing check the exemption is being asked to cover. An exemption scoped to `checklist` does not silently cover `adr`; omitting `scope:` in the block covers both, which is what the bare label meant implicitly.
Expected output
Exit 0 and a line naming the expiry, scope and reason when a valid exemption applies. Exit 1 and the specific problem when it does not — no block at all, a reason under 20 characters, a missing or unparseable expiry, an expiry in the past, an expiry more than 90 days out, or a scope that does not cover the failing check. Exit 1, not 2: a pull request without a valid exemption is a normal answer the gate acts on, not the caller misusing the command.
How to edit it
cli/exempt/parse.ts holds the parse; scripts/lib/exemptions.mjs mirrors it for the collector, which runs in the metrics repo with no build step to import from. scripts/validate.mjs fails the build if the two diverge — the failure it guards is the gate accepting a block the audit cannot read, which is exactly the state this piece exists to end.
Run npm test and npm run typecheck before pushing: the command surface is unit-tested against a fake host client, so a behaviour change shows up as a failing assertion rather than as a surprise on someone's repository.
The full file
import { readFileSync } from 'node:fs';
import { RedlineError } from '../core/errors.ts';
import { covers, parseExemption, type Exemption } from '../exempt/parse.ts';
export interface ExemptOptions {
// Path to a file holding the pull request body. A file rather than an
// argument: a pull request body contains newlines, quotes and backticks, and
// passing it through a shell argument is how a body with a backtick becomes a
// command.
bodyFile: string;
// The failing check this exemption is being asked to cover.
scope?: string;
now?: Date;
}
export interface ExemptReport {
exemption: Exemption | null;
applies: boolean;
messages: string[];
}
// Decide whether a pull request carries a valid exemption for a named check.
//
// The gate calls this instead of re-implementing the parse in shell. Two
// independent readings of the same block would eventually disagree, and the
// disagreement would be a pull request the gate exempts and the audit does not —
// or worse, the reverse.
export function exempt(opts: ExemptOptions): ExemptReport {
let body: string;
try {
body = readFileSync(opts.bodyFile, 'utf8');
} catch (error) {
throw new RedlineError(
'usage',
`cannot read the pull request body from ${opts.bodyFile}: ${error instanceof Error ? error.message : String(error)}`
);
}
const { exemption, problems } = parseExemption(body, opts.now ?? new Date());
if (!exemption) {
return {
exemption: null,
applies: false,
messages: problems.map((p) => p.detail),
};
}
const scope = opts.scope;
if (scope && !covers(exemption, scope)) {
return {
exemption,
applies: false,
messages: [
`the exemption covers ${exemption.scope.join(', ')} — it does not cover "${scope}". ` +
'Widen its scope deliberately, or fix the check.',
],
};
}
return {
exemption,
applies: true,
messages: [
`exempt until ${exemption.until} (${exemption.scope.join(', ')}): ${exemption.reason}`,
],
};
}