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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +406 -0
  3. package/action.yml +53 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/action-mode.d.ts +1 -0
  8. package/dist/src/action-mode.js +109 -0
  9. package/dist/src/action-mode.js.map +1 -0
  10. package/dist/src/cli.d.ts +2 -0
  11. package/dist/src/cli.js +780 -0
  12. package/dist/src/cli.js.map +1 -0
  13. package/dist/src/config.d.ts +2 -0
  14. package/dist/src/config.js +291 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/dependabot.d.ts +1 -0
  17. package/dist/src/dependabot.js +11 -0
  18. package/dist/src/dependabot.js.map +1 -0
  19. package/dist/src/enforcement.d.ts +12 -0
  20. package/dist/src/enforcement.js +238 -0
  21. package/dist/src/enforcement.js.map +1 -0
  22. package/dist/src/github-app.d.ts +6 -0
  23. package/dist/src/github-app.js +4 -0
  24. package/dist/src/github-app.js.map +1 -0
  25. package/dist/src/index.d.ts +2 -0
  26. package/dist/src/index.js +16 -0
  27. package/dist/src/index.js.map +1 -0
  28. package/dist/src/logging.d.ts +8 -0
  29. package/dist/src/logging.js +38 -0
  30. package/dist/src/logging.js.map +1 -0
  31. package/dist/src/multi-repo-scanner.d.ts +69 -0
  32. package/dist/src/multi-repo-scanner.js +121 -0
  33. package/dist/src/multi-repo-scanner.js.map +1 -0
  34. package/dist/src/netrc-auth.d.ts +13 -0
  35. package/dist/src/netrc-auth.js +123 -0
  36. package/dist/src/netrc-auth.js.map +1 -0
  37. package/dist/src/org.d.ts +49 -0
  38. package/dist/src/org.js +162 -0
  39. package/dist/src/org.js.map +1 -0
  40. package/dist/src/pattern-match.d.ts +5 -0
  41. package/dist/src/pattern-match.js +59 -0
  42. package/dist/src/pattern-match.js.map +1 -0
  43. package/dist/src/pinner.d.ts +6 -0
  44. package/dist/src/pinner.js +148 -0
  45. package/dist/src/pinner.js.map +1 -0
  46. package/dist/src/pr.d.ts +87 -0
  47. package/dist/src/pr.js +165 -0
  48. package/dist/src/pr.js.map +1 -0
  49. package/dist/src/report.d.ts +10 -0
  50. package/dist/src/report.js +54 -0
  51. package/dist/src/report.js.map +1 -0
  52. package/dist/src/resolver.d.ts +44 -0
  53. package/dist/src/resolver.js +227 -0
  54. package/dist/src/resolver.js.map +1 -0
  55. package/dist/src/scanner.d.ts +8 -0
  56. package/dist/src/scanner.js +128 -0
  57. package/dist/src/scanner.js.map +1 -0
  58. package/dist/src/types.d.ts +170 -0
  59. package/dist/src/types.js +41 -0
  60. package/dist/src/types.js.map +1 -0
  61. package/dist/src/version.d.ts +1 -0
  62. package/dist/src/version.js +22 -0
  63. package/dist/src/version.js.map +1 -0
  64. package/dist/src/workflow-paths.d.ts +4 -0
  65. package/dist/src/workflow-paths.js +29 -0
  66. package/dist/src/workflow-paths.js.map +1 -0
  67. package/package.json +62 -0
@@ -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