nestjs-doctorGitHub

CI & Pull Requests

Running a linter on a codebase that predates it produces a wall of findings nobody reads. nestjs-doctor solves that by reporting only what a change introduced, and leaving the existing backlog to the score.

GitHub Action

# .github/workflows/nestjs-doctor.yml
name: NestJS Doctor

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  pull-requests: write
  statuses: write

jobs:
  doctor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # required for `scope: changed`
      - uses: RoloBits/nestjs-doctor@v1

On a pull request the action reports in three places: a sticky summary comment that is rewritten in place on every push, inline review comments on the changed lines, and a commit status carrying the score. The full report is also mirrored into the run's job summary.

fetch-depth: 0 matters for the default scope. The action compares against the merge base of your branch and its target; a shallow checkout cannot resolve one, so it falls back to the base branch tip — and if it cannot reach that either, to reporting every finding in the changed files. The comment says which of those happened, and silence-missing-baseline-warning hides the notice if you would rather live with the fallback.

Inputs

InputDefaultDescription
directory.Project directory to scan
scopechangedWhat a pull request reports — see below
blockingnoneSeverity that fails the workflow: none, warning, error
min-scoreFail when the project's health score drops below this
configPath to a config file
commenttrueSticky pull request summary comment
review-commentstrueInline review comments on changed lines
commit-statustrueCommit status with the score
sariffalseAlso write a SARIF report
sarif-filenestjs-doctor.sarifWhere the SARIF report is written
silence-missing-baseline-warningfalseHide the degraded-scope warning
github-token${{ github.token }}Token used to comment and set the commit status
node-version22Node.js version
versionlatestnestjs-doctor version to run

Failing the build

The action is advisory by default. blocking is none and min-score is unset, so a fresh install comments, annotates, and sets a status without ever turning a pull request red. Enforcement is something you turn on when you are ready for it.

Two gates decide the outcome, they are independent, and either one failing fails the run.

      - uses: RoloBits/nestjs-doctor@v1
        with:
          blocking: error
          min-score: 80

That configuration fails a pull request when it introduces any error-severity finding, and separately when the project's health score is below 80. Drop either input to use only the other gate.

blocking — gate on findings

Gates on the findings the run actually reported, which the scope input decides. With the default scope: changed, that is only what the pull request introduced.

LevelFails when
noneNever. The default — report only
warningAny error or warning is reported
errorAny error is reported

This is the gate to reach for first. Because it follows the scope, a repository with a thousand pre-existing findings can adopt blocking: error on day one: the gate only ever sees what each pull request adds, so nobody inherits somebody else's backlog.

The CLI's --blocking default varies by output mode — error for the console report, none for --json and --score — because those exit codes predate the flag. The action sidesteps that by always passing --blocking explicitly, so the action's default is none whichever way you look at it.

min-score — gate on the score

Gates on the 0–100 project health score. Unset by default; set it to a number and the run fails whenever the score falls below it.

The score always reflects the whole project, whatever scope says. That is a deliberate invariant: narrowing a report must never make a codebase look healthier than it is. The practical consequence is that min-score is a gate on the repository's overall health, not on the change — a pull request touching one file can fail it because of debt somewhere else entirely.

So the two gates answer different questions:

GateQuestionSees
blockingDid this change make things worse?The reported scope
min-scoreIs the project healthy enough?The whole project

Pick blocking alone to hold the line on new code. Add min-score when you also want a floor under the codebase as a whole, and expect to raise it over time rather than setting it at today's number and forgetting it.

Setting the threshold in config instead

minScore is also a config-file key, so a repository can carry its floor in nestjs-doctor.config.js and have local runs, the pre-commit hook, and CI all agree on it:

export default {
  minScore: 75,
};

The action's min-score input overrides that when set, which is the hook for holding CI to a higher bar than a local run. Leave the input empty and the config value applies.

Which findings count

Both gates count whatever the run reports, so the config file is the third lever. Disable a rule or a whole category and it stops counting toward either gate — see Configuration. Inline nestjs-doctor-ignore comments work the same way: a suppressed finding is not reported, so it cannot fail a gate.

Point the action at a CI-specific file with the config input when you want CI stricter than what contributors run locally.

Exit codes

0 pass · 1 a gate failed · 2 bad input.

A push to the default branch always exits 0, whatever the gates say — see below.

Outputs

score, label, total-issues, fixed-issues, error-count, warning-count, affected-files, report-file, sarif-file.

      - uses: RoloBits/nestjs-doctor@v1
        id: doctor
      - run: echo "Health is ${{ steps.doctor.outputs.score }}/100"

