Home/Docs/CI & automation

CI & automation

The scanner is built for CI: zero configuration, zero network (after an initial sync), machine-readable JSON output, and deterministic exit behaviour.

Machine output

shell
# once, to populate the local change database (the only networked command)
mendapi sync

# JSON to stdout, human summary suppressed
mendapi scan --json > impact.json

# or write the report directly
mendapi scan --out impact.json

# fixer JSON: one document per run, human summary moves to stderr
mendapi fix --from-report impact.json --json > fix.json

Every machine-readable report (scan --json, deps --json, fix --json, fix-report.json) carries a top-level schema_version integer (currently 1). It is bumped only on breaking JSON-shape changes — pin your parsers and agents to it instead of sniffing keys.

Fix verification evidence chain

Every fix report carries a verification block: before the report is written, each rewritten file is syntax-checked with node --check and the verdict ships inside the JSON. A reviewer — or an automated gate — cites these numbers mechanically instead of trusting the patch on faith. Files that node --check cannot parse (TypeScript, JSX) are recorded as skipped with a reason, never silently counted as passed.

shell
{
  "verification": {
    "syntax_check": {
      "tool": "node --check (v24.14.0)",
      "passed": 1,
      "failed": 0,
      "skipped": 0
    }
  },
  "files": [
    { "file": "lib/ai.js", "rules_applied": [ ... ], "syntax_check": { "status": "pass" } }
  ]
}

Each entry in files carries its own syntax_check verdict (pass / fail / skipped); a fail verdict also lands its first error line in reason. A dry-run never suppresses a failing patch — it emits the diff and the verdict together and writes a warning line per failed file to stderr, so the failure is loud and the evidence is preserved for review. Gate a pipeline on it directly:

shell
node -e "
  const r = require('./fix.json');
  const v = r.verification.syntax_check;
  if (v.failed > 0) {
    console.error(v.failed + ' rewritten file(s) failed node --check');
    process.exit(1);
  }
  console.error(v.passed + ' passed / ' + v.skipped + ' skipped — safe to review');
"

Second layer: repo checks after apply

Syntax checking proves the patch parses. It does not prove the patch left the repository's own tests green. When you run fix with both --apply and --run-checks, the fixer runs the target repo's own test and typecheck npm scripts against the rewritten files on disk and records a per-script verdict in verification.repo_checks. This is opt-in because running a repository's scripts executes third-party code; it is off unless you ask for it.

shell
{
  "verification": {
    "syntax_check": { "tool": "node --check (v24.14.0)", "passed": 1, "failed": 0, "skipped": 0 },
    "repo_checks": {
      "status": "ran",
      "passed": 1,
      "failed": 0,
      "checks": [
        {
          "script": "test",
          "command": "node test/smoke.js",
          "status": "pass",
          "exit_code": 0,
          "output_tail": "smoke: 3 file(s) parsed OK"
        }
      ]
    }
  }
}

Only test and typecheck scripts are run — never build, start, or anything with side effects beyond verification. The status field is honest about what actually happened: ran means the scripts executed (with passed / failed counts and a per-script verdict carrying its exit_code and the last lines of output in output_tail), while skipped ships a reason — you did not pass --run-checks, you passed it without --apply, the repo has no package.json, or it defines none of the whitelisted scripts. A skip is never dressed up as a pass. When a script fails the fixer writes a warning line per failed script to stderr, so the failure is loud. Gate on both layers together:

shell
node -e "
  const r = require('./fix.json');
  const s = r.verification.syntax_check;
  const c = r.verification.repo_checks;
  if (s.failed > 0) { console.error(s.failed + ' file(s) failed node --check'); process.exit(1); }
  if (c.status === 'ran' && c.failed > 0) {
    console.error(c.failed + ' repo check script(s) failed after apply');
    process.exit(1);
  }
  console.error('syntax ' + s.passed + ' passed; repo checks ' + c.status + ' — safe to review');
"

Exit codes

