Skip to content
Fallow home
All docs pages

CI integration

Check every push and pull request for changed-code risk, cleanup opportunities, duplication, and complexity hotspots before they merge. Runs in GitHub Actions or GitLab CI, with SARIF upload, Code Quality reports, MR comments, inline review with suggestions, and baselines.

Run fallow in CI to catch changed-code risk, cleanup opportunities, duplication, and complexity issues on every push and pull request. CI catches the issues that get past agent workflows and editor review. You get the results as PR or MR comments, inline review, annotations, and a merge gate.

To roll out on TypeScript, start with type-aware analysis in best-effort mode and check _meta.type_aware in the 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-26.04
        steps:
          - uses: actions/checkout@v4
          - uses: fallow-rs/fallow@v3
            with:
              format: sarif

    By default, this runs all analyses (dead code, duplication, and complexity). To run one analysis, set the command input.

    When you enable comment: true or review-comments: true, grant the job contents: read, id-token: write, pull-requests: write, and checks: write. With id-token: write, the Action gets a short-lived Fallow GitHub App token, and fallow-cloud[bot] posts the feedback. Without id-token: write, the analysis and the posts still run, as github-actions[bot].

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

    The action takes these inputs:

    InputDefaultDescription
    command-- (all)Command to run (dead-code, architecture, dupes, health, audit, security, fix, or empty for all). Legacy alias: check = dead-code. architecture reports import cycles, boundary violations, and policy violations. See fallow architecture.
    root.Project root directory
    config--Path to config file (.fallowrc.json, .fallowrc.jsonc, fallow.toml, or .fallow.toml)
    formatsarifOutput format (human, json, sarif, compact, markdown, codeclimate, pr-comment-github, pr-comment-gitlab, review-github, review-gitlab, or badge)
    sariffalseUpload SARIF to GitHub Code Scanning. Needs permissions: security-events: write.
    artifacts-dir.Directory for the files that the action writes, such as fallow-results.json and fallow-results.sarif. Relative to the workflow workspace.
    productionfalseTurn on 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 when there are issues
    issue-types--Comma-separated issue types to report (dead-code and architecture commands). For architecture, the values are cycles, boundaries and policy.
    include-entry-exportsfalseReport unused exports in entry files and do not mark them as used. This catches typos in framework exports (dead-code analysis).
    fail-on-regressionfalseExit with code 1 when the issue counts increase compared to the regression baseline (dead-code command)
    tolerance0Allowed increase before a regression fails, for example 2% or 5 (dead-code command)
    regression-baseline--Path to the regression baseline file to compare against (dead-code command)
    save-regression-baseline--Save the current results as a regression baseline file (dead-code command)
    changed-since--Only check files changed since this ref
    auto-changed-sincetrueIn a PR, check only changed files, compared to the base SHA. Ignored when changed-since is set.
    baseline--Path to a baseline file to compare against. With command: audit, the action rejects it with exit 2. Use dead-code-baseline, health-baseline, or dupes-baseline for audit. With command: fix, the action also rejects it with exit 2, because fix does not load a baseline. When command is empty, it holds the dead-code baseline only: pass the health and duplication baselines with health-baseline and dupes-baseline.
    save-baseline--Save the current results as a baseline file. With command: audit, the action rejects it with exit 2, because audit runs three analyses with different baseline formats. command: fix also rejects it with exit 2. The file must be inside the project, its Git work tree, the CI workspace, RUNNER_TEMP, or the system temp directory. See Save destinations.
    fail-on-stale-baselinefalseFail the job when the loaded baseline has entries that match nothing in this run, or when it is a baseline of another kind. You get the staleness warning without this input: every run that loads a baseline reports it. A scoped run cannot judge a whole-project baseline, so a narrowed run first reads the baseline again over the whole project. This input only decides if that verdict fails the job. It does not depend on fail-on-issues, because a fully stale baseline on a now-clean project reports zero issues. When the run still cannot cover the whole project (production, workspace, changed-workspaces, or a positional path in args), the gate warns and does not judge.
    min-score--command: health only: fail when the health score is below this threshold (0-100). Implies --score. Unless you set a health section input, the action adds --complexity, so annotations, SARIF, and the PR comment still have content. target_thresholds and hotspot_summary do not come back. With this input, the CLI turns off its own findings rule and the count gate, so only the score decides the run. Any other command rejects it with exit 2.
    min-severity--command: health only: fail when a complexity finding reaches this severity (moderate, high, or critical). A finding whose complexity-* rule is warn never trips this gate. The fail-on-issues count gate is separate and still counts every finding. To gate on severity alone, set fail-on-issues: false.
    fail-on-empty-analysisfalseFail the job when the run analyzed no source file. In that case every count is zero because the run measured nothing, not because the project is clean. By default the run warns and passes, so a repository whose scope has no source on purpose still passes.
    version--Fallow version to install. The Action ref does not set the fallow CLI version: uses: fallow-rs/fallow@v3 selects only the Action wrapper code. The action picks the CLI version from this input, then the fallow dependency spec in the project package.json, then latest.
    workspace--Limit the output to one or more workspaces (exact names, globs, ! negation; comma-separated)
    changed-workspaces--Limit a monorepo run to workspaces with a file changed since REF (for example origin/main). Requires fetch-depth: 0. Cannot be used with workspace. A missing ref is a hard error (exit 2). The run does not fall back to the full scope.
    commentfalsePost results as a PR comment
    review-commentsfalsePost inline PR review comments from typed review-github output. Later runs resolve threads for fixed findings
    review-guidancefalseAdd collapsed "What to do" guidance blocks to inline PR review comments. Requires review-comments: true
    max-comments50Maximum number of inline review comments, and of items in the sticky details table
    comment-layoutdefaultLayout of the sticky PR comment: default, compact, gate-only, or details. Use gate-only when the Check Run is the main place where you read PR findings.
    comment-id--Marker id of the sticky comment. The default is fallow-results, with the workspace name added when the run covers one workspace.
    summary-scopeallScope of the sticky PR summary. Use diff to also apply the diff filter to project-level dependency, catalog, and override findings. Inline review comments do not change.
    diff-filteraddedShow line-level findings only on added PR lines. Use diff_context, file, or nofilter for a wider review scope.
    diff-file--Path to a unified diff file. Fallow then limits source findings to lines in an added hunk. When you also set changed-since, the diff filter decides the findings, and changed-since still decides the file discovery.
    api-retries3Maximum number of HTTP retries for fallow ci reconcile-review and the GitHub API calls of the action
    api-retry-delay2Minimum delay in seconds between retries after a rate limit. A Retry-After header from the server overrides it.
    review-id--Stable identifier (1-64 characters, A-Za-z0-9._-) that keeps the inline review comments of each job separate when more than one fallow review job posts to the same pull request. See Isolating parallel review jobs.
    annotationstrueShow findings as inline PR annotations through workflow commands (no Advanced Security required). On fallow >= 3.4.2, the action renders them natively with fallow report --format github-annotations. Older binaries use a frozen legacy renderer that gets no new finding types, and the step log shows a notice to upgrade. With version: latest, you get the native path with no change on your side. 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. Each annotation takes its level from the rule severity of the finding, so an error finding gives ::error. See Levels in CI formats.
    max-annotations50Maximum number of inline annotations. On the native path, the cap applies to the rendered stream. GitHub shows at most 10 annotations per type per step.
    github-token$\{\{ github.token \}\}GitHub token for PR comments and SARIF upload
    branded-tokentrueWhen the workflow grants id-token: write, post comments and reviews with a short-lived Fallow GitHub App token. When that token is not available, the action uses github-token.
    broker-urlhttps://api.fallow.cloudFallow token broker that exchanges the workflow identity for the branded app token.
    dupes-modemildDetection mode for the dupes command
    min-tokens--Minimum token count for a clone (dupes command)
    min-lines--Minimum line count for a clone (dupes command)
    threshold--Fail when duplication is more than this % (dupes command)
    skip-localfalseOnly report cross-directory duplicates (dupes command)
    cross-languagefalseRemove TypeScript type annotations, so TypeScript and JavaScript clones match (dupes command)
    ignore-importsfalseExclude import declarations from clone detection (dupes command)
    top--Show only the N worst offenders (health and dupes commands)
    max-cyclomatic--Maximum cyclomatic complexity (health command, CLI default 20)
    max-cognitive--Maximum cognitive complexity (health command, CLI default 15)
    sort--Sort order for health results: cyclomatic (default), cognitive, lines, or severity (health command)
    complexityfalseShow only the complexity findings section (health command)
    file-scoresfalseCompute the maintainability index for each file (health command)
    hotspotsfalseFind files that are complex and change often (health command)
    targetsfalseShow ranked refactoring recommendations (health command)
    since--Git history window for hotspots: a duration (6m, 90d, 1y) or an ISO date (health command)
    min-commits--Minimum number of commits for a file in the hotspot ranking (health command, CLI default 3)
    scorefalseCompute the health score (0-100 with letter grade). Adds the health delta header to PR comments (health and bare command)
    trendfalseCompare the current metrics with the most recent saved snapshot. Implies score (health and bare command)
    save-snapshot--Save a vital signs snapshot for trend tracking. Set true for the default path, or give a custom path (health and bare command)
    type-awareautoOptional project-wide TypeScript semantic evidence: true, false, or auto. With auto, the action installs the matching fallow-type-aware sidecar when your fallow config turns on typeAware, and the config decides. true also passes --type-aware. false skips the sidecar and passes --no-type-aware. Does not report compiler diagnostics or generic typed lint findings.
    type-aware-projects""Comma-separated tsconfig paths. Empty uses automatic project discovery.
    type-aware-require""best-effort or complete. When empty, the action passes no value, and your config or the CLI default (best-effort) decides. Use complete to fail when semantic analysis is partial or unavailable, including valid zero-finding runs. The action passes the value to fallow only with type-aware: true. With auto or false, the action checks the result itself after the run, so complete also fails a run in which type-aware analysis did not run.
    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 remove from Istanbul file paths before matching (health and audit commands). Use it when CI or Docker generated the coverage under a different checkout root, for example /home/runner/work/myapp.
    runtime-coverage--Path to runtime coverage input for the health command: a V8 coverage directory, a V8 JSON file, or an Istanbul coverage-final.json. A single local capture is free. Continuous or multi-capture monitoring needs a license. See fallow license.
    min-invocations-hot--Hot-path threshold for runtime coverage findings (health command, CLI default 100)
    min-observation-volume--Minimum observation volume for high-confidence runtime coverage verdicts (health command)
    low-traffic-threshold--Share of the total trace volume below which an invoked function is low_traffic and not active (health command)
    max-crap30.0CRAP score threshold (health and audit commands). Functions at or above this score count toward the verdict.
    gatenew-onlyAudit verdict gate. new-only fails only on findings that the changeset introduces. 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, and needs a base ref from changed-since or PR auto-scoping. A gated security failure exits with code 8, and the issues output counts only the candidates that match the gate. The PR comment and review renderers do not support security envelopes yet, so the action skips them for this command.
    dead-code-baseline / health-baseline / dupes-baseline--Baseline file paths for each analysis in the audit command (saved by `fallow dead-code
    no-cachefalseTurn off the incremental parse cache and parse all files again
    threads--Number of parser threads (default: the number of CPU cores)
    only--Without command: run only these analyses (comma-separated: dead-code, architecture, dupes, health)
    skip--Without command: skip these analyses (comma-separated: dead-code, architecture, dupes, health)
    args--More arguments to pass to fallow. With command: audit, the action rejects --fail-on-stale-baseline with exit 2. An audit analyzes only the files that changed against its base, so it cannot judge a whole-project baseline.
  3. Upload SARIF (optional)

    To get inline annotations on the PR diff, upload the results to GitHub Code Scanning:

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

    Each dead-code result has a fallowFinding/v1 partial fingerprint with the finding_id of the finding. The location-based keys did not change, so code scanning does not close and reopen the open alerts after an upgrade. See SARIF output.

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 free on public repositories (no GitHub Advanced Security needed). On private or internal repositories, it needs GitHub Advanced Security. On a public repository, the action always tries the upload, and the first upload sets up Code Scanning. On a private or internal repository without Advanced Security, the action warns and skips the SARIF upload. The job summary and the main fallow output are still available.

The upload needs permissions: security-events: write on the job. Without it, the upload step fails. On a public repository the job fails, so add the permission together with sarif: true.

PR summary comments use the native fallow pr-comment-github format. Inline review comments use review-github. When findings go away, fallow ci reconcile-review --provider github marks the old fallow review threads as resolved. A dead-code review thread stays open when you add lines above its finding, because its fingerprint comes from the finding_id.

GitHub inline review comments point to the current state of the PR file (side: RIGHT). Fallow does not yet model findings on deleted lines. In normal use, fallow diagnostics describe the current state of the code.

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

In a PR or MR, the GitHub Action and the GitLab CI template check only the changed files. You do not need to configure this.

Isolating parallel review jobs

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

A run only deduplicates and resolves comments with the same review id. A run without an id only sees comments without an id. So when you add an id to a new job, the threads of existing jobs do not change.

# 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 different ids, parallel jobs share one scope. Each job then deduplicates against the comments of the other jobs and resolves their threads as stale.

Binary verification

Fallow checks that the binary you run is the binary that the fallow release built. The fallow-rs/fallow GitHub Action and the GitLab CI template both install fallow with npm install -g fallow@<spec>. Each @fallow-cli/<platform> npm package has an Ed25519 .sig file next to each binary, and a SHA-256 digest in its package.json#fallowDigests field. Verification has two layers, and it runs in two separate places:

  1. Ed25519 signature (offline, based on the signing key of the release workflow): the public key in the fallow 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, based on the fallowDigests field of the platform package): the verifier checks that the SHA-256 of each binary matches the digest that the release wrote into the package.json of the platform package. A swapped binary with a valid signature would still fail the digest check.

Both layers run in these two places:

  • First run of fallow, fallow-lsp, or fallow-mcp: on the first run after an install or upgrade, the bin wrapper runs the Ed25519 and SHA-256 checks before it starts the platform binary. A small JSON sentinel file caches the verified state, so later runs skip verification on a cache hit. The sentinel is next to the platform binary, or in $XDG_CACHE_HOME/fallow/sentinels/ when the platform package directory is read-only (for example with yarn PnP or Docker layered images). fallow --version adds a last line verified: yes (<sentinel-path>), so vendor questionnaires and CI logs can confirm the integrity status with one command.
  • The Action installer runs both layers again (action/scripts/install.sh) after npm install -g --ignore-scripts. It loads the verifier from the checked-out Action tree, not from the installed npm package, so a tampered installer cannot validate itself. This adds defense in depth on CI runners, where the risk of secrets exposure is highest.

A failed verification writes a ::error:: annotation to the Action log and exits with a non-zero code. The workflow stops before any user code reads a swapped binary.

The npm wrapper used to run verification during postinstall. Fallow removed that hook before npm RFC 868 (npm/cli#9360) Phase 2. Phase 2 will block postinstall hooks by default, unless you add fallow to package.json#allowScripts. The first-run check gives exactly the same cryptographic guarantee: the same public key, the same offline fallowDigests lookup, and the same fail-closed behavior.

Three environment variables change how the first-run check works:

  • FALLOW_SKIP_BINARY_VERIFY=1 skips the Ed25519 and SHA-256 checks. Use it only when you replace the published binary on purpose (source builds, airgapped mirrors, signed-repack registries). fallow --version then shows verified: skipped (FALLOW_SKIP_BINARY_VERIFY is set), so CI logs and vendor audits still show the bypass.
  • FALLOW_VERIFY_CACHE_DIR=<path> puts the sentinel file in a writable directory when the platform package directory is read-only. The order is: the platform package directory, then this override, then $XDG_CACHE_HOME/fallow/sentinels/ (or %LOCALAPPDATA%\fallow\sentinels\ on Windows).
  • FALLOW_VERIFY_LOG=1 writes one structured stderr line for each outcome (fallow-verify outcome=ok cache=hit sentinel=...) for CI diagnostic logs.

To skip 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 steps for manual out-of-band verification are in SECURITY.md in the main repo. The VS Code extension does its own Ed25519 verification on the binary it downloads. See the extension docs for details.

PR/MR-only analysis

Analyze only the files that the current pull request or merge request changes:

The action does this with auto-changed-since, which is on by default. To turn it off and run a full analysis on PRs:

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

To compare against a custom ref in place of the PR base SHA:

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

Does your codebase already have findings? With a baseline, fallow reports only new issues. See PR enforcement in the adoption guide.

Gates

Each gate that the run turned on writes a verdict to the analysis envelope. Both integrations read that verdict, not the CLI exit code. They ignore the exit code when stdout parses as JSON. A gate fails the build only when all three of these are true:

  • The input or variable for the gate asked for it.
  • The CLI concluded fail.
  • The CLI marked the verdict as enforced.

Each gate has its own input:

GateInput
Regressionfail-on-regression
Duplicationthreshold
Health (two gates)min-score and min-severity
Securitysecurity-gate
Baselinefail-on-stale-baseline
Semantic completenesstype-aware-require
Parse errorNone. Set failOnParseError in the config, or pass --fail-on-parse-error in args or FALLOW_ARGS. The gate entry exists only when you arm it, so a failure always fails the job. See Parse error gate.

These gates do not depend on fail-on-issues or FALLOW_FAIL_ON_ISSUES, which still gates on the issue count. command: audit is the exception: it gates on its verdict through fail-on-issues. So an audit job with fail-on-issues: false only reports.

When a gate concludes fail but its input is not set, it warns and does not fail. So a flag in args cannot override fail-on-issues: false. When a gate did not judge the run, it warns if its input asked for it. Each failing gate prints its own error line. The step exits once at the end, after it writes the outputs and artifacts, so the comment, annotation, and summary steps still run. The security gate keeps its documented exit code 8.

The step has these outputs:

  • gates-failed, gates-warned, gates-skipped, and gates-passed list the gate names, comma-separated. The list always includes the default rule of the command (error-severity-findings, health-findings or audit-verdict). A run with findings names that rule in gates-failed, also when fail-on-issues: false keeps the job green. A job that analyzes duplication and passes --fail-on-issues or --ci in args or FALLOW_ARGS also lists duplication-findings. fail-on-issues governs this entry in the same way. The count gate already counts clone groups, so the result of the job does not change. To act on one gate, read its name. Do not treat a non-empty gates-failed as a failed job.
  • analysis-degraded tells you if the findings cover less than the whole project.
  • requests-unapplied names the requests that the run could not apply, each with its reason in parentheses, for example changed-since (git-failed).
  • baseline-path is the baseline that the run compared against, as the baseline input wrote it. It is empty for a baseline passed through args.

GitLab writes the same names, and FALLOW_REQUESTS_UNAPPLIED, to fallow-gates.env, and publishes that file through artifacts:reports:dotenv.

The sticky PR comment, the MR note, the inline review bodies, and the Check Run show the same verdict. They show the baseline notice as a blockquote, and one row for each gate that the run turned on. Each row gives the value that the gate measured and the threshold it compared against. A gate that the run did not enforce shows as neutral and does not change the Check Run conclusion. To make each gate a separate required check, post the decision sidecar yourself with fallow ci post-check-run --split-gates. That flag creates one Fallow / <gate> status context for each gate, in place of a single Fallow check. Neither integration passes this flag.

In combined mode, the CLI enforces the duplication threshold only with --fail-on-issues, and says so in the envelope. Without that flag, the integrations warn and do not fail. When --fail-on-issues comes through args or FALLOW_ARGS, the threshold fails the job only when fail-on-issues or FALLOW_FAIL_ON_ISSUES is true. To gate on the threshold alone, run the dupes command.

On a fallow older than 3.27.0, the envelope has no gate verdicts. For the regression, security, baseline, and semantic gates, both integrations then read the fields that those releases already had. threshold, min-score, and min-severity had no field, so these gates fail open with one warning. That warning shows only when the matching input is set.

On a fallow older than 3.28.0, the envelope has no request_outcomes, and baseline_staleness has no scope_reasons or unrecognised_format. requests-unapplied, baseline-scope-reasons, and baseline-unrecognised are then empty, and the warnings that read them do not show. The whole-project baseline re-read then uses the production, workspace, and changed-workspaces inputs.

Degraded and empty analysis

When the findings of a run cover less than the whole project, the run warns once with Fallow ran with degraded inputs:, followed by the diagnostic kinds and their counts. Both integrations filter on degrades_analysis in workspace_diagnostics, so they also handle a kind that a later release adds. A run that analyzed no source file gets its own sentence. Its clean result means that the run measured nothing, not that it found nothing. By default, such a run warns and passes. To make it fail, set fail-on-empty-analysis or FALLOW_FAIL_ON_EMPTY_ANALYSIS.

When the run cannot apply a request that narrows the report, it warns with Fallow could not apply: and names each request with its reason. The report then covers more of the project than you asked for, so do not read it as limited to the change. The warning filters on affects: "scope", so a request that writes a file next to the report never shows in it. For the request names, statuses, and reasons, see request_outcomes.

The action gives a separate warning when format: sarif or sarif: true produced no SARIF document. The warning says that the action uploads nothing, and that code scanning keeps the alerts of the previous upload. When the binary gives a reason in request_outcomes["sarif-file"], the warning shows it.

In the opposite case, the run warns with Fallow applied <request> over an empty scope. The run applied the request, and the remaining scope was empty. An empty scope cannot have findings, so a clean report there covered nothing. For example, a --changed-since run where only a README changed gives scope_size: 0. Check the diff or the ref that you gave the run before you trust a clean result. Both integrations check for scope_size: 0 together with status: "applied", and for affects: "scope". So a file written next to the report never triggers this warning. The comment, review, job summary, and annotation bodies also state this, on their request-outcome line.

A run that loads a baseline reports how much of the baseline still matches. When entries are stale, it warns in the log and the job summary. A scoped run cannot judge a whole-project baseline, so for a narrowed run the integrations first read the baseline again over the whole project. baseline_staleness.scope_reasons controls this:

  • The integrations read the baseline again when they added each narrowing channel themselves: diff, changed-since, changed-files, scope, file, and issue-type-filter.
  • For production, workspace, and changed-workspaces, they do not judge the baseline and they name the channel, because these inputs define what you consider the project to be. The same rule applies to a scope that comes in through args or FALLOW_ARGS.

The action publishes the list as baseline-scope-reasons. To fail the build on a stale baseline, set fail-on-stale-baseline (GitHub) or FALLOW_FAIL_ON_STALE_BASELINE (GitLab). The baseline input and the save-baseline input must not name the same file, because the run saves before it compares. The integrations warn when they see this. The PR comment and the MR note show the gate line from fallow report, which states what each gate concluded.

A baseline that the command cannot read as its own format (baseline_staleness.unrecognised_format) suppresses nothing. It is a baseline that another command saved, or a file with no content for this command. Fallow does not report a baseline that you saved correctly on a project with nothing to record. dead-code, dupes, and health all report an unrecognised baseline, and all keep their exit code. The step log, the job summary, the PR comment, the MR note, and the Check Run show the sentence. The action publishes baseline-unrecognised, and the job summary names the file through baseline-path. When the file names the command that saved it, the action publishes that command as baseline-saved-by (dead-code, dupes, or health), and the step log, the job summary, and the comments name it. The GitLab template names it in the job log and the MR note. Such a file trips the baseline gate, which baseline-gate-trips also reports, so fail-on-stale-baseline fails the job. Each command has its own baseline format. See dead-code, dupes, and health.

command: audit does not judge the baselines it loads, because it analyzes only the files that changed against its base. For each loaded baseline, it prints one notice with the entry count and the unscoped command that can judge it. It rejects --fail-on-stale-baseline in args or FALLOW_ARGS with exit 2. For the baseline inputs of each analysis, see audit.

Where each advisory lands

A run limited to the change says nothing about staleness, because it cannot judge a whole-project baseline. The PR comment, the MR note, and the Check Run come from the envelope of that run, so they show no baseline notice on a pull request. The step log and the job summary show the verdict of the whole-project re-read. The baseline rows below describe a run that could judge the baseline it loaded. The job summary and the Check Run exist on GitHub only. On GitLab, read the job log, the MR note, and the Code Quality report.

AdvisoryStep or job logJob summaryPR comment or MR noteCheck Run
Baseline partially stale, or matched nothingYesYesYesYes
Baseline has stale entries on a project that is now cleanYesYesYesYes
Baseline recognises nothingYesYesYesYes
Why fallow could not judge the baseline, and which channel narrowed the runYesNoNoNo
Gate inventory, one entry per armed gate and the default rule of the commandYesYesYesYes
Degraded inputsYesYesNoNo
No source file analyzedYesNoNoNo
Requests the run could not applyYesYesYesYes
A request that applied over an empty scopeYesYesYesYes
SARIF requested and not produced (GitHub)YesNoNoNo

When the stale-baseline gate does not judge the run, every place in the table shows this through the gate inventory. Only the sentence that says why, and which channel narrowed the run, stays in the log.

Severity-aware PR gate (audit)

The default combined run gates on the raw issue count: any finding in the changed files fails CI. This gives fast feedback, but it ignores rule severity. A project with unused-exports: warn (or any warn-tier rule) still fails CI when a PR touches a file with old warn-tier findings.

fallow audit gates on severity. It runs dead-code, complexity, and duplication analysis on the changed files and returns a verdict (pass, warn, or 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 that the current changeset introduces change the verdict. Old findings show in the PR comment as inherited (with a count), but they do not block 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 has the outputs outputs.verdict (pass, warn, or fail) and outputs.gate, so later steps can act 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

When the GitHub or GitLab API returns a 5xx, a rate-limit response, or a partial-pagination failure, a CI integration can do less than it should without an error. Structured signals tell you when this happens:

The action sets three outputs on every run, also when nothing failed. Each output has a default value, so a later if: can match on a value (== 'false' or == 'none') and does not have to tell an absent output from false:

OutputValuesMeaning
changed-files-unavailablefalse (default) / trueThe analyze step could not list the files that the PR changed, so the analysis ran on the full codebase. Usual causes: a temporary GitHub API failure, or a token without pull_requests: read scope.
post-skipped-reasonnone (default) / pagination_failureThe Post review comments step stopped the inline-review POST. The value is pagination_failure only when the fingerprint dedup lookup for the comments failed, and the action did not post, to prevent duplicate threads.
dedup-lookup-failedfalse (default) / trueA dedup lookup failed in the Post PR comment step or the Post review comments step. This is not the same as post-skipped-reason: when the lookup fails, the summary-only paths still post a new comment (possible duplicate). Check this output to find either problem.
- 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, or permission error) on the multi-comment review path causes exit 1 and fails the action step, because a new run cannot fix a configuration error. 5xx responses, 429s after all retries, and network errors return exit 0 with the warning, so a temporary problem does not break PRs.

Migrating from combined to audit

To gate on severity when your project uses the default combined run, add command: audit (FALLOW_COMMAND: audit). The PR comment, annotations, and review comments still work. The audit run adds a 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)

To gate on every finding in changed files, inherited or introduced, use gate: all (FALLOW_AUDIT_GATE: "all"). This is the strict mode. No new finding gets in, but old findings in touched files block the merge until you clean them up.

Audit needs a base ref. The action and the GitLab CI template detect the PR or MR base, so PR and MR pipelines need no extra configuration. On other pipelines (pushes, release branches, scheduled jobs), set changed-since (FALLOW_CHANGED_SINCE). Without it, fallow looks for a base itself: the upstream branch, then origin/HEAD, origin/main or origin/master, then a local main or master. When it finds none, the run stops with exit code 2. When the base it finds is the commit itself, for example on a push to main, audit has no changed files and returns pass without analyzing anything.

The three tracks together

CI works best together with the agent and editor integrations:

  1. Agent writes code and runs fallow --changed-since HEAD~1 to check its own work
  2. Human reviews in VS Code and sees Code Lens annotations on new exports
  3. CI runs the full analysis and catches what the first two steps missed
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