CI & Hooks

Use jscpd in CI

Run jscpd in GitHub Actions, GitLab CI, and other CI systems to enforce duplication thresholds.

GitHub Action

The jscpd-copy-paste-detector GitHub Action runs jscpd in your CI workflow. It installs the Rust engine, runs detection, uploads SARIF to GitHub Code Scanning, and optionally uploads the report as an artifact.

Basic Usage

.github/workflows/jscpd.yml
name: Duplication Check

on: [push, pull_request]

jobs:
  jscpd:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: kucherenko/jscpd@v5

This scans the entire repository with default settings and uploads SARIF results to GitHub Code Scanning.

Fail on Threshold

Set threshold to fail the build when duplication exceeds a percentage:

- uses: kucherenko/jscpd@v5
  with:
    threshold: 5

The workflow fails if more than 5% of the code is duplicated.

Action Inputs

InputDescriptionDefault
pathPaths to scan (space-separated).
configPath to .jscpd.json config file—
min-tokensMinimum tokens for a clone50
min-linesMinimum lines for a clone5
max-linesMaximum lines per block—
modeDetection mode: mild, weak, strictmild
formatComma-separated formats to check—
ignoreComma-separated glob patterns to ignore—
ignore-patternComma-separated regex patterns to skip—
reportersComma-separated reportersconsole
outputOutput directory for file reportersreport
thresholdMax duplication % before exit 1—
baselinePath to a clone baseline file: clones absent from it are reported as new—
update-baselineRewrite the baseline file from the current run (requires baseline)false
fail-on-new-clonesExit 1 on new clones (requires baseline or baseline-from-ref): true for zero tolerance, or an integer N to allow up to N—
baseline-from-refEphemeral baseline from a git ref's tree (e.g. origin/main); needs fetch-depth: 0. Conflicts with baseline—
historyDuplication trend over git history: scan every commit in this range (e.g. v5.0.0..HEAD) and print a chart and table in the log; needs fetch-depth: 0 (5.2.1+)—
blameEnrich clones with git blame datafalse
exit-codeExit with code when duplicates found (true or integer)—
patternGlob pattern for file search—
max-sizeSkip files larger than SIZE—
skip-localSkip clones in same directoryfalse
ignore-caseIgnore case of symbols (experimental)false
follow-symlinksFollow symbolic linksfalse
no-gitignoreDon't respect .gitignore filesfalse
absoluteUse absolute paths in reportsfalse
formats-extsCustom format-to-extension mappings—
formats-namesCustom format-to-filename mappings—
versionjscpd version to installlatest
install-prefixInstallation directory for the binary—
skip-installSkip installation (binary already present)false
extra-argsAdditional arguments passed to jscpd—
upload-reportUpload report directory as artifactfalse
upload-sarifUpload SARIF to GitHub Code Scanningtrue

Action Outputs

OutputDescription
duplication-percentagePercentage of duplicated code found
clones-foundNumber of clone pairs found
duplicated-linesNumber of duplicated lines
total-linesTotal lines scanned
files-countNumber of source files scanned
report-pathPath to the output directory
sarif-pathPath to the SARIF report file
exit-codeExit code from jscpd

Examples

Scan specific directories with threshold

- uses: kucherenko/jscpd@v5
  with:
    path: src/lib src/utils
    threshold: 3
    ignore: "**/*.test.*,**/*.spec.*"

Use a config file

- uses: kucherenko/jscpd@v5
  with:
    config: .jscpd.json
    upload-report: true

Multi-reporter with artifact upload

- uses: kucherenko/jscpd@v5
  with:
    reporters: console,json,html,sarif
    output: jscpd-report
    upload-report: true

Pin a specific version

- uses: kucherenko/jscpd@v5
  with:
    version: "5.0.16"

Skip install (binary already in image)

- uses: kucherenko/jscpd@v5
  with:
    skip-install: true

Use outputs in subsequent steps

- uses: kucherenko/jscpd@v5
  id: jscpd

- name: Check results
  if: steps.jscpd.outputs.duplication-percentage > 5
  run: |
    echo "Duplication is ${{ steps.jscpd.outputs.duplication-percentage }}%"
    echo "Found ${{ steps.jscpd.outputs.clones-found }} clones"

Gate on new duplication only (baseline)

--threshold gates on the aggregate percentage, so a PR can add a fresh copy-paste while staying under the limit — or fail on legacy duplication it didn't touch. The clone baseline gates on new clones only: commit a .jscpd-baseline.json recording the accepted state, and fail the build when a clone appears that is not in it.

