-
-
Notifications
You must be signed in to change notification settings - Fork 242
Expand file tree
/
Copy pathaction.yml
More file actions
396 lines (370 loc) · 12.7 KB
/
Copy pathaction.yml
File metadata and controls
396 lines (370 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
name: jscpd-copy-paste-detector
description: "Copy/paste detector for source code — find duplicated blocks across 223+ languages (Rust v5)"
branding:
icon: copy
color: blue
inputs:
path:
description: "Paths to scan for duplicates (space-separated)"
required: false
default: "."
config:
description: "Path to .jscpd.json config file"
required: false
default: ""
min-tokens:
description: "Minimum number of tokens to consider a duplicate"
required: false
default: "50"
min-lines:
description: "Minimum number of lines to consider a duplicate"
required: false
default: "5"
max-lines:
description: "Maximum lines per block to consider"
required: false
default: ""
mode:
description: "Detection mode: mild, weak, or strict"
required: false
default: "mild"
format:
description: "Comma-separated list of file extensions/formats to check"
required: false
default: ""
ignore:
description: "Comma-separated glob patterns to ignore"
required: false
default: ""
ignore-pattern:
description: "Comma-separated regex patterns to skip matching tokens"
required: false
default: ""
reporters:
description: "Comma-separated list of reporters (console, json, xml, csv, html, markdown, badge, sarif, ai, xcode, console-full, threshold, silent)"
required: false
default: "console"
output:
description: "Output directory for file reporters"
required: false
default: "report"
threshold:
description: "Maximum duplication percentage before exit 1"
required: false
default: ""
blame:
description: "Enrich clones with git blame data"
required: false
default: "false"
exit-code:
description: "Exit with code when duplicates found. Use 'true' for default code 1, or an integer like '2' for a custom code"
required: false
default: ""
pattern:
description: "Glob pattern to find files to scan"
required: false
default: ""
max-size:
description: "Skip files larger than SIZE (e.g. 100kb, 1mb)"
required: false
default: ""
skip-local:
description: "Skip clones where both fragments are in the same directory"
required: false
default: "false"
ignore-case:
description: "Ignore case of symbols (experimental)"
required: false
default: "false"
follow-symlinks:
description: "Follow symbolic links"
required: false
default: "false"
no-gitignore:
description: "Do not respect .gitignore files"
required: false
default: "false"
absolute:
description: "Use absolute paths in reports"
required: false
default: "false"
formats-exts:
description: "Custom format-to-extension mappings (e.g. javascript:es,es6;dart:dt)"
required: false
default: ""
formats-names:
description: "Custom format-to-filename mappings (e.g. makefile:Makefile,GNUmakefile;docker:Dockerfile)"
required: false
default: ""
version:
description: "Version of jscpd to install (default: latest)"
required: false
default: "latest"
install-prefix:
description: "Installation directory for the cpd binary (default: /usr/local/bin on Linux/macOS, npm global bin on Windows)"
required: false
default: ""
skip-install:
description: "Skip installation (use when jscpd/cpd is already installed)"
required: false
default: "false"
extra-args:
description: "Additional arguments passed directly to cpd/jscpd"
required: false
default: ""
upload-report:
description: "Upload the report directory as a workflow artifact"
required: false
default: "false"
upload-sarif:
description: "Upload SARIF report to GitHub Code Scanning"
required: false
default: "true"
outputs:
duplication-percentage:
description: "Percentage of duplicated code found"
value: ${{ steps.parse-output.outputs.duplication-percentage }}
clones-found:
description: "Number of clone pairs found"
value: ${{ steps.parse-output.outputs.clones-found }}
duplicated-lines:
description: "Number of duplicated lines"
value: ${{ steps.parse-output.outputs.duplicated-lines }}
total-lines:
description: "Total number of lines scanned"
value: ${{ steps.parse-output.outputs.total-lines }}
files-count:
description: "Number of source files scanned"
value: ${{ steps.parse-output.outputs.files-count }}
report-path:
description: "Path to the output directory"
value: ${{ steps.parse-output.outputs.report-path }}
sarif-path:
description: "Path to the SARIF report file (empty if not generated)"
value: ${{ steps.parse-output.outputs.sarif-path }}
exit-code:
description: "Exit code from jscpd (non-zero if threshold exceeded or --exit-code used)"
value: ${{ steps.run-jscpd.outputs.exit-code }}
runs:
using: composite
steps:
- name: Install jscpd (Linux/macOS)
if: runner.os != 'Windows' && inputs.skip-install != 'true'
shell: bash
env:
JSCPD_VERSION: ${{ inputs.version }}
JSCPD_PREFIX: ${{ inputs.install-prefix }}
run: |
ARGS=""
if [ -n "$JSCPD_VERSION" ] && [ "$JSCPD_VERSION" != "latest" ]; then
ARGS="$ARGS --version $JSCPD_VERSION"
fi
if [ -n "$JSCPD_PREFIX" ]; then
ARGS="$ARGS --prefix $JSCPD_PREFIX"
fi
# Try install script first (downloads from GitHub Releases, falls back to npm)
for attempt in 1 2 3; do
if curl -fsSL --retry 3 --retry-delay 5 https://jscpd.dev/install.sh | bash -s -- $ARGS; then
break
fi
echo "Install attempt $attempt failed, retrying in 10s..."
sleep 10
done
# Fallback: if install script failed, try npm directly
if ! command -v cpd >/dev/null 2>&1 && ! command -v jscpd >/dev/null 2>&1; then
echo "Install script failed, falling back to npm install..."
if [ -n "$JSCPD_VERSION" ] && [ "$JSCPD_VERSION" != "latest" ]; then
npm install -g "jscpd@$JSCPD_VERSION" || npm install -g "cpd@$JSCPD_VERSION"
else
npm install -g jscpd || npm install -g cpd
fi
fi
- name: Install jscpd (Windows)
if: runner.os == 'Windows' && inputs.skip-install != 'true'
shell: pwsh
env:
JSCPD_VERSION: ${{ inputs.version }}
run: |
$version = $env:JSCPD_VERSION
$pkgs = if ($version -eq "latest" -or [string]::IsNullOrEmpty($version)) {
@("jscpd", "cpd")
} else {
@("jscpd@$version", "cpd@$version")
}
$installed = $false
foreach ($pkg in $pkgs) {
try {
npm install -g $pkg
$installed = $true
break
} catch {
Write-Host "npm install -g $pkg failed, trying next..."
Start-Sleep -Seconds 5
}
}
if (-not $installed) {
# Retry once
foreach ($pkg in $pkgs) {
try {
npm install -g $pkg
$installed = $true
break
} catch {
Write-Host "npm install -g $pkg retry failed"
}
}
}
if (-not $installed) {
Write-Error "Failed to install jscpd via npm"
exit 1
}
- name: Run jscpd
id: run-jscpd
shell: bash
env:
INPUT_PATH: ${{ inputs.path }}
INPUT_CONFIG: ${{ inputs.config }}
INPUT_MIN_TOKENS: ${{ inputs.min-tokens }}
INPUT_MIN_LINES: ${{ inputs.min-lines }}
INPUT_MAX_LINES: ${{ inputs.max-lines }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_FORMAT: ${{ inputs.format }}
INPUT_IGNORE: ${{ inputs.ignore }}
INPUT_IGNORE_PATTERN: ${{ inputs.ignore-pattern }}
INPUT_REPORTERS: ${{ inputs.reporters }}
INPUT_OUTPUT: ${{ inputs.output }}
INPUT_THRESHOLD: ${{ inputs.threshold }}
INPUT_BLAME: ${{ inputs.blame }}
INPUT_EXIT_CODE: ${{ inputs.exit-code }}
INPUT_PATTERN: ${{ inputs.pattern }}
INPUT_MAX_SIZE: ${{ inputs.max-size }}
INPUT_SKIP_LOCAL: ${{ inputs.skip-local }}
INPUT_IGNORE_CASE: ${{ inputs.ignore-case }}
INPUT_FOLLOW_SYMLINKS: ${{ inputs.follow-symlinks }}
INPUT_NO_GITIGNORE: ${{ inputs.no-gitignore }}
INPUT_ABSOLUTE: ${{ inputs.absolute }}
INPUT_FORMATS_EXTS: ${{ inputs.formats-exts }}
INPUT_FORMATS_NAMES: ${{ inputs.formats-names }}
INPUT_EXTRA_ARGS: ${{ inputs.extra-args }}
run: |
# Ensure json reporter is always included for output parsing
REPORTERS="$INPUT_REPORTERS"
if echo "$REPORTERS" | grep -qv "json"; then
if [ -z "$REPORTERS" ]; then
REPORTERS="json"
else
REPORTERS="${REPORTERS},json"
fi
fi
# Build the command
ARGS=()
# Positional paths
if [ -n "$INPUT_PATH" ]; then
for p in $INPUT_PATH; do
ARGS+=("$p")
done
fi
# Config file
if [ -n "$INPUT_CONFIG" ]; then
ARGS+=("--config" "$INPUT_CONFIG")
fi
# Numeric options (only add if non-default)
if [ -n "$INPUT_MIN_TOKENS" ] && [ "$INPUT_MIN_TOKENS" != "50" ]; then
ARGS+=("--min-tokens" "$INPUT_MIN_TOKENS")
fi
if [ -n "$INPUT_MIN_LINES" ] && [ "$INPUT_MIN_LINES" != "5" ]; then
ARGS+=("--min-lines" "$INPUT_MIN_LINES")
fi
if [ -n "$INPUT_MAX_LINES" ]; then
ARGS+=("--max-lines" "$INPUT_MAX_LINES")
fi
# Mode
if [ -n "$INPUT_MODE" ] && [ "$INPUT_MODE" != "mild" ]; then
ARGS+=("--mode" "$INPUT_MODE")
fi
# Format and ignore
if [ -n "$INPUT_FORMAT" ]; then
ARGS+=("--format" "$INPUT_FORMAT")
fi
if [ -n "$INPUT_IGNORE" ]; then
ARGS+=("--ignore" "$INPUT_IGNORE")
fi
if [ -n "$INPUT_IGNORE_PATTERN" ]; then
ARGS+=("--ignore-pattern" "$INPUT_IGNORE_PATTERN")
fi
# Reporters and output
ARGS+=("--reporters" "$REPORTERS")
ARGS+=("--output" "$INPUT_OUTPUT")
# Threshold
if [ -n "$INPUT_THRESHOLD" ]; then
ARGS+=("--threshold" "$INPUT_THRESHOLD")
fi
# Boolean flags
if [ "$INPUT_BLAME" = "true" ]; then
ARGS+=("--blame")
fi
# Exit code: empty=omit, 'true'=bare flag, integer=value
if [ -n "$INPUT_EXIT_CODE" ]; then
if [ "$INPUT_EXIT_CODE" = "true" ]; then
ARGS+=("--exit-code")
else
ARGS+=("--exit-code" "$INPUT_EXIT_CODE")
fi
fi
# Pattern
if [ -n "$INPUT_PATTERN" ]; then
ARGS+=("--pattern" "$INPUT_PATTERN")
fi
# Max size (only if non-default)
if [ -n "$INPUT_MAX_SIZE" ] && [ "$INPUT_MAX_SIZE" != "1mb" ]; then
ARGS+=("--max-size" "$INPUT_MAX_SIZE")
fi
# Boolean flags
if [ "$INPUT_SKIP_LOCAL" = "true" ]; then
ARGS+=("--skip-local")
fi
if [ "$INPUT_IGNORE_CASE" = "true" ]; then
ARGS+=("--ignore-case")
fi
if [ "$INPUT_FOLLOW_SYMLINKS" = "true" ]; then
ARGS+=("--follow-symlinks")
fi
if [ "$INPUT_NO_GITIGNORE" = "true" ]; then
ARGS+=("--no-gitignore")
fi
if [ "$INPUT_ABSOLUTE" = "true" ]; then
ARGS+=("--absolute")
fi
# Format mappings
if [ -n "$INPUT_FORMATS_EXTS" ]; then
ARGS+=("--formats-exts" "$INPUT_FORMATS_EXTS")
fi
if [ -n "$INPUT_FORMATS_NAMES" ]; then
ARGS+=("--formats-names" "$INPUT_FORMATS_NAMES")
fi
# Extra args escape hatch
if [ -n "$INPUT_EXTRA_ARGS" ]; then
ARGS+=($INPUT_EXTRA_ARGS)
fi
# Run the detection, capture exit code for threshold/exit-code handling
# Post-steps use `if: always()` so they execute regardless of exit code
jscpd "${ARGS[@]}" || EXIT_CODE=$?
echo "exit-code=${EXIT_CODE:-0}" >> "$GITHUB_OUTPUT"
- name: Parse output
id: parse-output
if: always()
shell: bash
run: node "${{ github.action_path }}/.github/actions/jscpd/parse-output.js" "${{ inputs.output }}"
- name: Upload SARIF to GitHub Code Scanning
if: inputs.upload-sarif == 'true' && always() && steps.parse-output.outputs.sarif-path != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.parse-output.outputs.sarif-path }}
continue-on-error: true
- name: Upload report artifact
if: inputs.upload-report == 'true' && always()
uses: actions/upload-artifact@v4
with:
name: jscpd-report
path: ${{ inputs.output }}
continue-on-error: true