CI integration

CI catches changed-code risk, cleanup opportunities, duplication, and complexity issues that get past agent workflows and editor review.

For TypeScript rollout, start with type-aware analysis in best-effort mode and inspect _meta.type_aware in JSON output. Switch to complete only when partial or unavailable semantic evidence must fail the job. Type-aware analysis adds a slower semantic pass. See Type-aware TypeScript analysis.

  1. Add the action

    Add fallow to your workflow file:

    name: Fallow analysis
    on: [push, pull_request]
    
    jobs:
      fallow:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: fallow-rs/fallow@v3
            with:
              format: sarif

    This runs all analyses (dead code + duplication + complexity) by default. Use the command input to run a specific analysis.

    When you enable comment: true or review-comments: true, grant the job contents: read, id-token: write, pull-requests: write, and checks: write. The identity permission lets the Action obtain a short-lived Fallow GitHub App token, so feedback is authored by fallow-cloud[bot]. Without id-token: write, analysis and posting continue with github-actions[bot].

    permissions:
      contents: read
      id-token: write
      pull-requests: write
      checks: write
  2. Configure inputs

    Customize the action with these inputs:

    InputDefaultDescription
    command-- (all)Command to run (dead-code, dupes, health, audit, security, fix, or empty for all). Legacy alias: check = dead-code.
    root.Project root directory
    config--Path to config file (.fallowrc.json, .fallowrc.jsonc, fallow.toml, or .fallow.toml)
    formatsarifOutput format
    productionfalseEnable production mode for every analysis
    production-dead-codefalseCombined mode only: per-analysis production mode for dead-code
    production-healthfalseCombined mode only: per-analysis production mode for health
    production-dupesfalseCombined mode only: per-analysis production mode for duplication
    fail-on-issuestrueExit with code 1 if issues are found
    changed-since--Only check files changed since this ref
    auto-changed-sincetrueAutomatically scope to changed files in PR context using base SHA. Ignored when changed-since is set.
    baseline--Path to baseline file for comparison. Rejected with exit 2 when command: audit; use dead-code-baseline / health-baseline / dupes-baseline instead.
    save-baseline--Save current results as a baseline file. Rejected with exit 2 when command: audit (audit runs three analyses with incompatible baseline formats).
    version--Fallow version override. When omitted, the action uses the project package.json fallow dependency spec if present, otherwise latest. The Action ref and the fallow CLI version are independent: uses: fallow-rs/fallow@v3 selects the Action wrapper code only, while the installed CLI resolves from this input, then the project package.json fallow dependency, then latest.
    workspace--Scope output to one or more workspaces (exact names, globs, ! negation; comma-separated)
    changed-workspaces--Git-derived monorepo scoping: scope to workspaces containing any file changed since REF (e.g. origin/main). Requires fetch-depth: 0. Mutually exclusive with workspace. A missing ref is a hard error (exit 2) rather than silent full-scope fallback.
    commentfalsePost results as a PR comment
    review-commentsfalsePost inline PR review comments with typed review-github output and reconcile resolved threads on later runs
    review-guidancefalseAdd collapsed "What to do" guidance blocks to inline PR review comments. Requires review-comments: true
    review-id--Stable identifier (1-64 characters, A-Za-z0-9._-) that isolates inline review comments when multiple fallow review jobs target the same pull request. See Isolating parallel review jobs.
    annotationstrueEmit findings as inline PR annotations via workflow commands (no Advanced Security required). On fallow >= 3.4.2 the action renders these natively via fallow report --format github-annotations; older binaries fall back to the bundled jq renderers, and version: latest users flip to the native path silently. The step log line fallow: annotations rendered via native github-annotations (or via jq fallback) tells you which renderer ran. The job summary follows the same rule with github-summary.
    max-annotations50Maximum number of inline annotations to emit. On the native path the cap is applied to the rendered stream (GitHub itself displays at most 10 annotations per type per step).
    github-token$\{\{ github.token \}\}GitHub token for PR comments and SARIF upload
    branded-tokentrueUse a short-lived Fallow GitHub App token for comments and reviews when the workflow grants id-token: write. Falls back to github-token when unavailable.
    broker-urlhttps://api.fallow.cloudFallow token broker used to exchange the workflow identity for the branded app token.
    dupes-modemildDetection mode for dupes command
    min-tokens--Minimum token count for a clone (dupes command)
    min-lines--Minimum line count for a clone (dupes command)
    threshold--Fail if duplication exceeds this % (dupes command)
    skip-localfalseOnly report cross-directory duplicates (dupes command)
    scorefalseCompute health score (0-100 with letter grade). Enables the health delta header in PR comments (health and bare command)
    trendfalseCompare current metrics against the most recent saved snapshot. Implies score (health and bare command)
    save-snapshot--Save vital signs snapshot for trend tracking. Set to true for default path or provide a custom path (health and bare command)
    type-awarefalseEnable optional project-wide TypeScript semantic evidence. Does not emit compiler diagnostics or generic typed lint findings.
    type-aware-projects""Comma-separated tsconfig paths. Empty uses automatic project discovery.
    type-aware-requirebest-effortUse complete to fail when semantic analysis is partial or unavailable, including valid zero-finding runs.
    dry-runtruePreview changes without modifying files (fix command)
    coverage--Path to Istanbul coverage-final.json for accurate per-function CRAP scores (health and audit commands)
    coverage-root--Absolute prefix to strip from Istanbul file paths before matching (health and audit commands). Use when coverage was generated under a different checkout root in CI / Docker (e.g., /home/runner/work/myapp).
    max-crap30.0CRAP score threshold (health and audit commands). Functions meeting or exceeding this score contribute to the verdict.
    gatenew-onlyAudit verdict gate. new-only fails only on findings introduced by the changeset; all fails on every finding in changed files.
    security-gate--Security delta gate for command: security. new fails only on changed-line candidates; newly-reachable fails only on candidates that became reachable from entry points (needs a base ref via changed-since or PR auto-scoping). Gated security failures exit with code 8, and the issues output counts only matching gate candidates. PR comment and review renderers do not support security envelopes yet and are skipped for this command.
    dead-code-baseline / health-baseline / dupes-baseline--Per-analysis baseline file paths for the audit command (saved by `fallow dead-code
    args--Additional arguments to pass to fallow
  3. Upload SARIF (optional)

    Upload results to GitHub Code Scanning to get inline annotations on the PR diff:

    - uses: fallow-rs/fallow@v3
      with:
        format: sarif
    
    - uses: github/codeql-action/upload-sarif@v4
      with:
        sarif_file: fallow-results.sarif
fallow: 8 issues found

Dead Code (3 issues)
| Type | File | Symbol | Line |
|------|------|--------|------|
| unused-export | src/utils/format.ts | formatCurrency | 12 |
| unused-export | src/utils/format.ts | formatPercentage | 28 |
| unused-file | src/legacy/oldApi.ts | n/a | n/a |

Duplication (3 clone groups, 1.8%)
| Files | Lines | Tokens |
|-------|-------|--------|
| src/tax/utils.ts src/savings/utils.ts | 25 | 92 |

Complexity (2 hotspots)
| File | Function | Cyclomatic | Cognitive |
|------|----------|------------|-----------|
| src/server/router.ts:42 | handleRequest | 28 | 34 |

Completed in 48ms

GitHub Code Scanning is available on public repositories for free (no GitHub Advanced Security needed) and on private or internal repositories with GitHub Advanced Security enabled. On a public repository the action always attempts the upload (the first upload initializes Code Scanning); on a private or internal repository without Advanced Security it warns, skips SARIF upload, and keeps the job summary plus primary fallow output available.

The upload requires the job to grant permissions: security-events: write. Without it, the upload step fails. On a public repository this surfaces as a job failure rather than a silent skip, so add the permission alongside sarif: true.

PR summary comments use fallow's native pr-comment-github format. Inline review comments use review-github, then fallow ci reconcile-review --provider github marks stale fallow review threads resolved when findings disappear.

GitHub inline review comments target the current PR file state (side: RIGHT). Findings on deleted lines are not modeled yet; fallow's diagnostics are current-state oriented in normal use.

The action automatically detects your package manager (npm, pnpm, or yarn) from lock files. Review comments and annotations show the correct install/uninstall commands for your project.

Both the GitHub Action and GitLab CI template automatically scope analysis to changed files in PR/MR context. No extra configuration needed.

Isolating parallel review jobs

When multiple fallow review jobs target the same pull request or merge request (for example, one job per workspace in a CI matrix), give each job a stable review id: the review-id Action input on GitHub, or the FALLOW_REVIEW_ID variable on GitLab. The id must be 1-64 characters from A-Za-z0-9._-.

Reconciliation is exact-scope: a run only deduplicates against, and resolves, comments that carry the same review id. Unscoped runs only see unscoped comments, so adding an id to a new job never touches an existing job's threads.

# GitHub Actions matrix: one review scope per workspace
- uses: fallow-rs/fallow@v3
  with:
    review-comments: true
    workspace: ${{ matrix.workspace }}
    review-id: ${{ matrix.workspace }}

Without distinct ids, parallel jobs share one scope: each job would deduplicate against the other jobs' comments and resolve their threads as stale.

Binary verification

The fallow-rs/fallow GitHub Action and the GitLab CI template both install fallow via npm install -g fallow@<spec>. Each @fallow-cli/<platform> npm package ships an Ed25519 .sig next to every binary plus an embedded SHA-256 digest in its package.json#fallowDigests field. Verification runs in two layers across two independent surfaces:

  1. Ed25519 signature (offline, rooted in the workflow's signing key): the embedded public key in fallow's npm wrapper verifies all three binaries (fallow, fallow-lsp, fallow-mcp). A tampered binary fails closed with a specific failure code (sig-invalid, digest-mismatch, binary-missing, sig-missing, or digest-unavailable).
  2. SHA-256 digest (offline, rooted in the platform package's fallowDigests field): the verifier confirms each binary's SHA-256 matches the digest written into the platform package's package.json at release time. A swapped binary that somehow carried a valid signature would still fail the digest check.

These two layers run on every install surface:

  • fallow, fallow-lsp, fallow-mcp first invocation: the bin wrapper runs Ed25519 + SHA-256 before exec'ing the platform binary on the first run after install or upgrade. A small JSON sentinel next to the platform binary (or in $XDG_CACHE_HOME/fallow/sentinels/ when the platform pkg dir is read-only, e.g. yarn PnP, Docker layered images) caches the verified state so subsequent invocations skip verification on a cache hit. fallow --version adds a trailing verified: yes (<sentinel-path>) line so vendor questionnaires and CI logs can confirm the integrity posture in one command.
  • Action installer re-runs both layers (action/scripts/install.sh) after npm install -g --ignore-scripts. The verifier is loaded from the checked-out Action tree, not the installed npm package, so a tampered installer cannot self-validate. This is defense in depth on CI runners, where secrets exposure is highest.

A failed verification produces a ::error:: annotation in the Action log and a non-zero exit, aborting the workflow before any user code reads a swapped binary.

The npm wrapper used to run verification during postinstall. That hook is removed ahead of npm RFC 868 (npm/cli#9360) Phase 2, which will block postinstall hooks by default unless consumers add fallow to their package.json#allowScripts. The cryptographic property is preserved bit-for-bit by the first-invocation path; same public key, same offline fallowDigests lookup, same fail-closed behavior.

Three environment variables tune the lazy-verify path:

  • FALLOW_SKIP_BINARY_VERIFY=1 skips Ed25519 + SHA-256 verification. Use only when deliberately replacing the published binary (source builds, airgapped mirrors, signed-repack registries). The skipped state is recorded in fallow --version output as verified: skipped (FALLOW_SKIP_BINARY_VERIFY is set) so the bypass remains visible in CI logs and vendor audits.
  • FALLOW_VERIFY_CACHE_DIR=<path> redirects the sentinel file to a writable directory when the platform package dir is read-only. Cascade is platform-pkg-dir, then this override, then $XDG_CACHE_HOME/fallow/sentinels/ (or %LOCALAPPDATA%\fallow\sentinels\ on Windows).
  • FALLOW_VERIFY_LOG=1 emits one structured stderr line per outcome (fallow-verify outcome=ok cache=hit sentinel=...) for CI diagnostic logs.

To override verification in the Action:

- uses: fallow-rs/fallow@v3
  env:
    FALLOW_SKIP_BINARY_VERIFY: '1'

Do not set this in normal CI configurations. The public key fingerprint and the manual out-of-band verification recipe live in SECURITY.md on the main repo. The VS Code extension performs its own Ed25519 verification on its auto-downloaded binary; see the extension docs for details.

PR/MR-only analysis

Only analyze files changed in the current pull request or merge request:

The action does this automatically via auto-changed-since (enabled by default). To disable and run a full analysis on PRs:

- uses: fallow-rs/fallow@v3
  with:
    auto-changed-since: false

To use a custom ref instead of the PR base SHA:

- uses: fallow-rs/fallow@v3
  with:
    changed-since: origin/main

Adopting fallow on a codebase with existing findings? Baselines report only new issues; see PR enforcement in the adoption guide.

Severity-aware PR gate (audit)

The default combined run gates on raw issue count: any finding in the changed files fails CI. That's the right contract for a tight feedback loop, but it doesn't honor rule severity. A project with unused-exports: warn (or any warn-tier rule) still fails CI when a PR touches a file with pre-existing warn-tier findings.

fallow audit is the severity-aware alternative. It combines dead-code, complexity, and duplication analysis scoped to changed files and returns a verdict (pass / warn / fail):

  • pass: no issues in changed files
  • warn: only warn-tier issues; CI does not fail
  • fail: error-tier issues found; CI fails

By default audit runs in gate: new-only mode, so only findings introduced by the current changeset affect the verdict. Pre-existing findings show up in the PR comment as inherited (with a count), but they do not gate the merge.

- uses: fallow-rs/fallow@v3
  with:
    command: audit
    gate: new-only        # default; fails only on findings introduced by this PR
    fail-on-issues: true

The action exposes outputs.verdict (pass/warn/fail) and outputs.gate so downstream steps can branch on the verdict:

- uses: fallow-rs/fallow@v3
  id: fallow
  with:
    command: audit

- name: Block release on regression
  if: steps.fallow.outputs.verdict == 'fail'
  run: exit 1

Detecting silent failures

CI integrations can degrade silently when the GitHub or GitLab API returns a 5xx, rate-limit response, or partial-pagination failure. Three structured signals let workflow operators detect this:

Three composite-action outputs are emitted on every run, regardless of whether the failure path was taken. Default values let downstream if: gates match positively (== 'false' or == 'none') without an absent-vs-false ambiguity:

OutputValuesMeaning
changed-files-unavailablefalse (default) / trueThe analyze step could not enumerate PR-changed files. Analysis ran against the full codebase. Common cause: transient GitHub API failure or token without pull_requests: read scope.
post-skipped-reasonnone (default) / pagination_failureThe Post review comments step aborted the inline-review POST. Only set to pagination_failure when the multi-comment fingerprint dedup lookup failed and the action skipped posting to avoid duplicate threads.
dedup-lookup-failedfalse (default) / trueA dedup lookup failed on either the Post PR comment or the Post review comments step. Distinct from post-skipped-reason: the summary-only paths post a fresh comment anyway (potential duplicate) when their lookup fails. Gate on this to detect either degraded state.
- uses: fallow-rs/fallow@v3
  id: fallow

- name: Alert on degraded scoping
  if: steps.fallow.outputs.changed-files-unavailable == 'true'
  run: echo "::warning::Fallow ran unscoped; PR scoping disabled by API failure"

- name: Alert on dedup degradation
  if: steps.fallow.outputs.dedup-lookup-failed == 'true'
  run: echo "::warning::Fallow PR comments may be duplicated"

A 4xx response (auth, scope, permission errors) on the multi-comment review path escalates to exit 1 and fails the action step, because re-running will not resolve a configuration error. 5xx responses, retry-exhausted 429s, and network errors return exit 0 with the warning so transient blips do not break PRs.

Migrating from combined to audit

If your project is on the default combined run today and you want severity-aware gating, add command: audit (FALLOW_COMMAND: audit) and the existing PR comment, annotations, and review comments continue to work. The audit run produces an extra verdict banner at the top of the PR comment:

> :x: Audit failed · gate: `new-only` · 2 new findings introduced by this PR · 5 inherited (not gated)

Use gate: all (FALLOW_AUDIT_GATE: "all") if you want every finding in changed files to gate, ignoring the inherited-vs-introduced split. This is the strict posture: nothing slips in, but pre-existing findings on touched files block the merge until cleaned up.

Audit needs a base ref. The action and GitLab CI template auto-detect the PR/MR base, so no extra configuration is needed for that workflow. On non-PR pipelines (release branches, scheduled jobs), set changed-since (FALLOW_CHANGED_SINCE) explicitly; the runners hard-error rather than silently analyze nothing.

The three tracks together

CI works best when combined with agent and editor integration:

  1. Agent generates code and runs fallow --changed-since HEAD~1 to self-check
  2. Human reviews in VS Code, sees Code Lens annotations on new exports
  3. CI runs the full analysis and catches anything that slipped through
flowchart TB
  A["Agent<br/>fallow --changed-since HEAD~1"] -->|generates + self-checks| B["Code"]
  B --> C["Human<br/>VS Code Code Lens"]
  C -->|reviews| D["Pull Request"]
  D --> E["CI<br/>Full fallow analysis + SARIF"]
  E -->|passes| F["main"]

See also