# once, locally: record the accepted state and commit the file
jscpd --baseline .jscpd-baseline.json --update-baseline .
git add .jscpd-baseline.json && git commit -m "chore: record jscpd clone baseline"

GitHub Actions

.github/workflows/jscpd.yml
name: Duplication Check

on: [pull_request]

jobs:
  jscpd:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: kucherenko/jscpd@v5
        with:
          baseline: .jscpd-baseline.json
          fail-on-new-clones: true

Legacy duplication recorded in the baseline is tolerated; any new clone fails the check. New clones are reported at level error in the SARIF upload, so GitHub Code Scanning highlights only the regression. When a PR intentionally accepts new duplication, refresh the file with --update-baseline and commit it — the regeneration prints added/removed fingerprint counts, and the baseline diff is reviewable in the PR (sorted, one fingerprint per line).

New-clone information reaches every reporter, so whichever one you already use will show it: [NEW] markers in console/console-full, per-clone isNew plus newClones / newDuplicatedLines statistics in json, level error in sarif, major severity in codeclimate, and the jscpd_new_clones / jscpd_new_duplicated_lines gauges in openmetrics. Fingerprints are content hashes, so they survive line-number shifts, file renames, and CRLF/LF differences between platforms.

GitLab CI

.gitlab-ci.yml
jscpd:
  stage: test
  image: node:20
  before_script:
    - npm install -g jscpd@5
  script:
    - jscpd --baseline .jscpd-baseline.json --fail-on-new-clones --reporters console,openmetrics .
  artifacts:
    reports:
      metrics: report/jscpd-metrics.txt

The openmetrics report includes jscpd_new_clones and jscpd_new_duplicated_lines gauges, so the merge-request metrics widget shows the new-duplication delta.

To allow up to N new clones instead of zero, pass a number: --fail-on-new-clones 3. The gate composes with --threshold — either one failing fails the build.

Stateless variant: compare against a git ref

If you'd rather not commit a baseline file, --baseline-from-ref builds the baseline on the fly from the base branch's tree (checked out into a temporary git worktree and scanned with the same configuration):

- uses: actions/checkout@v4
  with:
    fetch-depth: 0   # the base ref must be present locally
- uses: kucherenko/jscpd@v5
  with:
    baseline-from-ref: origin/main
    fail-on-new-clones: true

Trade-offs versus the committed file: no baseline to maintain and no update workflow, but every run scans the corpus twice, and a missing base ref (shallow checkout) fails the run — the error message tells you to fetch it. With the fast Rust engine the double scan is rarely a problem.

Duplication trend over history

Since 5.2.1, the history input scans every commit in a range and prints a bar chart and a per-commit table in the job log, with the change between points and, when threshold is set, how far it could be tightened. A scheduled job is the natural home for it:

on:
  schedule:
    - cron: "0 6 * * 1"   # Monday morning
jobs:
  trend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # the whole range must be present locally
      - uses: kucherenko/jscpd@v5
        with:
          history: v5.0.0..HEAD
          threshold: 3

Each point costs one scan; --history-every and --history-limit (default 30 points) keep long ranges short. See the history guide for the output and the options.

GitLab CI

Use jscpd in GitLab CI with a simple pipeline job. Since v5.1.0 the codeclimate and openmetrics reporters plug into GitLab's merge-request widgets:

.gitlab-ci.yml
jscpd:
  stage: test
  image: node:22
  before_script:
    - npm install -g jscpd@5
  script:
    - jscpd --threshold 5 --reporters console,codeclimate,openmetrics --output report .
  artifacts:
    reports:
      codequality: report/gl-code-quality-report.json
      metrics: report/jscpd-metrics.txt
    paths:
      - report/

Merge requests then show duplicates as Code Quality issues and duplication Metrics deltas against the target branch.

Generic CI

Any CI system that can run shell commands works with jscpd:

npm install -g jscpd@5
jscpd --threshold 5 ./src

The --threshold flag makes jscpd exit with code 1 when duplication exceeds the specified percentage, which causes CI builds to fail.

Tips

  • Use --reporters console,sarif in CI to get both console output and SARIF for code scanning platforms
  • Use --threshold to set a failure threshold — the process exits with code 1 if exceeded
  • Add --fail-on-empty so a scan that analyzes no files (a wrong path filter, an --ignore that swallows everything) fails the job instead of passing with an empty report; a nonexistent path, an unknown --format and a reporter that cannot write its file exit 1 on their own
  • Use --ignore to exclude generated files, test fixtures, or vendor directories
  • jscpd is a self-contained native binary with no Node.js startup cost; on large repos tune --workers to match the runner's CPU count to keep CI times low
  • Consider --format to limit detection to specific languages during CI, with a full scan in a scheduled job