action-pinner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/action.yml +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/action-mode.d.ts +1 -0
- package/dist/src/action-mode.js +109 -0
- package/dist/src/action-mode.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +780 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +291 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/dependabot.d.ts +1 -0
- package/dist/src/dependabot.js +11 -0
- package/dist/src/dependabot.js.map +1 -0
- package/dist/src/enforcement.d.ts +12 -0
- package/dist/src/enforcement.js +238 -0
- package/dist/src/enforcement.js.map +1 -0
- package/dist/src/github-app.d.ts +6 -0
- package/dist/src/github-app.js +4 -0
- package/dist/src/github-app.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logging.d.ts +8 -0
- package/dist/src/logging.js +38 -0
- package/dist/src/logging.js.map +1 -0
- package/dist/src/multi-repo-scanner.d.ts +69 -0
- package/dist/src/multi-repo-scanner.js +121 -0
- package/dist/src/multi-repo-scanner.js.map +1 -0
- package/dist/src/netrc-auth.d.ts +13 -0
- package/dist/src/netrc-auth.js +123 -0
- package/dist/src/netrc-auth.js.map +1 -0
- package/dist/src/org.d.ts +49 -0
- package/dist/src/org.js +162 -0
- package/dist/src/org.js.map +1 -0
- package/dist/src/pattern-match.d.ts +5 -0
- package/dist/src/pattern-match.js +59 -0
- package/dist/src/pattern-match.js.map +1 -0
- package/dist/src/pinner.d.ts +6 -0
- package/dist/src/pinner.js +148 -0
- package/dist/src/pinner.js.map +1 -0
- package/dist/src/pr.d.ts +87 -0
- package/dist/src/pr.js +165 -0
- package/dist/src/pr.js.map +1 -0
- package/dist/src/report.d.ts +10 -0
- package/dist/src/report.js +54 -0
- package/dist/src/report.js.map +1 -0
- package/dist/src/resolver.d.ts +44 -0
- package/dist/src/resolver.js +227 -0
- package/dist/src/resolver.js.map +1 -0
- package/dist/src/scanner.d.ts +8 -0
- package/dist/src/scanner.js +128 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/types.d.ts +170 -0
- package/dist/src/types.js +41 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +22 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/workflow-paths.d.ts +4 -0
- package/dist/src/workflow-paths.js +29 -0
- package/dist/src/workflow-paths.js.map +1 -0
- package/package.json +62 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { simpleGit } from "simple-git";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { generateDependabotActionsSnippet } from "./dependabot.js";
|
|
6
|
+
import { pinReferences } from "./pinner.js";
|
|
7
|
+
import { createPullRequestBranch, publishPullRequest } from "./pr.js";
|
|
8
|
+
import { buildRunFingerprint, formatEvidence } from "./report.js";
|
|
9
|
+
import { ActionResolver } from "./resolver.js";
|
|
10
|
+
import { scanWorkflows } from "./scanner.js";
|
|
11
|
+
import { scanRepositories } from "./multi-repo-scanner.js";
|
|
12
|
+
import { applyEnforcementExceptions, evaluateEnforcement, evaluateMultiRepoEnforcement } from "./enforcement.js";
|
|
13
|
+
import { AmbiguousRefError, UnresolvedRefError } from "./types.js";
|
|
14
|
+
import { getToolVersion } from "./version.js";
|
|
15
|
+
import { resolveWorkflowPatterns, toDisplayPath } from "./workflow-paths.js";
|
|
16
|
+
import { safeLog } from "./logging.js";
|
|
17
|
+
import { filterRepositoryMetadata, listOwnerRepositories } from "./org.js";
|
|
18
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name("action-pinner")
|
|
22
|
+
.description("Pin GitHub Action refs to immutable commit SHAs.")
|
|
23
|
+
.version("0.1.0")
|
|
24
|
+
.addHelpText("after", `
|
|
25
|
+
SECURITY & TRUST
|
|
26
|
+
|
|
27
|
+
Fail-Closed Behavior:
|
|
28
|
+
If a ref cannot be resolved to a SHA, action-pinner fails (exit 1).
|
|
29
|
+
Use --continue-on-error to skip unresolved refs (logged as warnings).
|
|
30
|
+
|
|
31
|
+
Token Safety:
|
|
32
|
+
Your GitHub token is never logged, even in verbose mode.
|
|
33
|
+
All credential data is automatically redacted from logs.
|
|
34
|
+
Use minimal scopes: contents:read for read-only, add pull_requests:write for PR creation.
|
|
35
|
+
Never use: admin:repo_hook, admin:org, or full repo scopes.
|
|
36
|
+
|
|
37
|
+
Deterministic Output:
|
|
38
|
+
All scans and rewrites are stable across runs on the same input.
|
|
39
|
+
Evidence fingerprints support audit trails and reproducible results.
|
|
40
|
+
|
|
41
|
+
EXAMPLES
|
|
42
|
+
|
|
43
|
+
Scan for unpinned actions:
|
|
44
|
+
$ action-pinner scan
|
|
45
|
+
|
|
46
|
+
GitHub Enterprise Server:
|
|
47
|
+
$ action-pinner scan --github-api-url https://enterprise.example.com/api/v3 --token $GHES_TOKEN
|
|
48
|
+
|
|
49
|
+
Private repositories with .netrc:
|
|
50
|
+
$ action-pinner scan --use-netrc
|
|
51
|
+
|
|
52
|
+
Dry run to preview changes:
|
|
53
|
+
$ action-pinner fix --dry-run
|
|
54
|
+
|
|
55
|
+
Pin all unpinned actions:
|
|
56
|
+
$ action-pinner fix
|
|
57
|
+
|
|
58
|
+
Enforce policy in CI:
|
|
59
|
+
$ action-pinner enforce
|
|
60
|
+
|
|
61
|
+
Multi-repo targeting:
|
|
62
|
+
$ action-pinner scan --github-org acme --include-repo "platform-*" --exclude-repo "*-archive"
|
|
63
|
+
$ action-pinner scan --github-user octocat --include-repo "demo-*"
|
|
64
|
+
|
|
65
|
+
Filter workflow paths and actions:
|
|
66
|
+
$ action-pinner scan --exclude-path ".github/workflows/legacy/**" --exclude-action "actions/cache"
|
|
67
|
+
|
|
68
|
+
Open a PR with pinned updates:
|
|
69
|
+
$ action-pinner pr --open
|
|
70
|
+
|
|
71
|
+
Continue on errors:
|
|
72
|
+
$ action-pinner scan --continue-on-error
|
|
73
|
+
|
|
74
|
+
AUTHENTICATION PRECEDENCE
|
|
75
|
+
|
|
76
|
+
1. CLI --token flag
|
|
77
|
+
2. PIN_ACTIONS_TOKEN environment variable
|
|
78
|
+
3. .netrc file (if --use-netrc enabled)
|
|
79
|
+
4. GITHUB_TOKEN environment variable (GitHub Actions)
|
|
80
|
+
5. Anonymous (rate-limited to 60 requests/hour)
|
|
81
|
+
|
|
82
|
+
See SECURITY.md for detailed security guidance.
|
|
83
|
+
See docs/ENTERPRISE.md for enterprise deployments.
|
|
84
|
+
`);
|
|
85
|
+
program
|
|
86
|
+
.command("scan")
|
|
87
|
+
.option("--config <path>", "Path to .action-pinner.json", ".action-pinner.json")
|
|
88
|
+
.option("-p, --path <path...>", "Workflow file, directory, or glob to scan")
|
|
89
|
+
.option("--exclude-path <path...>", "Workflow file, directory, or glob to exclude")
|
|
90
|
+
.option("--include-action <pattern...>", "Only include action refs matching these patterns")
|
|
91
|
+
.option("--exclude-action <pattern...>", "Exclude action refs matching these patterns")
|
|
92
|
+
.option("--repo <repo...>", "Explicit repository targets (owner/repo) for multi-repo scans")
|
|
93
|
+
.option("--github-org <org>", "Organization to enumerate repositories from")
|
|
94
|
+
.option("--github-user <user>", "User account to enumerate repositories from")
|
|
95
|
+
.option("--include-repo <pattern...>", "Include only repositories matching these patterns")
|
|
96
|
+
.option("--exclude-repo <pattern...>", "Exclude repositories matching these patterns")
|
|
97
|
+
.option("--json", "Emit JSON output", false)
|
|
98
|
+
.option("--token <token>", "GitHub token for API authentication (overrides env vars)")
|
|
99
|
+
.option("--github-api-url <url>", "GitHub API base URL (for GHES deployments)")
|
|
100
|
+
.option("--use-netrc", "Use .netrc for authentication (if --token not provided)", false)
|
|
101
|
+
.action(async (opts) => {
|
|
102
|
+
const config = await loadConfig(opts.config);
|
|
103
|
+
const include = resolveIncludePatterns(opts.path, config.include);
|
|
104
|
+
const exclude = resolveExcludePatterns(opts.excludePath, config.exclude);
|
|
105
|
+
const includeActions = resolveStringList(opts.includeAction, []);
|
|
106
|
+
const excludeActions = resolveStringList(opts.excludeAction, config.excludeActions);
|
|
107
|
+
const toolVersion = await getToolVersion();
|
|
108
|
+
const fingerprint = buildRunFingerprint(config, toolVersion);
|
|
109
|
+
const token = opts.token || process.env.PIN_ACTIONS_TOKEN || process.env.GITHUB_TOKEN;
|
|
110
|
+
const targets = await resolveRepoTargets(opts, config, token);
|
|
111
|
+
const requestedMultiRepo = Boolean(targets.targetName) ||
|
|
112
|
+
targets.explicitRepositories.length > 0 ||
|
|
113
|
+
targets.includePatterns.length > 0 ||
|
|
114
|
+
targets.excludePatterns.length > 0;
|
|
115
|
+
if (targets.repositories.length > 0) {
|
|
116
|
+
const rawResult = await scanRepositories(targets.repositoryTargets, {
|
|
117
|
+
includePatterns: include,
|
|
118
|
+
excludePatterns: exclude,
|
|
119
|
+
includeActions,
|
|
120
|
+
excludeActions,
|
|
121
|
+
token,
|
|
122
|
+
githubApiUrl: opts.githubApiUrl || config.githubApiUrl
|
|
123
|
+
});
|
|
124
|
+
const result = applyEnforcementExceptionsToMultiRepo(rawResult, config.enforcement.exceptions);
|
|
125
|
+
printMultiRepoScan(result, targets, fingerprint, {
|
|
126
|
+
command: "scan",
|
|
127
|
+
target: "multi-repo",
|
|
128
|
+
output: opts.json ? "json" : "text"
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (requestedMultiRepo) {
|
|
133
|
+
printMultiRepoScan({
|
|
134
|
+
repositories: [],
|
|
135
|
+
consolidated: {
|
|
136
|
+
summary: {
|
|
137
|
+
filesScanned: 0,
|
|
138
|
+
referencesFound: 0,
|
|
139
|
+
unpinnedFound: 0
|
|
140
|
+
},
|
|
141
|
+
references: [],
|
|
142
|
+
unpinned: []
|
|
143
|
+
},
|
|
144
|
+
summary: {
|
|
145
|
+
repositoriesScanned: 0,
|
|
146
|
+
repositoriesWithUnpinned: 0,
|
|
147
|
+
filesScanned: 0,
|
|
148
|
+
referencesFound: 0,
|
|
149
|
+
unpinnedFound: 0
|
|
150
|
+
}
|
|
151
|
+
}, targets, fingerprint, {
|
|
152
|
+
command: "scan",
|
|
153
|
+
target: "multi-repo",
|
|
154
|
+
output: opts.json ? "json" : "text"
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const result = applyEnforcementExceptions(await scanWorkflows(include, process.cwd(), {
|
|
159
|
+
excludePatterns: exclude,
|
|
160
|
+
includeActions,
|
|
161
|
+
excludeActions
|
|
162
|
+
}), config.enforcement.exceptions);
|
|
163
|
+
printScan(result, config, fingerprint, {
|
|
164
|
+
command: "scan",
|
|
165
|
+
target: "local",
|
|
166
|
+
output: opts.json ? "json" : "text"
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
program
|
|
170
|
+
.command("fix")
|
|
171
|
+
.option("--dry-run", "Do not write files", false)
|
|
172
|
+
.option("--config <path>", "Path to .action-pinner.json", ".action-pinner.json")
|
|
173
|
+
.option("-p, --path <path...>", "Workflow file, directory, or glob to scan")
|
|
174
|
+
.option("--exclude-path <path...>", "Workflow file, directory, or glob to exclude")
|
|
175
|
+
.option("--include-action <pattern...>", "Only include action refs matching these patterns")
|
|
176
|
+
.option("--exclude-action <pattern...>", "Exclude action refs matching these patterns")
|
|
177
|
+
.option("--repo <repo...>", "Explicit repository targets (owner/repo) for multi-repo scans")
|
|
178
|
+
.option("--github-org <org>", "Organization to enumerate repositories from")
|
|
179
|
+
.option("--github-user <user>", "User account to enumerate repositories from")
|
|
180
|
+
.option("--include-repo <pattern...>", "Include only repositories matching these patterns")
|
|
181
|
+
.option("--exclude-repo <pattern...>", "Exclude repositories matching these patterns")
|
|
182
|
+
.option("--continue-on-error", "Skip unresolved refs instead of failing", false)
|
|
183
|
+
.option("--fail-on-ambiguous", "Fail on ambiguous refs (security mode)", false)
|
|
184
|
+
.option("--comment-format <template>", "Version comment template; tokens: {ref}, {action}, {sha_short}")
|
|
185
|
+
.option("--token <token>", "GitHub token for API authentication (overrides env vars)")
|
|
186
|
+
.option("--github-api-url <url>", "GitHub API base URL (for GHES deployments)")
|
|
187
|
+
.option("--use-netrc", "Use .netrc for authentication (if --token not provided)", false)
|
|
188
|
+
.action(async (opts) => {
|
|
189
|
+
const config = applyCommentFormatOverride(await loadConfig(opts.config), opts.commentFormat);
|
|
190
|
+
const include = resolveIncludePatterns(opts.path, config.include);
|
|
191
|
+
const exclude = resolveExcludePatterns(opts.excludePath, config.exclude);
|
|
192
|
+
const includeActions = resolveStringList(opts.includeAction, []);
|
|
193
|
+
const excludeActions = resolveStringList(opts.excludeAction, config.excludeActions);
|
|
194
|
+
const result = applyEnforcementExceptions(await scanWorkflows(include, process.cwd(), {
|
|
195
|
+
excludePatterns: exclude,
|
|
196
|
+
includeActions,
|
|
197
|
+
excludeActions
|
|
198
|
+
}), config.enforcement.exceptions);
|
|
199
|
+
const token = opts.token || process.env.PIN_ACTIONS_TOKEN || process.env.GITHUB_TOKEN;
|
|
200
|
+
const resolver = new ActionResolver(token, undefined, {
|
|
201
|
+
apiBaseUrl: opts.githubApiUrl || config.githubApiUrl,
|
|
202
|
+
useNetrc: Boolean(opts.useNetrc || config.useNetrc),
|
|
203
|
+
verbose: false
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
const patches = await pinReferences(result.unpinned, resolver, config, Boolean(opts.dryRun), {
|
|
207
|
+
continueOnError: Boolean(opts.continueOnError),
|
|
208
|
+
failOnAmbiguous: Boolean(opts.failOnAmbiguous)
|
|
209
|
+
});
|
|
210
|
+
const toolVersion = await getToolVersion();
|
|
211
|
+
const fingerprint = buildRunFingerprint(config, toolVersion);
|
|
212
|
+
const runDetails = {
|
|
213
|
+
command: "fix",
|
|
214
|
+
target: "local",
|
|
215
|
+
output: "text",
|
|
216
|
+
dryRun: Boolean(opts.dryRun),
|
|
217
|
+
continueOnError: Boolean(opts.continueOnError),
|
|
218
|
+
failOnAmbiguous: Boolean(opts.failOnAmbiguous)
|
|
219
|
+
};
|
|
220
|
+
if (opts.dryRun) {
|
|
221
|
+
console.log(`Dry run complete. ${patches.length} file(s) would be updated across ${countUpdatedReferences(patches)} reference(s).`);
|
|
222
|
+
printDryRunPreview(patches, fingerprint, runDetails);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(`Updated ${patches.length} file(s) across ${countUpdatedReferences(patches)} reference(s).`);
|
|
226
|
+
printEvidenceReport(patches);
|
|
227
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error instanceof AmbiguousRefError || error instanceof UnresolvedRefError) {
|
|
232
|
+
console.error(safeLog(`Error: ${error.message}`));
|
|
233
|
+
console.error(safeLog(JSON.stringify(error.details, null, 2)));
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
program
|
|
242
|
+
.command("enforce")
|
|
243
|
+
.option("--config <path>", "Path to .action-pinner.json", ".action-pinner.json")
|
|
244
|
+
.option("-p, --path <path...>", "Workflow file, directory, or glob to scan")
|
|
245
|
+
.option("--exclude-path <path...>", "Workflow file, directory, or glob to exclude")
|
|
246
|
+
.option("--include-action <pattern...>", "Only include action refs matching these patterns")
|
|
247
|
+
.option("--exclude-action <pattern...>", "Exclude action refs matching these patterns")
|
|
248
|
+
.option("--repo <repo...>", "Explicit repository targets (owner/repo) for multi-repo scans")
|
|
249
|
+
.option("--github-org <org>", "Organization to enumerate repositories from")
|
|
250
|
+
.option("--github-user <user>", "User account to enumerate repositories from")
|
|
251
|
+
.option("--include-repo <pattern...>", "Include only repositories matching these patterns")
|
|
252
|
+
.option("--exclude-repo <pattern...>", "Exclude repositories matching these patterns")
|
|
253
|
+
.option("--allow-action <pattern...>", "Enforcement allowlist patterns")
|
|
254
|
+
.option("--exception <rule...>", "Enforcement exception rule: <action>[@ref][::workflow-glob]")
|
|
255
|
+
.option("--json", "Emit JSON output", false)
|
|
256
|
+
.option("--continue-on-error", "Skip unresolved refs instead of failing", false)
|
|
257
|
+
.option("--fail-on-ambiguous", "Fail on ambiguous refs (security mode)", false)
|
|
258
|
+
.option("--token <token>", "GitHub token for API authentication (overrides env vars)")
|
|
259
|
+
.option("--github-api-url <url>", "GitHub API base URL (for GHES deployments)")
|
|
260
|
+
.option("--use-netrc", "Use .netrc for authentication (if --token not provided)", false)
|
|
261
|
+
.action(async (opts) => {
|
|
262
|
+
const config = await loadConfig(opts.config);
|
|
263
|
+
const include = resolveIncludePatterns(opts.path, config.include);
|
|
264
|
+
const exclude = resolveExcludePatterns(opts.excludePath, config.exclude);
|
|
265
|
+
const allowActions = resolveStringList(opts.allowAction, config.enforcement.allowActions);
|
|
266
|
+
const includeActions = resolveStringList(opts.includeAction, []);
|
|
267
|
+
const excludeActions = resolveStringList(opts.excludeAction, config.excludeActions);
|
|
268
|
+
const exceptions = [
|
|
269
|
+
...config.enforcement.exceptions,
|
|
270
|
+
...parseExceptionRules(opts.exception)
|
|
271
|
+
];
|
|
272
|
+
const token = opts.token || process.env.PIN_ACTIONS_TOKEN || process.env.GITHUB_TOKEN;
|
|
273
|
+
const targets = await resolveRepoTargets(opts, config, token);
|
|
274
|
+
const requestedMultiRepo = Boolean(targets.targetName) ||
|
|
275
|
+
targets.explicitRepositories.length > 0 ||
|
|
276
|
+
targets.includePatterns.length > 0 ||
|
|
277
|
+
targets.excludePatterns.length > 0;
|
|
278
|
+
const toolVersion = await getToolVersion();
|
|
279
|
+
const fingerprint = buildRunFingerprint(config, toolVersion);
|
|
280
|
+
const runDetails = {
|
|
281
|
+
command: "enforce",
|
|
282
|
+
target: targets.repositories.length > 0 || requestedMultiRepo ? "multi-repo" : "local",
|
|
283
|
+
output: opts.json ? "json" : "text"
|
|
284
|
+
};
|
|
285
|
+
if (targets.repositories.length > 0) {
|
|
286
|
+
const rawResult = await scanRepositories(targets.repositoryTargets, {
|
|
287
|
+
includePatterns: include,
|
|
288
|
+
excludePatterns: exclude,
|
|
289
|
+
includeActions,
|
|
290
|
+
excludeActions,
|
|
291
|
+
token,
|
|
292
|
+
githubApiUrl: opts.githubApiUrl || config.githubApiUrl
|
|
293
|
+
});
|
|
294
|
+
const result = evaluateMultiRepoEnforcement(rawResult, {
|
|
295
|
+
allowActions,
|
|
296
|
+
exceptions
|
|
297
|
+
});
|
|
298
|
+
printMultiRepoEnforcement(result, targets, fingerprint, runDetails);
|
|
299
|
+
if (!result.compliant && config.enforcement.failOnUnpinned) {
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (requestedMultiRepo) {
|
|
305
|
+
const emptyResult = evaluateEnforcement({
|
|
306
|
+
summary: {
|
|
307
|
+
filesScanned: 0,
|
|
308
|
+
referencesFound: 0,
|
|
309
|
+
unpinnedFound: 0
|
|
310
|
+
},
|
|
311
|
+
references: [],
|
|
312
|
+
unpinned: []
|
|
313
|
+
}, {
|
|
314
|
+
allowActions,
|
|
315
|
+
exceptions
|
|
316
|
+
});
|
|
317
|
+
printMultiRepoEnforcement({
|
|
318
|
+
repositories: [],
|
|
319
|
+
summary: {
|
|
320
|
+
repositoriesScanned: 0,
|
|
321
|
+
repositoriesWithViolations: 0,
|
|
322
|
+
filesScanned: 0,
|
|
323
|
+
referencesFound: 0,
|
|
324
|
+
unpinnedFound: 0,
|
|
325
|
+
allowedCount: 0,
|
|
326
|
+
violationCount: 0,
|
|
327
|
+
invalidExceptionCount: emptyResult.summary.invalidExceptionCount
|
|
328
|
+
},
|
|
329
|
+
invalidExceptions: emptyResult.invalidExceptions,
|
|
330
|
+
compliant: emptyResult.compliant
|
|
331
|
+
}, targets, fingerprint, runDetails);
|
|
332
|
+
if (!emptyResult.compliant && config.enforcement.failOnUnpinned) {
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const result = evaluateEnforcement(await scanWorkflows(include, process.cwd(), {
|
|
338
|
+
excludePatterns: exclude,
|
|
339
|
+
includeActions,
|
|
340
|
+
excludeActions
|
|
341
|
+
}), {
|
|
342
|
+
allowActions,
|
|
343
|
+
exceptions
|
|
344
|
+
});
|
|
345
|
+
printEnforcement(result, fingerprint, runDetails);
|
|
346
|
+
if (!result.compliant && config.enforcement.failOnUnpinned) {
|
|
347
|
+
process.exitCode = 1;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
program
|
|
351
|
+
.command("pr")
|
|
352
|
+
.option("--config <path>", "Path to .action-pinner.json", ".action-pinner.json")
|
|
353
|
+
.option("-p, --path <path...>", "Workflow file, directory, or glob to scan")
|
|
354
|
+
.option("--exclude-path <path...>", "Workflow file, directory, or glob to exclude")
|
|
355
|
+
.option("--include-action <pattern...>", "Only include action refs matching these patterns")
|
|
356
|
+
.option("--exclude-action <pattern...>", "Exclude action refs matching these patterns")
|
|
357
|
+
.option("--repo <repo...>", "Explicit repository targets (owner/repo) for multi-repo scans")
|
|
358
|
+
.option("--github-org <org>", "Organization to enumerate repositories from")
|
|
359
|
+
.option("--include-repo <pattern...>", "Include only repositories matching these patterns")
|
|
360
|
+
.option("--exclude-repo <pattern...>", "Exclude repositories matching these patterns")
|
|
361
|
+
.option("--continue-on-error", "Skip unresolved refs instead of failing", false)
|
|
362
|
+
.option("--fail-on-ambiguous", "Fail on ambiguous refs (security mode)", false)
|
|
363
|
+
.option("--comment-format <template>", "Version comment template; tokens: {ref}, {action}, {sha_short}")
|
|
364
|
+
.option("--token <token>", "GitHub token for API authentication (overrides env vars)")
|
|
365
|
+
.option("--github-api-url <url>", "GitHub API base URL (for GHES deployments)")
|
|
366
|
+
.option("--use-netrc", "Use .netrc for authentication (if --token not provided)", false)
|
|
367
|
+
.action(async (opts) => {
|
|
368
|
+
const config = applyCommentFormatOverride(await loadConfig(opts.config), opts.commentFormat);
|
|
369
|
+
const include = resolveIncludePatterns(opts.path, config.include);
|
|
370
|
+
const exclude = resolveExcludePatterns(opts.excludePath, config.exclude);
|
|
371
|
+
const includeActions = resolveStringList(opts.includeAction, []);
|
|
372
|
+
const excludeActions = resolveStringList(opts.excludeAction, config.excludeActions);
|
|
373
|
+
const result = applyEnforcementExceptions(await scanWorkflows(include, process.cwd(), {
|
|
374
|
+
excludePatterns: exclude,
|
|
375
|
+
includeActions,
|
|
376
|
+
excludeActions
|
|
377
|
+
}), config.enforcement.exceptions);
|
|
378
|
+
const git = simpleGit();
|
|
379
|
+
const token = opts.token || process.env.PIN_ACTIONS_TOKEN || process.env.GITHUB_TOKEN;
|
|
380
|
+
const resolver = new ActionResolver(token, undefined, {
|
|
381
|
+
apiBaseUrl: opts.githubApiUrl || config.githubApiUrl,
|
|
382
|
+
useNetrc: Boolean(opts.useNetrc || config.useNetrc),
|
|
383
|
+
verbose: false
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
const patches = await pinReferences(result.unpinned, resolver, config, false, {
|
|
387
|
+
continueOnError: Boolean(opts.continueOnError),
|
|
388
|
+
failOnAmbiguous: Boolean(opts.failOnAmbiguous)
|
|
389
|
+
});
|
|
390
|
+
const toolVersion = await getToolVersion();
|
|
391
|
+
const fingerprint = buildRunFingerprint(config, toolVersion);
|
|
392
|
+
const runDetails = {
|
|
393
|
+
command: "pr",
|
|
394
|
+
target: "local",
|
|
395
|
+
output: "text",
|
|
396
|
+
continueOnError: Boolean(opts.continueOnError),
|
|
397
|
+
failOnAmbiguous: Boolean(opts.failOnAmbiguous),
|
|
398
|
+
prCreate: config.pr.create
|
|
399
|
+
};
|
|
400
|
+
const branch = await createPullRequestBranch({ config, patches, git });
|
|
401
|
+
if (!config.pr.create) {
|
|
402
|
+
console.log(`Created branch ${branch.branch} from ${branch.baseBranch} with ${patches.length} updated workflow file(s).`);
|
|
403
|
+
console.log("PR creation is disabled by config.pr.create.");
|
|
404
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const pullRequest = await publishPullRequest({
|
|
408
|
+
config,
|
|
409
|
+
patches,
|
|
410
|
+
branch: branch.branch,
|
|
411
|
+
baseBranch: branch.baseBranch,
|
|
412
|
+
commitMessage: branch.commitMessage,
|
|
413
|
+
token: token || process.env.GITHUB_TOKEN,
|
|
414
|
+
git
|
|
415
|
+
});
|
|
416
|
+
if (!pullRequest) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
console.log(`Opened PR #${pullRequest.number}: ${pullRequest.htmlUrl} with ${patches.length} updated workflow file(s).`);
|
|
420
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
if (error instanceof AmbiguousRefError || error instanceof UnresolvedRefError) {
|
|
424
|
+
console.error(safeLog(`Error: ${error.message}`));
|
|
425
|
+
console.error(safeLog(JSON.stringify(error.details, null, 2)));
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
program.command("dependabot-snippet").action(() => {
|
|
434
|
+
console.log(generateDependabotActionsSnippet());
|
|
435
|
+
});
|
|
436
|
+
await program.parseAsync(["node", "action-pinner", ...argv]);
|
|
437
|
+
}
|
|
438
|
+
function resolveIncludePatterns(cliPaths, configInclude) {
|
|
439
|
+
return resolveWorkflowPatterns(cliPaths ?? configInclude);
|
|
440
|
+
}
|
|
441
|
+
function resolveExcludePatterns(cliPaths, configExclude) {
|
|
442
|
+
const values = cliPaths ?? configExclude;
|
|
443
|
+
return values.length === 0 ? [] : resolveWorkflowPatterns(values);
|
|
444
|
+
}
|
|
445
|
+
function resolveStringList(cliValues, configValues) {
|
|
446
|
+
return cliValues ?? configValues;
|
|
447
|
+
}
|
|
448
|
+
async function resolveRepoTargets(opts, config, token) {
|
|
449
|
+
if (opts.githubOrg && opts.githubUser) {
|
|
450
|
+
throw new Error("Specify either --github-org or --github-user, not both.");
|
|
451
|
+
}
|
|
452
|
+
const targetName = opts.githubUser ?? opts.githubOrg ?? config.org.name;
|
|
453
|
+
const targetType = opts.githubUser
|
|
454
|
+
? "user"
|
|
455
|
+
: opts.githubOrg
|
|
456
|
+
? "org"
|
|
457
|
+
: config.org.name
|
|
458
|
+
? config.org.type ?? "org"
|
|
459
|
+
: undefined;
|
|
460
|
+
const explicitRepositories = opts.repo ?? config.repos;
|
|
461
|
+
const includePatterns = opts.includeRepo ?? config.includeRepos;
|
|
462
|
+
const excludePatterns = opts.excludeRepo ?? config.excludeRepos;
|
|
463
|
+
const candidates = explicitRepositories.map((repository) => ({
|
|
464
|
+
fullName: repository,
|
|
465
|
+
defaultBranch: "",
|
|
466
|
+
archived: false
|
|
467
|
+
}));
|
|
468
|
+
if (targetName && targetType) {
|
|
469
|
+
const discoveredRepositories = await listOwnerRepositories({
|
|
470
|
+
target: targetName,
|
|
471
|
+
targetType,
|
|
472
|
+
includePrivate: config.org.includePrivate,
|
|
473
|
+
includeArchived: config.org.includeArchived,
|
|
474
|
+
githubApiUrl: opts.githubApiUrl || config.githubApiUrl
|
|
475
|
+
}, token);
|
|
476
|
+
candidates.push(...discoveredRepositories);
|
|
477
|
+
}
|
|
478
|
+
const repositories = filterRepositoryMetadata(candidates, {
|
|
479
|
+
includePatterns,
|
|
480
|
+
excludePatterns
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
targetName,
|
|
484
|
+
targetType,
|
|
485
|
+
explicitRepositories,
|
|
486
|
+
includePatterns,
|
|
487
|
+
excludePatterns,
|
|
488
|
+
repositories: repositories.map((repository) => repository.fullName),
|
|
489
|
+
repositoryTargets: repositories.map((repository) => ({
|
|
490
|
+
repository: repository.fullName,
|
|
491
|
+
defaultBranch: repository.defaultBranch || undefined
|
|
492
|
+
}))
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function parseExceptionRules(values) {
|
|
496
|
+
if (!values || values.length === 0) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
return values.map((rawRule) => {
|
|
500
|
+
const [actionAndRef, workflow] = rawRule.split("::", 2);
|
|
501
|
+
const [action, ref] = actionAndRef.split("@", 2);
|
|
502
|
+
if (!action) {
|
|
503
|
+
throw new Error(`Invalid enforcement exception rule '${rawRule}'. Expected <action>[@ref][::workflow-glob].`);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
action,
|
|
507
|
+
ref: ref || undefined,
|
|
508
|
+
workflow: workflow || undefined
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function applyEnforcementExceptionsToMultiRepo(result, exceptions) {
|
|
513
|
+
if (exceptions.length === 0) {
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
const repositories = result.repositories.map((entry) => ({
|
|
517
|
+
...entry,
|
|
518
|
+
scan: applyEnforcementExceptions(entry.scan, exceptions)
|
|
519
|
+
}));
|
|
520
|
+
return {
|
|
521
|
+
repositories,
|
|
522
|
+
consolidated: applyEnforcementExceptions(result.consolidated, exceptions),
|
|
523
|
+
summary: {
|
|
524
|
+
repositoriesScanned: repositories.length,
|
|
525
|
+
repositoriesWithUnpinned: repositories.filter((entry) => entry.scan.unpinned.length > 0)
|
|
526
|
+
.length,
|
|
527
|
+
filesScanned: repositories.reduce((sum, entry) => sum + entry.scan.summary.filesScanned, 0),
|
|
528
|
+
referencesFound: repositories.reduce((sum, entry) => sum + entry.scan.summary.referencesFound, 0),
|
|
529
|
+
unpinnedFound: repositories.reduce((sum, entry) => sum + entry.scan.summary.unpinnedFound, 0)
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function printMultiRepoScan(result, targets, fingerprint, runDetails) {
|
|
534
|
+
if (runDetails.output === "json") {
|
|
535
|
+
console.log(JSON.stringify({
|
|
536
|
+
summary: result.summary,
|
|
537
|
+
consolidated: result.consolidated,
|
|
538
|
+
repositories: result.repositories,
|
|
539
|
+
targeting: {
|
|
540
|
+
targetName: targets.targetName,
|
|
541
|
+
targetType: targets.targetType,
|
|
542
|
+
explicitRepositories: targets.explicitRepositories,
|
|
543
|
+
includePatterns: targets.includePatterns,
|
|
544
|
+
excludePatterns: targets.excludePatterns
|
|
545
|
+
},
|
|
546
|
+
run: toRunOutput(fingerprint, runDetails)
|
|
547
|
+
}, null, 2));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
console.log(`Scanned ${result.summary.repositoriesScanned} repositories (${result.summary.filesScanned} workflow file(s), ${result.summary.unpinnedFound} unpinned reference(s)).`);
|
|
551
|
+
if (targets.targetName && targets.targetType) {
|
|
552
|
+
console.log(`Target ${targets.targetType}: ${targets.targetName}`);
|
|
553
|
+
}
|
|
554
|
+
if (targets.explicitRepositories.length > 0) {
|
|
555
|
+
console.log(`Explicit repos: ${targets.explicitRepositories.join(", ")}`);
|
|
556
|
+
}
|
|
557
|
+
if (targets.includePatterns.length > 0) {
|
|
558
|
+
console.log(`Include repo patterns: ${targets.includePatterns.join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
if (targets.excludePatterns.length > 0) {
|
|
561
|
+
console.log(`Exclude repo patterns: ${targets.excludePatterns.join(", ")}`);
|
|
562
|
+
}
|
|
563
|
+
if (result.consolidated.unpinned.length > 0) {
|
|
564
|
+
console.log("Consolidated findings:");
|
|
565
|
+
for (const ref of result.consolidated.unpinned) {
|
|
566
|
+
console.log(`- ${ref.filePath}:${ref.line} -> ${ref.raw}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const entry of result.repositories) {
|
|
570
|
+
console.log(`- ${entry.repository} [${entry.defaultBranch}] -> ${entry.scan.summary.unpinnedFound} unpinned of ${entry.scan.summary.referencesFound} reference(s)`);
|
|
571
|
+
for (const ref of entry.scan.unpinned) {
|
|
572
|
+
console.log(` - ${ref.filePath}:${ref.line} -> ${ref.raw}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
576
|
+
}
|
|
577
|
+
function printMultiRepoEnforcement(result, targets, fingerprint, runDetails) {
|
|
578
|
+
if (runDetails.output === "json") {
|
|
579
|
+
console.log(JSON.stringify({
|
|
580
|
+
compliant: result.compliant,
|
|
581
|
+
summary: result.summary,
|
|
582
|
+
invalidExceptions: result.invalidExceptions,
|
|
583
|
+
repositories: result.repositories,
|
|
584
|
+
targeting: {
|
|
585
|
+
targetName: targets.targetName,
|
|
586
|
+
targetType: targets.targetType,
|
|
587
|
+
explicitRepositories: targets.explicitRepositories,
|
|
588
|
+
includePatterns: targets.includePatterns,
|
|
589
|
+
excludePatterns: targets.excludePatterns
|
|
590
|
+
},
|
|
591
|
+
run: toRunOutput(fingerprint, runDetails)
|
|
592
|
+
}, null, 2));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
console.log(result.compliant
|
|
596
|
+
? `Enforcement passed across ${result.summary.repositoriesScanned} repositories.`
|
|
597
|
+
: `Enforcement failed across ${result.summary.repositoriesScanned} repositories.`);
|
|
598
|
+
console.log(`Allowed ${result.summary.allowedCount} reference(s); found ${result.summary.violationCount} violation(s); ${result.summary.invalidExceptionCount} invalid or expired exception(s).`);
|
|
599
|
+
if (targets.targetName && targets.targetType) {
|
|
600
|
+
console.log(`Target ${targets.targetType}: ${targets.targetName}`);
|
|
601
|
+
}
|
|
602
|
+
if (targets.explicitRepositories.length > 0) {
|
|
603
|
+
console.log(`Explicit repos: ${targets.explicitRepositories.join(", ")}`);
|
|
604
|
+
}
|
|
605
|
+
if (targets.includePatterns.length > 0) {
|
|
606
|
+
console.log(`Include repo patterns: ${targets.includePatterns.join(", ")}`);
|
|
607
|
+
}
|
|
608
|
+
if (targets.excludePatterns.length > 0) {
|
|
609
|
+
console.log(`Exclude repo patterns: ${targets.excludePatterns.join(", ")}`);
|
|
610
|
+
}
|
|
611
|
+
if (result.invalidExceptions.length > 0) {
|
|
612
|
+
console.log("\nInvalid or expired exceptions:");
|
|
613
|
+
for (const issue of result.invalidExceptions) {
|
|
614
|
+
console.log(`- ${issue.message}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const entry of result.repositories) {
|
|
618
|
+
console.log(`\n- ${entry.repository} [${entry.defaultBranch}] -> ${entry.enforcement.summary.allowedCount} allowed, ${entry.enforcement.summary.violationCount} violation(s)`);
|
|
619
|
+
printEnforcementSections(entry.enforcement, " ", { includeInvalidExceptions: false });
|
|
620
|
+
}
|
|
621
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
622
|
+
}
|
|
623
|
+
function printScan(result, config, fingerprint, runDetails) {
|
|
624
|
+
if (runDetails.output === "json") {
|
|
625
|
+
console.log(JSON.stringify(toScanOutput(result, fingerprint, runDetails), null, 2));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (result.unpinned.length === 0) {
|
|
629
|
+
console.log("No unpinned actions found.");
|
|
630
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
console.log(`Found ${result.unpinned.length} unpinned action reference(s):`);
|
|
634
|
+
for (const entry of result.unpinned) {
|
|
635
|
+
console.log(`- ${toDisplayPath(entry.filePath)}:${entry.line} -> ${entry.raw}`);
|
|
636
|
+
}
|
|
637
|
+
if (runDetails.command === "scan") {
|
|
638
|
+
console.log("Run `action-pinner fix` to pin these references.");
|
|
639
|
+
}
|
|
640
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
641
|
+
}
|
|
642
|
+
function printEnforcement(result, fingerprint, runDetails) {
|
|
643
|
+
if (runDetails.output === "json") {
|
|
644
|
+
console.log(JSON.stringify(toEnforcementOutput(result, fingerprint, runDetails), null, 2));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
console.log(result.compliant
|
|
648
|
+
? "Enforcement passed."
|
|
649
|
+
: "Enforcement failed.");
|
|
650
|
+
console.log(`Allowed ${result.summary.allowedCount} reference(s); found ${result.summary.violationCount} violation(s); ${result.summary.invalidExceptionCount} invalid or expired exception(s).`);
|
|
651
|
+
if (result.summary.allowedCount === 0 &&
|
|
652
|
+
result.summary.violationCount === 0 &&
|
|
653
|
+
result.summary.invalidExceptionCount === 0) {
|
|
654
|
+
console.log("No unpinned action references found.");
|
|
655
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
printEnforcementSections(result);
|
|
659
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
660
|
+
}
|
|
661
|
+
function toScanOutput(result, fingerprint, runDetails) {
|
|
662
|
+
return {
|
|
663
|
+
summary: result.summary,
|
|
664
|
+
references: result.references,
|
|
665
|
+
unpinned: result.unpinned,
|
|
666
|
+
run: toRunOutput(fingerprint, runDetails)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function toEnforcementOutput(result, fingerprint, runDetails) {
|
|
670
|
+
return {
|
|
671
|
+
compliant: result.compliant,
|
|
672
|
+
summary: result.summary,
|
|
673
|
+
references: result.references,
|
|
674
|
+
allowed: result.allowed,
|
|
675
|
+
violations: result.violations,
|
|
676
|
+
invalidExceptions: result.invalidExceptions,
|
|
677
|
+
run: toRunOutput(fingerprint, runDetails)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function printEnforcementSections(result, indent = "", options = {}) {
|
|
681
|
+
if (result.allowed.length > 0) {
|
|
682
|
+
console.log(`${indent}Allowed references:`);
|
|
683
|
+
for (const entry of result.allowed) {
|
|
684
|
+
console.log(`${indent}- ${formatEnforcementFinding(entry)}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (options.includeInvalidExceptions !== false && result.invalidExceptions.length > 0) {
|
|
688
|
+
console.log(`${indent}Invalid or expired exceptions:`);
|
|
689
|
+
for (const issue of result.invalidExceptions) {
|
|
690
|
+
console.log(`${indent}- ${issue.message}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (result.violations.length > 0) {
|
|
694
|
+
console.log(`${indent}Violations:`);
|
|
695
|
+
for (const entry of result.violations) {
|
|
696
|
+
console.log(`${indent}- ${formatEnforcementFinding(entry)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function formatEnforcementFinding(entry) {
|
|
701
|
+
return `${toDisplayPath(entry.filePath)}:${entry.line} -> ${entry.raw} (${entry.message})`;
|
|
702
|
+
}
|
|
703
|
+
function countUpdatedReferences(patches) {
|
|
704
|
+
return patches.reduce((count, patch) => count + patch.referencesUpdated.length, 0);
|
|
705
|
+
}
|
|
706
|
+
function applyCommentFormatOverride(config, commentFormat) {
|
|
707
|
+
if (commentFormat === undefined) {
|
|
708
|
+
return config;
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
...config,
|
|
712
|
+
dependabot: {
|
|
713
|
+
...config.dependabot,
|
|
714
|
+
commentFormat
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function printDryRunPreview(patches, fingerprint, runDetails) {
|
|
719
|
+
if (patches.length === 0) {
|
|
720
|
+
console.log("No changes would be made.");
|
|
721
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
for (const patch of patches) {
|
|
725
|
+
const originalLines = patch.originalContent.split(/\r?\n/);
|
|
726
|
+
const updatedLines = patch.updatedContent.split(/\r?\n/);
|
|
727
|
+
console.log(`\n--- ${toDisplayPath(patch.filePath)} ---`);
|
|
728
|
+
const refs = [...patch.referencesUpdated].sort((a, b) => a.line - b.line);
|
|
729
|
+
for (const ref of refs) {
|
|
730
|
+
const lineIndex = ref.line - 1;
|
|
731
|
+
const before = originalLines[lineIndex] ?? "";
|
|
732
|
+
const after = updatedLines[lineIndex] ?? "";
|
|
733
|
+
console.log(`@@ line ${ref.line} @@`);
|
|
734
|
+
console.log(`- ${before}`);
|
|
735
|
+
console.log(`+ ${after}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
printEvidenceReport(patches);
|
|
739
|
+
printRunFingerprint(fingerprint, runDetails);
|
|
740
|
+
}
|
|
741
|
+
function printRunFingerprint(fingerprint, runDetails) {
|
|
742
|
+
const lines = [
|
|
743
|
+
["Tool version", formatToolVersionForDisplay(fingerprint.toolVersion)],
|
|
744
|
+
["Config hash", fingerprint.configHash],
|
|
745
|
+
["Fingerprint", fingerprint.fingerprint],
|
|
746
|
+
["Command", runDetails.command],
|
|
747
|
+
["Target", runDetails.target],
|
|
748
|
+
["Output", runDetails.output]
|
|
749
|
+
];
|
|
750
|
+
if (runDetails.dryRun !== undefined) {
|
|
751
|
+
lines.push(["Dry run", String(runDetails.dryRun)]);
|
|
752
|
+
}
|
|
753
|
+
if (runDetails.continueOnError !== undefined) {
|
|
754
|
+
lines.push(["Continue on error", String(runDetails.continueOnError)]);
|
|
755
|
+
}
|
|
756
|
+
if (runDetails.failOnAmbiguous !== undefined) {
|
|
757
|
+
lines.push(["Fail on ambiguous", String(runDetails.failOnAmbiguous)]);
|
|
758
|
+
}
|
|
759
|
+
if (runDetails.prCreate !== undefined) {
|
|
760
|
+
lines.push(["Create PR", String(runDetails.prCreate)]);
|
|
761
|
+
}
|
|
762
|
+
console.log("\n📋 Run fingerprint:");
|
|
763
|
+
for (const [label, value] of lines) {
|
|
764
|
+
console.log(` ${(label + ":").padEnd(19)} ${value}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function printEvidenceReport(patches) {
|
|
768
|
+
console.log("\nEvidence:");
|
|
769
|
+
console.log(formatEvidence(patches));
|
|
770
|
+
}
|
|
771
|
+
function toRunOutput(fingerprint, runDetails) {
|
|
772
|
+
return {
|
|
773
|
+
...fingerprint,
|
|
774
|
+
execution: runDetails
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function formatToolVersionForDisplay(toolVersion) {
|
|
778
|
+
return toolVersion.startsWith("action-pinner@") ? toolVersion : `action-pinner@${toolVersion}`;
|
|
779
|
+
}
|
|
780
|
+
//# sourceMappingURL=cli.js.map
|