actions-warden 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 actions-warden contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # actions-warden
2
+
3
+ Audit, pin, and upgrade GitHub Actions workflows. Designed for safe, hands-off
4
+ invocation by humans **or** LLMs.
5
+
6
+ - **Audit** - scan workflows for supply-chain and injection vulnerabilities
7
+ - **Pin** - rewrite tag refs (`@v3`) to immutable commit SHAs
8
+ - **Upgrade** - bump pinned actions to the newest permitted version
9
+ - **Report** - combined audit + dry-run plan, ideal for LLM context
10
+
11
+ ## Why
12
+
13
+ Tag references in `uses:` are mutable - anyone with write access to the action
14
+ repo (or a stolen maintainer token) can rewrite `v3` to point at malicious code.
15
+ Pinning to a 40-character commit SHA closes that hole. `actions-warden` finds
16
+ the holes, plans the fix, and applies it - but never without your explicit
17
+ say-so. `--dry-run` is the default for every destructive command.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install -g actions-warden
23
+ # or one-shot
24
+ npx actions-warden audit
25
+ ```
26
+
27
+ Requires Node.js 20 or newer.
28
+
29
+ ## Quick start
30
+
31
+ ```sh
32
+ # Audit every workflow under .github/workflows
33
+ actions-warden audit
34
+
35
+ # Audit a specific file with remediation hints
36
+ actions-warden audit -w .github/workflows/release.yml --explain
37
+
38
+ # Plan a SHA-pinning pass (does NOT write)
39
+ actions-warden pin
40
+
41
+ # Actually write the pins
42
+ actions-warden pin --write
43
+
44
+ # Plan upgrades within the same major version
45
+ actions-warden upgrade --mode=minor
46
+
47
+ # Get one combined LLM-friendly report
48
+ actions-warden report --format=toon
49
+ ```
50
+
51
+ ## Output formats
52
+
53
+ ### TOON (default - `--format=toon`)
54
+
55
+ Token-Oriented Object Notation. Each line is a labeled record with `key=value`
56
+ fields. Parses cleanly without a schema and ends with a machine-readable
57
+ `STATUS:` trailer.
58
+
59
+ ```
60
+ SCAN: file=.github/workflows/release.yml
61
+ FINDING: id=18b82e86d7 type=unpinned-action sev=high action=actions/checkout@v3 line=15
62
+ FINDING: id=b50d0d45ab type=secrets-in-env sev=critical key=AWS_SECRET line=4
63
+ SUMMARY: files=1 findings=2 critical=1 high=1 medium=0 low=0
64
+ STATUS: FAIL
65
+ ```
66
+
67
+ ### JSON (`--format=json`)
68
+
69
+ Structured payload for programmatic integrations.
70
+
71
+ ### Text (`--format=text`)
72
+
73
+ Plain human-readable lines - useful when piping through `less`.
74
+
75
+ ## Commands
76
+
77
+ ### `audit`
78
+
79
+ Scan workflows for security findings.
80
+
81
+ | flag | default | description |
82
+ |---|---|---|
83
+ | `-w, --workflow <pattern>` | discover under `.github/workflows/` | repeatable path or glob |
84
+ | `--severity <level>` | `low` (i.e. include all) | minimum severity to report |
85
+ | `--explain` | `false` | include plain-English remediation hint per finding |
86
+ | `--format <fmt>` | `toon` | `toon`, `json`, or `text` |
87
+ | `--output <dest>` | `stdout` | `stdout` or `file` |
88
+ | `--output-path <path>` | - | required when `--output=file` |
89
+ | `--cwd <dir>` | `.` | working directory |
90
+
91
+ Exit codes: `0` if no findings, `1` if any finding reported, `2` on usage error.
92
+
93
+ ### `pin`
94
+
95
+ Resolve every tag/branch ref to a 40-char commit SHA. Preserves the original
96
+ tag as an inline `# v3` comment so `upgrade` can find it later.
97
+
98
+ | flag | default | description |
99
+ |---|---|---|
100
+ | `-w, --workflow <pattern>` | discover | repeatable |
101
+ | `--write` | `false` | apply changes (otherwise dry-run) |
102
+ | `--dry-run <bool>` | `true` | explicit dry-run toggle |
103
+ | `--token <token>` | `$GITHUB_TOKEN` | GitHub API token |
104
+ | `--fix <id>` | - | apply only the change with this id |
105
+ | `--format <fmt>` | `toon` | |
106
+
107
+ ### `upgrade`
108
+
109
+ Bump pinned/tagged actions to the newest version allowed by `--mode`.
110
+
111
+ | flag | default | description |
112
+ |---|---|---|
113
+ | `--mode <m>` | `minor` | `major`, `minor`, or `patch` |
114
+ | `--write` | `false` | apply changes |
115
+ | `--fix <id>` | - | apply only this change id |
116
+ | `--token <token>` | `$GITHUB_TOKEN` | |
117
+
118
+ ### `report`
119
+
120
+ Runs `audit`, plus dry-run `pin` and `upgrade`. Single combined output.
121
+
122
+ | flag | default | description |
123
+ |---|---|---|
124
+ | `--mode <m>` | `minor` | upgrade scope |
125
+ | `--offline` | `false` | skip network-dependent stages |
126
+
127
+ ### `rules`
128
+
129
+ Print the rule catalog.
130
+
131
+ ## Use it as a GitHub Action
132
+
133
+ `actions-warden` ships an `action.yml` at the repo root, so you can drop it
134
+ into any workflow.
135
+
136
+ ```yaml
137
+ permissions:
138
+ contents: read
139
+
140
+ jobs:
141
+ audit:
142
+ runs-on: ubuntu-latest
143
+ steps:
144
+ - uses: actions/checkout@v5
145
+ with:
146
+ persist-credentials: false
147
+ - uses: <owner>/actions-warden@<commit-sha> # pin to a SHA
148
+ with:
149
+ command: audit
150
+ severity: high
151
+ explain: 'true'
152
+ token: ${{ github.token }}
153
+ ```
154
+
155
+ Inputs (all optional unless noted):
156
+
157
+ | input | default | applies to | description |
158
+ |---|---|---|---|
159
+ | `command` | `audit` | all | `audit`, `pin`, `upgrade`, `report`, `rules` |
160
+ | `workflow` | discover | all | space-separated paths or globs |
161
+ | `severity` | - | audit/report | `low` / `medium` / `high` / `critical` |
162
+ | `format` | `toon` | all | `toon` / `json` / `text` |
163
+ | `mode` | `minor` | upgrade/report | `major` / `minor` / `patch` |
164
+ | `min-age` | `7` | upgrade/report | cooldown in days before accepting a new tag |
165
+ | `write` | `false` | pin/upgrade | `true` to apply changes |
166
+ | `explain` | `false` | audit | include remediation hints |
167
+ | `offline` | `false` | report | skip network calls |
168
+ | `output-path` | - | all | also save the report to this file |
169
+ | `token` | - | all | GitHub token (pass `${{ github.token }}`) |
170
+ | `working-directory` | `$GITHUB_WORKSPACE` | all | directory to scan |
171
+ | `node-version` | `20` | - | Node.js version to install |
172
+
173
+ Outputs:
174
+
175
+ | output | description |
176
+ |---|---|
177
+ | `status` | `OK` or `FAIL` (mirrors the CLI's exit signal) |
178
+ | `findings` | number of audit findings (audit/report) |
179
+ | `report-path` | absolute path of the saved report, if `output-path` was set |
180
+
181
+ The action also writes a job summary block with the output of the command,
182
+ making findings visible directly in the GitHub UI.
183
+
184
+ **Important:** the audit command exits non-zero when findings are reported,
185
+ which fails the job by default. To collect findings without failing the build,
186
+ set `continue-on-error: true` on the step.
187
+
188
+ ## Programmatic API
189
+
190
+ Each command is also exported as an async function, so an LLM agent or
191
+ larger Node tool can invoke it without spawning a subprocess.
192
+
193
+ ```js
194
+ import { audit, pin, upgrade, report } from 'actions-warden';
195
+
196
+ const result = await audit({ cwd: '/path/to/repo', explain: true });
197
+ for (const finding of result.findings) {
198
+ // result.findings[i].id is stable; pass it to pin({ fix: id })
199
+ }
200
+ ```
201
+
202
+ Available functions: `audit`, `pin`, `upgrade`, `report`, `listRules`,
203
+ `parseWorkflowFile`, `renderAudit`, `renderPin`, `renderUpgrade`,
204
+ `renderReport`, `format`, `redact`.
205
+
206
+ ## Audit rules
207
+
208
+ | id | severity | catches |
209
+ |---|---|---|
210
+ | `unpinned-action` | high | `uses:` refs that aren't 40-char SHAs |
211
+ | `excessive-permissions` | medium | `write-all` and broad write scopes |
212
+ | `secrets-in-env` | critical | secrets at workflow/job env (leaks to every step) |
213
+ | `script-injection` | critical | `github.event.*` interpolated into `run:` |
214
+ | `pull-request-target-checkout` | critical | "pwn-request" pattern |
215
+
216
+ Run `actions-warden rules` for the live list.
217
+
218
+ ## LLM invocation safety
219
+
220
+ Every command is designed to be safe to invoke from an autonomous agent:
221
+
222
+ - `pin` and `upgrade` default to `--dry-run=true`. You cannot accidentally
223
+ mutate files without explicitly passing `--write`.
224
+ - Output is deterministic and idempotent - re-running on an unchanged repo
225
+ produces identical bytes.
226
+ - Every finding and every planned change carries a stable `id`. To apply a
227
+ single fix without scope creep, pass `--fix=<id>`.
228
+ - The CLI never prompts interactively. All decisions are flag-driven.
229
+ - Secrets that leak into log lines (tokens, AWS keys, PEM blocks) are passed
230
+ through a redactor before output.
231
+
232
+ ## Authentication
233
+
234
+ ```sh
235
+ export GITHUB_TOKEN=$(gh auth token) # or set GH_TOKEN
236
+ actions-warden pin
237
+ ```
238
+
239
+ The `--token` flag takes precedence. Without a token the GitHub API allows 60
240
+ requests/hour, which is enough for small repos but will rate-limit on larger
241
+ audits.
242
+
243
+ ## Caching
244
+
245
+ GitHub API responses are cached in `.actions-warden-cache/` (gitignore it).
246
+ TTL defaults to 1 hour. Delete the directory to force a refresh.
247
+
248
+ ## Exit codes
249
+
250
+ | code | meaning |
251
+ |---|---|
252
+ | `0` | success, no findings |
253
+ | `1` | findings reported, or errors during pin/upgrade |
254
+ | `2` | invalid arguments |
255
+
256
+ ## Releasing
257
+
258
+ To cut a release:
259
+
260
+ 1. Bump `version` in `package.json` (e.g. `0.1.0` → `0.2.0`).
261
+ 2. Commit and push to `main`.
262
+ 3. Create and push the tag:
263
+
264
+ ```sh
265
+ git tag v0.2.0
266
+ git push origin v0.2.0
267
+ ```
268
+
269
+ The `.github/workflows/release.yml` workflow then:
270
+
271
+ - verifies `package.json` version matches the tag and runs the test suite,
272
+ - creates a GitHub Release with auto-generated notes,
273
+ - force-updates the floating major tag (e.g. `v0`) to point at the new commit.
274
+
275
+ Consumers can pin precisely (`@v0.2.0`), float on the major (`@v0`), or
276
+ pin to a commit SHA (recommended - and what `actions-warden pin` will
277
+ produce when run against their workflow).
278
+
279
+ ### Publishing to the GitHub Marketplace
280
+
281
+ The repo's `action.yml` already declares `branding`, so it is Marketplace-eligible.
282
+ After the first release tag is pushed, open the release on github.com and tick
283
+ "Publish this Action to the GitHub Marketplace" to list it. No automation
284
+ required - the release workflow above handles everything except that opt-in.
285
+
286
+ ### Publishing to npm
287
+
288
+ The release workflow includes a `publish-npm` job that publishes to npm with
289
+ provenance via OIDC trusted publishing - no long-lived `NPM_TOKEN` lives in
290
+ GitHub secrets.
291
+
292
+ **npm does not support pre-publish trusted-publisher configuration** — the
293
+ Trusted Publisher panel only appears on packages that already exist on the
294
+ registry. So the very first publish has to be done with a one-time automation
295
+ token; after that, OIDC takes over.
296
+
297
+ #### Step 1 — bootstrap publish (one time, locally)
298
+
299
+ ```sh
300
+ npm login # browser auth
301
+ npm publish --access public # no --provenance on the first publish;
302
+ # local publishes can't sign provenance
303
+ ```
304
+
305
+ #### Step 2 — configure the trusted publisher on npmjs.com
306
+
307
+ Once the package exists, go to:
308
+
309
+ **npmjs.com → Packages → actions-warden → Settings → Trusted publishing**
310
+
311
+ Add a new GitHub Actions publisher with:
312
+
313
+ - Organization or user: `chiz0me`
314
+ - Repository: `actions-warden`
315
+ - Workflow filename: `release.yml`
316
+ - Environment: *(leave blank)*
317
+
318
+ Save. From this point on, no token is needed.
319
+
320
+ #### Step 3 — release future versions
321
+
322
+ Bump `version` in `package.json`, push, then:
323
+
324
+ ```sh
325
+ git tag v0.2.0
326
+ git push origin v0.2.0
327
+ ```
328
+
329
+ The release workflow then:
330
+
331
+ - runs the full test suite and dependency-pin verification,
332
+ - publishes to npm with `--provenance --access public` (the published package
333
+ carries a verifiable link back to this exact commit and workflow run),
334
+ - creates the GitHub Release with auto-generated notes,
335
+ - force-moves the floating major tag (`v0`).
336
+
337
+ The package is published as **`actions-warden`** (unscoped, public). Consumers
338
+ install it with:
339
+
340
+ ```sh
341
+ npm install -g actions-warden
342
+ # or
343
+ npx actions-warden audit
344
+ ```
345
+
346
+ > **Requirements:** trusted publishing needs npm ≥ 11.5.1 and Node ≥ 24, so the
347
+ > `publish-npm` job uses Node 24 and upgrades npm to latest before publishing.
348
+
349
+ ## Security
350
+
351
+ See [SECURITY.md](./SECURITY.md) for the disclosure policy.
352
+
353
+ ## License
354
+
355
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,52 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please **do not** open public issues for security-sensitive reports. Send the
6
+ details to the maintainers via one of:
7
+
8
+ - GitHub Security Advisory: open a private advisory on this repository.
9
+ - Email: see the maintainer profile linked from `package.json`.
10
+
11
+ Please include:
12
+
13
+ - A clear description of the issue.
14
+ - A minimal reproduction (workflow YAML, command line, expected vs. actual).
15
+ - The version of `actions-warden` (`npx actions-warden --version`).
16
+ - Any disclosure constraints on your side.
17
+
18
+ We aim to acknowledge within **3 business days** and to ship a fix or a
19
+ documented mitigation within **30 days** of triage for high-severity issues.
20
+
21
+ ## Scope
22
+
23
+ In scope:
24
+
25
+ - Path traversal or arbitrary file write in `pin`, `upgrade`, or `report`.
26
+ - Bypasses of the `--dry-run` guard.
27
+ - Logging or persisting credentials anywhere (cache, output, stderr).
28
+ - Workflow parser crashes on attacker-controlled YAML.
29
+ - Network requests sent to hosts other than `api.github.com`.
30
+
31
+ Out of scope:
32
+
33
+ - Findings produced by audit rules themselves (those describe vulnerabilities
34
+ in users' workflows, not in `actions-warden`).
35
+ - Rate-limit responses from GitHub API.
36
+ - Outdated transitive dependencies that do not reach our code paths.
37
+
38
+ ## Supply chain
39
+
40
+ - All runtime dependencies are pinned to exact versions (`save-exact=true`).
41
+ - `ignore-scripts=true` in `.npmrc` to disable arbitrary postinstall scripts.
42
+ - CI runs `npm audit --audit-level=high` on every PR.
43
+ - Releases are published with `npm publish --provenance`.
44
+
45
+ ## Responsible-disclosure timeline
46
+
47
+ | event | target |
48
+ |---|---|
49
+ | Acknowledgement | 3 business days |
50
+ | Triage + severity rating | 7 days |
51
+ | Fix or mitigation for critical/high | 30 days |
52
+ | Public disclosure | by mutual agreement, default 90 days |
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "actions-warden",
3
+ "version": "0.1.0",
4
+ "description": "Audit, pin, and upgrade GitHub Actions workflows. LLM-friendly TOON output, safe-by-default.",
5
+ "author": "Naveen Yagati",
6
+ "homepage": "https://github.com/chiz0me/actions-warden#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/chiz0me/actions-warden.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/chiz0me/actions-warden/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./src/index.js",
16
+ "exports": {
17
+ ".": "./src/index.js",
18
+ "./commands/audit": "./src/commands/audit.js",
19
+ "./commands/pin": "./src/commands/pin.js",
20
+ "./commands/upgrade": "./src/commands/upgrade.js",
21
+ "./commands/report": "./src/commands/report.js"
22
+ },
23
+ "bin": {
24
+ "actions-warden": "./src/cli.js"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "README.md",
29
+ "SECURITY.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "scripts": {
40
+ "test": "vitest --run",
41
+ "test:watch": "vitest",
42
+ "lint": "eslint src test",
43
+ "verify-deps": "node scripts/verify-deps.js",
44
+ "audit": "npm audit --audit-level=high",
45
+ "prepublishOnly": "node scripts/verify-deps.js && npm test"
46
+ },
47
+ "dependencies": {
48
+ "commander": "14.0.3",
49
+ "picomatch": "4.0.4",
50
+ "semver": "7.8.1",
51
+ "yaml": "2.9.0"
52
+ },
53
+ "devDependencies": {
54
+ "vitest": "4.1.6"
55
+ },
56
+ "keywords": [
57
+ "github-actions",
58
+ "security",
59
+ "audit",
60
+ "pin",
61
+ "sha-pin",
62
+ "workflow",
63
+ "supply-chain",
64
+ "toon",
65
+ "llm"
66
+ ],
67
+ "license": "MIT"
68
+ }
package/src/cli.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * actions-warden CLI entry point.
4
+ *
5
+ * Commands: audit, pin, upgrade, report, rules
6
+ * Global flags: --format, --output, --output-path, --workflow, --token
7
+ *
8
+ * Destructive operations (pin, upgrade) default to --dry-run=true. Pass
9
+ * --write to mutate workflow files.
10
+ *
11
+ * Exit codes:
12
+ * 0 no findings / no errors
13
+ * 1 findings reported (audit FAIL) or errors during pin/upgrade
14
+ * 2 invalid arguments
15
+ */
16
+
17
+ import { writeFile } from 'node:fs/promises';
18
+ import { resolve } from 'node:path';
19
+ import { Command, Option } from 'commander';
20
+
21
+ import { audit, renderAudit } from './commands/audit.js';
22
+ import { pin, renderPin } from './commands/pin.js';
23
+ import { upgrade, renderUpgrade } from './commands/upgrade.js';
24
+ import { report, renderReport } from './commands/report.js';
25
+ import { listRules } from './rules/index.js';
26
+ import { format as fmt } from './lib/formatter.js';
27
+
28
+ const program = new Command();
29
+ program
30
+ .name('actions-warden')
31
+ .description('Audit, pin, and upgrade GitHub Actions workflows.')
32
+ .version('0.1.0');
33
+
34
+ const formatOption = new Option('--format <fmt>', 'output format').choices(['toon', 'json', 'text']).default('toon');
35
+ const outputOption = new Option('--output <dest>', 'output destination').choices(['stdout', 'file']).default('stdout');
36
+
37
+ function addCommonOptions(cmd) {
38
+ return cmd
39
+ .option('-w, --workflow <pattern...>', 'workflow path or glob (repeatable)')
40
+ .option('--cwd <dir>', 'working directory', process.cwd())
41
+ .option('--token <token>', 'GitHub token (overrides GITHUB_TOKEN / GH_TOKEN)')
42
+ .addOption(formatOption)
43
+ .addOption(outputOption)
44
+ .option('--output-path <path>', 'file path when --output=file');
45
+ }
46
+
47
+ async function emit(payload, opts) {
48
+ if (opts.output === 'file') {
49
+ const path = opts.outputPath ?? 'actions-warden-output.txt';
50
+ await writeFile(resolve(opts.cwd ?? process.cwd(), path), payload, 'utf8');
51
+ return;
52
+ }
53
+ process.stdout.write(payload);
54
+ }
55
+
56
+ addCommonOptions(program.command('audit'))
57
+ .description('Scan workflows for security findings')
58
+ .addOption(new Option('--severity <level>', 'minimum severity').choices(['low', 'medium', 'high', 'critical']))
59
+ .option('--explain', 'include plain-English remediation hint for each finding', false)
60
+ .action(async (opts) => {
61
+ const result = await audit({
62
+ cwd: opts.cwd,
63
+ workflows: opts.workflow,
64
+ severity: opts.severity,
65
+ explain: Boolean(opts.explain),
66
+ });
67
+ const payload = renderAudit(result, { format: opts.format, explain: Boolean(opts.explain), cwd: opts.cwd });
68
+ await emit(payload, opts);
69
+ process.exit(result.status === 'OK' ? 0 : 1);
70
+ });
71
+
72
+ addCommonOptions(program.command('pin'))
73
+ .description('Pin tag/branch refs to immutable commit SHAs')
74
+ .option('--write', 'apply changes (disables dry-run)', false)
75
+ .option('--dry-run <bool>', 'dry-run mode', 'true')
76
+ .option('--fix <id>', 'apply only the change with this id')
77
+ .action(async (opts) => {
78
+ const dryRun = !opts.write && opts.dryRun !== 'false';
79
+ const result = await pin({
80
+ cwd: opts.cwd,
81
+ workflows: opts.workflow,
82
+ dryRun,
83
+ token: opts.token,
84
+ fix: opts.fix,
85
+ });
86
+ const payload = renderPin(result, { format: opts.format, dryRun, cwd: opts.cwd });
87
+ await emit(payload, opts);
88
+ process.exit(result.status === 'OK' ? 0 : 1);
89
+ });
90
+
91
+ addCommonOptions(program.command('upgrade'))
92
+ .description('Upgrade pinned/tagged actions to a newer version')
93
+ .option('--write', 'apply changes (disables dry-run)', false)
94
+ .option('--dry-run <bool>', 'dry-run mode', 'true')
95
+ .addOption(new Option('--mode <m>', 'upgrade scope').choices(['major', 'minor', 'patch']).default('minor'))
96
+ .option('--min-age <days>', 'cooldown: only accept tags older than this many days', '7')
97
+ .option('--fix <id>', 'apply only the change with this id')
98
+ .action(async (opts) => {
99
+ const dryRun = !opts.write && opts.dryRun !== 'false';
100
+ const minAgeDays = Number.parseInt(opts.minAge, 10);
101
+ if (Number.isNaN(minAgeDays) || minAgeDays < 0) {
102
+ process.stderr.write('error: --min-age must be a non-negative integer\n');
103
+ process.exit(2);
104
+ }
105
+ const result = await upgrade({
106
+ cwd: opts.cwd,
107
+ workflows: opts.workflow,
108
+ dryRun,
109
+ token: opts.token,
110
+ mode: opts.mode,
111
+ fix: opts.fix,
112
+ minAgeDays,
113
+ });
114
+ const payload = renderUpgrade(result, { format: opts.format, dryRun, mode: opts.mode, cwd: opts.cwd });
115
+ await emit(payload, opts);
116
+ process.exit(result.status === 'OK' ? 0 : 1);
117
+ });
118
+
119
+ addCommonOptions(program.command('report'))
120
+ .description('Combined audit + pin (dry) + upgrade (dry) report')
121
+ .addOption(new Option('--mode <m>', 'upgrade scope').choices(['major', 'minor', 'patch']).default('minor'))
122
+ .option('--min-age <days>', 'cooldown for upgrades (days)', '7')
123
+ .option('--offline', 'skip network calls (audit only)', false)
124
+ .action(async (opts) => {
125
+ const minAgeDays = Number.parseInt(opts.minAge, 10);
126
+ if (Number.isNaN(minAgeDays) || minAgeDays < 0) {
127
+ process.stderr.write('error: --min-age must be a non-negative integer\n');
128
+ process.exit(2);
129
+ }
130
+ const result = await report({
131
+ cwd: opts.cwd,
132
+ workflows: opts.workflow,
133
+ token: opts.token,
134
+ mode: opts.mode,
135
+ skipResolve: Boolean(opts.offline),
136
+ minAgeDays,
137
+ });
138
+ const payload = renderReport(result, { format: opts.format, mode: opts.mode, cwd: opts.cwd });
139
+ await emit(payload, opts);
140
+ process.exit(result.status === 'OK' ? 0 : 1);
141
+ });
142
+
143
+ program.command('rules')
144
+ .description('List available audit rules')
145
+ .addOption(formatOption)
146
+ .action((opts) => {
147
+ const rules = listRules();
148
+ const payload = fmt(opts.format, rules.map(r => ({ label: 'RULE', fields: r })), { status: 'OK', json: { rules, status: 'OK' } });
149
+ process.stdout.write(payload);
150
+ });
151
+
152
+ program.parseAsync(process.argv).catch((err) => {
153
+ process.stderr.write(`error: ${err.message}\n`);
154
+ process.exit(2);
155
+ });