Use jscpd in CI
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
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
| Input | Description | Default |
|---|---|---|
path | Paths to scan (space-separated) | . |
config | Path to .jscpd.json config file | — |
min-tokens | Minimum tokens for a clone | 50 |
min-lines | Minimum lines for a clone | 5 |
max-lines | Maximum lines per block | — |
mode | Detection mode: mild, weak, strict | mild |
format | Comma-separated formats to check | — |
ignore | Comma-separated glob patterns to ignore | — |
ignore-pattern | Comma-separated regex patterns to skip | — |
reporters | Comma-separated reporters | console |
output | Output directory for file reporters | report |
threshold | Max duplication % before exit 1 | — |
baseline | Path to a clone baseline file: clones absent from it are reported as new | — |
update-baseline | Rewrite the baseline file from the current run (requires baseline) | false |
fail-on-new-clones | Exit 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-ref | Ephemeral baseline from a git ref's tree (e.g. origin/main); needs fetch-depth: 0. Conflicts with baseline | — |
history | Duplication 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+) | — |
blame | Enrich clones with git blame data | false |
exit-code | Exit with code when duplicates found (true or integer) | — |
pattern | Glob pattern for file search | — |
max-size | Skip files larger than SIZE | — |
skip-local | Skip clones in same directory | false |
ignore-case | Ignore case of symbols (experimental) | false |
follow-symlinks | Follow symbolic links | false |
no-gitignore | Don't respect .gitignore files | false |
absolute | Use absolute paths in reports | false |
formats-exts | Custom format-to-extension mappings | — |
formats-names | Custom format-to-filename mappings | — |
version | jscpd version to install | latest |
install-prefix | Installation directory for the binary | — |
skip-install | Skip installation (binary already present) | false |
extra-args | Additional arguments passed to jscpd | — |
upload-report | Upload report directory as artifact | false |
upload-sarif | Upload SARIF to GitHub Code Scanning | true |
Action Outputs
| Output | Description |
|---|---|
duplication-percentage | Percentage of duplicated code found |
clones-found | Number of clone pairs found |
duplicated-lines | Number of duplicated lines |
total-lines | Total lines scanned |
files-count | Number of source files scanned |
report-path | Path to the output directory |
sarif-path | Path to the SARIF report file |
exit-code | Exit 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
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
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:
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,sarifin CI to get both console output and SARIF for code scanning platforms - Use
--thresholdto set a failure threshold — the process exits with code 1 if exceeded - Add
--fail-on-emptyso a scan that analyzes no files (a wrong path filter, an--ignorethat swallows everything) fails the job instead of passing with an empty report; a nonexistent path, an unknown--formatand a reporter that cannot write its file exit 1 on their own - Use
--ignoreto 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
--workersto match the runner's CPU count to keep CI times low - Consider
--formatto limit detection to specific languages during CI, with a full scan in a scheduled job