Automation should branch on exit codes where they carry signal, and on JSON where they do not:

  • scan0 on a successful scan even when impacts are found (a finding is a report, not a failure); 2 on usage errors. To gate a pipeline on findings, inspect both impacts.length and repairs_available.length in the JSON — see the gate example below.
  • fix0 changes made or previewed, 1 nothing applicable, 2 usage error, 3 stale pack refused without --ack-stale. Treat 1 as success in automation (clean repo), 3 as a hard stop for human review.
  • pr — never pushes without an explicit --push flag, regardless of exit code.

Failing the build on impact

scan deliberately exits 0 when impacts are found. If you want a red build, gate on the JSON:

shell
npx mendapi sync   # once per runner; skip if a prior step already synced
npx mendapi scan --json > impact.json
node -e "
  const r = require('./impact.json');
  if (r.schema_version !== 1) throw new Error('unexpected schema_version');
  const repairs = r.repairs_available || [];
  if (r.impacts.length > 0 || repairs.length > 0) {
    if (r.impacts.length > 0) console.error(r.impacts.length + ' breaking-change impact(s) found');
    for (const p of repairs) console.error('repair available: ' + p.pack + ' rewrites ' + p.files.length + ' file(s)');
    process.exit(1);
  }
"

Both arrays have to be read. They come from two independent corpora: impacts is the upstream change feed joined against your code, while repairs_available is the migration pack set replayed over your bytes. A pack can rewrite your files while the feed holds no matching change record, so a gate that reads only impacts passes a build the fixer could demonstrably repair.

Pre-commit hook

A local hook keeps API-impact drift out of the repo without waiting for CI. Zero network: scan reads the local change database, so run sync on your own schedule (cron, CI nightly), not inside the hook.

shell
# .git/hooks/pre-commit  (chmod +x)
#!/bin/sh
# Prerequisite: the change database must already exist locally.
# Run `npx mendapi sync` once (and on a schedule) outside this hook.
npx mendapi scan --json > /tmp/mendapi-impact.json || exit 0  # usage errors never block commits
node -e "
  const r = require('/tmp/mendapi-impact.json');
  const high = r.impacts.filter((i) => i.confidence === 'high');
  const repairs = r.repairs_available || [];
  if (high.length > 0 || repairs.length > 0) {
    if (high.length > 0) console.error('mendapi: ' + high.length + ' high-confidence API impact(s). Run: npx mendapi fix --from-report /tmp/mendapi-impact.json');
    for (const p of repairs) console.error('mendapi: repair available. Run: npx mendapi fix --migration ' + p.pack);
    process.exit(1);
  }
"

Gating only on high confidence keeps the hook quiet: medium findings surface in nightly CI where a human triages them, instead of blocking every commit. Entries in repairs_available are not a confidence tier and are never filtered out — each one has already been proved by replaying the pack's rewrite over the files on disk, so it names a patch you can generate right now.

GitHub Actions example

shell
name: api-impact
on:
  schedule:
    - cron: "0 6 * * *"   # nightly
  workflow_dispatch:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Fetch change feed
        run: npx mendapi sync
      - name: Scan for breaking-change impact
        run: npx mendapi scan --out impact.json
      - name: Preview fixes (dry-run, never writes to the repo)
        run: npx mendapi fix --from-report impact.json --out-dir fixes --json > fix.json || test $? -eq 1
      - name: Upload impact report and fix preview
        uses: actions/upload-artifact@v4
        with:
          name: mendapi-report
          path: |
            impact.json
            fix.json
            fixes/

The || test $? -eq 1 keeps the job green when nothing is applicable (fix exit 1 means "clean", not "failed") while still failing on usage errors (2) and stale packs (3). The uploaded fixes/ directory contains the unified diff a human can review and apply — or feed to mendapi pr on a trusted runner.

  • Nightly scheduled scan — upstream changes land on the provider's clock, not on your commit cadence. A snapshot at review time can be stale by deploy time.
  • Scan on dependency bumps — pair with Dependabot/Renovate PRs to see the API-level blast radius of an SDK version bump, not just the version delta.
  • Gate fixes behind reviewmendapi fix is dry-run by default and mendapi pr never pushes without --push. Keep it that way in automation: generate the diff, let a human merge.
Documentation generated from the mendapi codebase. Command output and pack listings reflect the shipped CLI exactly.