Skip to content

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.