Permissions

The action degrades rather than failing when a permission is missing, and logs a warning naming the one it needs.

FeaturePermission
Sticky comment, inline review commentspull-requests: write
Commit statusstatuses: write
Changed-file API fallbackpull-requests: read
SARIF upload (via github/codeql-action/upload-sarif)security-events: write

Pushes to the default branch

A push run always scans the whole project and never fails, whatever blocking says. It is a health snapshot — surfaced through the job summary and the commit status — so main does not go red on findings that were already there.

Scope

The whole project is always analysed. Cross-file rules like architecture/no-circular-module-deps and performance/no-unused-providers would produce nonsense from a partial view, so --scope narrows only what gets reported.

ScopeReports
fullEvery finding in the project (the CLI default)
filesFindings in files the change touched
linesFindings on the lines the change touched
changedFindings the change introduced, measured against the base (the action's default)

changed checks the base revision out into a temporary git worktree, scans it with the same rules and config, and subtracts what was already there. Findings are matched on rule, file, message, and source text — never on line number — so an unrelated edit higher up in a file does not make an old finding look new. The comment also reports how many findings the change resolved.

lines reports only findings whose line falls inside a diff hunk. Schema findings describe an entity rather than a line, so they never appear at this scope; use files or changed if you want them.

# What did this branch introduce?
npx nestjs-doctor . --scope changed --base origin/main

# Pre-commit hook: only what you are about to commit
npx nestjs-doctor . --staged --blocking error

Gates on the command line

The same two gates the action exposes, as flags. Everything under Failing the build applies here too — what each gate sees, and why --min-score is a whole-project measure whatever the scope.

# Fail on anything this branch introduced, and on a project score under 80
npx nestjs-doctor . --scope changed --base origin/main --blocking error --min-score 80
FlagGates onDefault
--blocking none|warning|errorThe reported findingserror for the console report, none for --json and --score
--min-score <0-100>The whole-project scoreOff

The split default for --blocking reproduces the exit codes that shipped before the flag existed: the console report already failed on errors, while --json and --score failed only on --min-score. Pass --blocking explicitly and every output mode behaves the same.

Exit codes: 0 pass, 1 a gate failed, 2 bad input.

Code scanning

SARIF sends findings to the repository's Security tab, where they persist across runs as alerts rather than living only in a pull request comment.

permissions:
  contents: read
  security-events: write

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0
  - uses: RoloBits/nestjs-doctor@v1
    id: doctor
    with:
      scope: full
      sarif: "true"
  - uses: github/codeql-action/upload-sarif@v3
    if: always() && steps.doctor.outputs.sarif-file != ''
    with:
      sarif_file: ${{ steps.doctor.outputs.sarif-file }}

Every result carries a partialFingerprints entry derived from the rule, path, message, and source text. Without it GitHub computes its own fingerprint from surrounding source, and an edit near a finding closes the old alert and opens an identical new one.

GitLab

The gitlab format emits the CodeClimate subset GitLab's Code Quality widget reads, so findings appear on the merge request diff.

nestjs-doctor:
  image: node:22
  script:
    - npx nestjs-doctor@latest . --format gitlab --output gl-code-quality-report.json
  artifacts:
    reports:
      codequality: gl-code-quality-report.json

Other output formats

FormatUse
consoleHuman-readable report (default)
jsonThe full result object, for tooling
sarifSARIF 2.1.0, for code scanning
gitlabGitLab Code Quality report
markdownA comment body for any pull or merge request
githubWorkflow annotations plus a job summary, alongside the console report

--output <path> writes the payload to a file instead of stdout, and --json-compact drops the indentation from the JSON-based formats.

github is additive rather than a replacement: it prints annotations and appends the markdown report to $GITHUB_STEP_SUMMARY while still printing the readable console report to the log. GitHub caps annotations at ten errors and ten warnings per step and silently drops the rest, so the annotations are a convenience — the job summary carries every finding.

Pre-commit hook

# .husky/pre-commit
npx nestjs-doctor . --staged --blocking error

--staged scopes to the files in the git index. The project is still analysed in full, so a change that breaks a cross-file rule is caught even when the file it is reported against is not part of the commit.

Running from inside a hook is safe: git exports GIT_DIR, GIT_INDEX_FILE, and friends to every hook, and children inherit them, so nestjs-doctor clears those before shelling out to git and resolves everything against the directory it was pointed at.