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

terminal
$ 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

cli/commands/exempt.ts · 68 lines · 2.0 KB
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}`,
    ],
  };
}