DUP001 — near-duplicate-job¶
Severity: info · Category: Duplication · Autofix: no
What it checks¶
Flags a job that's structurally near-identical to another job found
elsewhere in the scan — the same shape of steps (same uses: actions,
near-identical run: scripts), possibly with a different version pin or
a different secret/ref referenced. Only jobs with at least 3 steps are
considered, and only pairs at or above 90% structural similarity — see
internal/fingerprint for
how the signature is computed.
Unlike every other rule, this isn't a rules.Violation under the hood
(a cluster can span two different files, which a single Violation's
one Path can't represent) — but it supports the same suppression
mechanisms as every other rule, described below.
Why it matters¶
A job copy-pasted across workflow files tends to drift: one copy gets a
version bump or a security fix, the other doesn't, silently. Finding the
duplication is the first step toward collapsing it into a single
reusable workflow or composite action — see
docs/adr/0004-duplicate-job-fingerprinting.md
for the full design, including a documented case where two genuinely
different jobs (two different security scanners) matched only because
they shared one copy-pasted guard clause — this rule is a signal to look
closer, not an automatic verdict.
Examples¶
Flagged — same steps, only the test suite name and artifact path differ:
jobs:
test-frontend:
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --suite frontend
test-backend:
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --suite backend
Fixed — collapsed into one job with a matrix:
jobs:
test:
strategy:
matrix:
suite: [frontend, backend]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --suite ${{ matrix.suite }}
Suppressing¶
One job, inline — on the job's key line:
jobs:
test-frontend: # vlotpipe: ignore[DUP001]
Repo-wide, with a reason on record:
ignore:
- code: DUP001
path: "*"
reason: "shared guard clause, not a real duplicate — see dast.yml"
Or the bare-code shorthand for the same thing, no reason recorded:
ignore:
- DUP001
Output¶
vlotpipe scan/check print every cluster found (text format only —
--format json/github/azure-devops are unaffected), each member's
exact path:line and job name, and the cluster's weakest pairwise
similarity:
1 duplicate job cluster found:
cluster 1 (2 jobs, 94% similar):
.github/workflows/tests.yml:36 job "Test Frontend"
.github/workflows/tests.yml:52 job "Test Backend"
This is entirely local, single-scan output — computed for free, no dashboard required. Aggregating this across every repo in an org (drift tracking, a golden-template suggestion) is the paid Insights tier; a single scan of one repo can't do that regardless of how much detail it prints about itself.