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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jon Galloway and 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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# action-pinner
|
|
2
|
+
|
|
3
|
+
Pin GitHub Actions refs like `@v4` or `@main` to immutable commit SHAs so your workflows are safer to review, harder to tamper with, and easier to reproduce. `action-pinner` scans workflow files, rewrites refs in place, enforces policy in CI, and can open a pull request with the changes.
|
|
4
|
+
|
|
5
|
+
> **Node:** 20+ **License:** MIT
|
|
6
|
+
|
|
7
|
+
## Why pin actions?
|
|
8
|
+
|
|
9
|
+
- **Tags can move.** `@v4` and `@main` are mutable; a SHA is not.
|
|
10
|
+
- **Supply chain risk goes down.** Pinning limits surprise changes from compromised or retagged releases.
|
|
11
|
+
- **Builds become reproducible.** The same workflow definition resolves to the same code every time.
|
|
12
|
+
- **Audits get easier.** SHA-based refs and exception metadata leave a clearer review trail.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
Install dependencies and build the CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
npm run build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Run directly without installing:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx action-pinner scan
|
|
29
|
+
npx action-pinner fix
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install globally:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g action-pinner
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Examples below use `action-pinner` on your PATH. From a local clone, you can also run `node dist/index.js <command>`.
|
|
39
|
+
|
|
40
|
+
Scan for unpinned actions:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
action-pinner scan
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Rewrite workflow files in place:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
action-pinner fix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fail CI when unpinned refs are found:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
action-pinner enforce
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## CLI Commands
|
|
59
|
+
|
|
60
|
+
### `scan`
|
|
61
|
+
|
|
62
|
+
Find unpinned `uses:` refs without modifying files.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
action-pinner scan
|
|
66
|
+
action-pinner scan --path ".github/workflows"
|
|
67
|
+
action-pinner scan --exclude-path ".github/workflows/legacy/**"
|
|
68
|
+
action-pinner scan --include-action "actions/*" --exclude-action "actions/cache"
|
|
69
|
+
action-pinner scan --github-org octo-org --include-repo "platform-*" --exclude-repo "*-archive"
|
|
70
|
+
action-pinner scan --repo octo-org/service-a octo-org/service-b --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Flags:
|
|
74
|
+
|
|
75
|
+
- `--config <path>`: config file path (default: `.action-pinner.json`)
|
|
76
|
+
- `--path <path...>`: workflow file, directory, or glob to scan
|
|
77
|
+
- `--exclude-path <path...>`: workflow file, directory, or glob to skip
|
|
78
|
+
- `--include-action <pattern...>`: only scan matching actions
|
|
79
|
+
- `--exclude-action <pattern...>`: skip matching actions
|
|
80
|
+
- `--repo <owner/repo...>`: explicit multi-repo targets
|
|
81
|
+
- `--github-org <org>`: enumerate repositories from an organization
|
|
82
|
+
- `--include-repo <pattern...>` / `--exclude-repo <pattern...>`: repo filters for org or explicit targets
|
|
83
|
+
- `--json`: emit machine-readable JSON
|
|
84
|
+
- `--token <token>`: GitHub token override
|
|
85
|
+
- `--github-api-url <url>`: GitHub API base URL, including GHES
|
|
86
|
+
- `--use-netrc`: read credentials from `.netrc` / `_netrc`
|
|
87
|
+
|
|
88
|
+
### `fix`
|
|
89
|
+
|
|
90
|
+
Resolve mutable refs to SHAs and update workflow files in place.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
action-pinner fix
|
|
94
|
+
action-pinner fix --dry-run
|
|
95
|
+
action-pinner fix --path ".github/workflows/release.yml"
|
|
96
|
+
action-pinner fix --continue-on-error --fail-on-ambiguous
|
|
97
|
+
action-pinner fix --comment-format "pin@{ref}"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Flags:
|
|
101
|
+
|
|
102
|
+
- `--dry-run`: preview changes without writing files
|
|
103
|
+
- All `scan` flags except `--json`
|
|
104
|
+
- `--continue-on-error`: skip unresolved refs instead of failing the run
|
|
105
|
+
- `--fail-on-ambiguous`: fail if a ref resolves ambiguously
|
|
106
|
+
- `--comment-format <template>`: customize pinned version comments with `{ref}`, `{action}`, and `{sha_short}` tokens
|
|
107
|
+
|
|
108
|
+
### `enforce`
|
|
109
|
+
|
|
110
|
+
Use policy mode for CI. `enforce` reports allowed refs, violations, invalid exceptions, and exits non-zero when policy fails.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
action-pinner enforce
|
|
114
|
+
action-pinner enforce --allow-action "actions/*"
|
|
115
|
+
action-pinner enforce --exception "actions/upload-artifact@v3::**/legacy.yml"
|
|
116
|
+
action-pinner enforce --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Flags:
|
|
120
|
+
|
|
121
|
+
- All `scan` flags
|
|
122
|
+
- `--allow-action <pattern...>`: allowlist unpinned actions by pattern
|
|
123
|
+
- `--exception <rule...>`: allow a specific exception using `<action>[@ref][::workflow-glob]`
|
|
124
|
+
- `--continue-on-error`: continue when a ref cannot be resolved
|
|
125
|
+
- `--fail-on-ambiguous`: fail if a ref resolves ambiguously
|
|
126
|
+
|
|
127
|
+
### `pr`
|
|
128
|
+
|
|
129
|
+
Pin refs, create a branch, and publish a pull request using the `pr` config block.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
action-pinner pr
|
|
133
|
+
action-pinner pr --path ".github/workflows"
|
|
134
|
+
action-pinner pr --continue-on-error --fail-on-ambiguous
|
|
135
|
+
action-pinner pr --comment-format "{ref}"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Flags:
|
|
139
|
+
|
|
140
|
+
- All `scan` flags except `--json`
|
|
141
|
+
- `--continue-on-error`: skip unresolved refs instead of failing the run
|
|
142
|
+
- `--fail-on-ambiguous`: fail if a ref resolves ambiguously
|
|
143
|
+
- `--comment-format <template>`: override the configured version comment template for this run
|
|
144
|
+
|
|
145
|
+
### `dependabot-snippet`
|
|
146
|
+
|
|
147
|
+
Generate a `github-actions` Dependabot snippet for pinned workflows.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
action-pinner dependabot-snippet
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## GitHub Action Usage
|
|
154
|
+
|
|
155
|
+
Run `action-pinner` as a GitHub Action:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
- uses: jongalloway/action-pinner@v1
|
|
159
|
+
with:
|
|
160
|
+
mode: scan
|
|
161
|
+
config: .action-pinner.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use `enforce` to gate workflow changes in CI:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
name: enforce-pinned-actions
|
|
168
|
+
|
|
169
|
+
on:
|
|
170
|
+
pull_request:
|
|
171
|
+
push:
|
|
172
|
+
branches: [main]
|
|
173
|
+
|
|
174
|
+
jobs:
|
|
175
|
+
action-pinner:
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
steps:
|
|
178
|
+
- uses: actions/checkout@v4
|
|
179
|
+
- uses: jongalloway/action-pinner@v1
|
|
180
|
+
with:
|
|
181
|
+
mode: enforce
|
|
182
|
+
config: .action-pinner.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Action inputs:
|
|
186
|
+
|
|
187
|
+
- `mode`: `scan`, `fix`, `enforce`, or `pr`
|
|
188
|
+
- `config`: config file path
|
|
189
|
+
- `path`, `exclude_path`, `include_action`, `exclude_action`
|
|
190
|
+
- `allow_actions`, `exception_rules`
|
|
191
|
+
- `json`
|
|
192
|
+
|
|
193
|
+
## Pre-commit
|
|
194
|
+
|
|
195
|
+
Use `action-pinner` as a [pre-commit](https://pre-commit.com/) hook to scan workflow changes before they land:
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# .pre-commit-config.yaml
|
|
199
|
+
repos:
|
|
200
|
+
- repo: https://github.com/jongalloway/action-pinner
|
|
201
|
+
rev: v0.1.0
|
|
202
|
+
hooks:
|
|
203
|
+
- id: action-pinner-scan
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Available hooks:
|
|
207
|
+
|
|
208
|
+
- `action-pinner-scan`: runs `action-pinner scan` against `.github/workflows` and fails if unpinned refs are found.
|
|
209
|
+
- `action-pinner-fix`: runs `action-pinner fix` against `.github/workflows` to auto-pin refs before commit.
|
|
210
|
+
|
|
211
|
+
## Configuration
|
|
212
|
+
|
|
213
|
+
Example `.action-pinner.json`:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"$schema": "./schemas/action-pinner.schema.json",
|
|
218
|
+
"mode": "scan",
|
|
219
|
+
"include": [
|
|
220
|
+
".github/workflows/**/*.yml",
|
|
221
|
+
".github/workflows/**/*.yaml"
|
|
222
|
+
],
|
|
223
|
+
"exclude": [
|
|
224
|
+
".github/workflows/legacy/**"
|
|
225
|
+
],
|
|
226
|
+
"repos": [
|
|
227
|
+
"octo-org/service-a",
|
|
228
|
+
"octo-org/service-b"
|
|
229
|
+
],
|
|
230
|
+
"includeRepos": [
|
|
231
|
+
"platform-*"
|
|
232
|
+
],
|
|
233
|
+
"excludeRepos": [
|
|
234
|
+
"*-archive"
|
|
235
|
+
],
|
|
236
|
+
"excludeActions": [
|
|
237
|
+
"actions/cache"
|
|
238
|
+
],
|
|
239
|
+
"org": {
|
|
240
|
+
"name": "octo-org",
|
|
241
|
+
"includePrivate": true,
|
|
242
|
+
"includeArchived": false
|
|
243
|
+
},
|
|
244
|
+
"enforcement": {
|
|
245
|
+
"enabled": true,
|
|
246
|
+
"failOnUnpinned": true,
|
|
247
|
+
"allowActions": [
|
|
248
|
+
"actions/*",
|
|
249
|
+
"github/codeql-action"
|
|
250
|
+
],
|
|
251
|
+
"exceptions": [
|
|
252
|
+
{
|
|
253
|
+
"action": "actions/upload-artifact",
|
|
254
|
+
"ref": "v3",
|
|
255
|
+
"workflow": "**/legacy.yml",
|
|
256
|
+
"reason": "Temporary migration exception",
|
|
257
|
+
"expiresAt": "2026-12-31"
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
"pr": {
|
|
262
|
+
"create": true,
|
|
263
|
+
"branchPrefix": "chore/action-pinner",
|
|
264
|
+
"title": "Pin GitHub Actions to commit SHAs",
|
|
265
|
+
"labels": [
|
|
266
|
+
"security",
|
|
267
|
+
"dependencies"
|
|
268
|
+
],
|
|
269
|
+
"reviewers": [
|
|
270
|
+
"octocat"
|
|
271
|
+
],
|
|
272
|
+
"assignees": [
|
|
273
|
+
"hubot"
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
"dependabot": {
|
|
277
|
+
"addVersionComments": true,
|
|
278
|
+
"commentFormat": "{ref}",
|
|
279
|
+
"generateConfigSnippet": false
|
|
280
|
+
},
|
|
281
|
+
"githubApiUrl": "https://enterprise.example.com/api/v3",
|
|
282
|
+
"useNetrc": false
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Notes:
|
|
287
|
+
|
|
288
|
+
- CLI flags override config values.
|
|
289
|
+
- `reason` and `expiresAt` make exceptions easier to review and clean up.
|
|
290
|
+
- `pr.create: false` creates the branch and commit without publishing a PR.
|
|
291
|
+
- `dependabot.commentFormat` supports `{ref}`, `{action}`, and `{sha_short}` tokens. Use `""` for no version comment, or set `dependabot.addVersionComments: false` to suppress comments entirely.
|
|
292
|
+
|
|
293
|
+
## Authentication
|
|
294
|
+
|
|
295
|
+
Authentication precedence:
|
|
296
|
+
|
|
297
|
+
| Priority | Source |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| 1 | `--token <token>` |
|
|
300
|
+
| 2 | `PIN_ACTIONS_TOKEN` |
|
|
301
|
+
| 3 | `.netrc` / `_netrc` when `--use-netrc` is enabled |
|
|
302
|
+
| 4 | `GITHUB_TOKEN` |
|
|
303
|
+
| 5 | Anonymous GitHub API access |
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
action-pinner scan --token ghp_xxx
|
|
309
|
+
action-pinner scan --use-netrc
|
|
310
|
+
action-pinner scan --github-api-url https://enterprise.example.com/api/v3
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For GitHub Enterprise Server, set `--github-api-url` or `githubApiUrl` in config. See [docs/ENTERPRISE.md](./docs/ENTERPRISE.md).
|
|
314
|
+
|
|
315
|
+
## Multi-Repo and Org Scanning
|
|
316
|
+
|
|
317
|
+
Scan explicit repositories:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
action-pinner scan --repo octo-org/service-a octo-org/service-b
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Discover repositories from an organization, then narrow the set:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
action-pinner scan --github-org octo-org --include-repo "platform-*" --exclude-repo "*-archive"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Target a subset of workflow files across selected repos:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
action-pinner scan --github-org octo-org --path ".github/workflows/**" --exclude-path "**/legacy/**"
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
For user-owned repositories, pass explicit `--repo owner/repo` values.
|
|
336
|
+
|
|
337
|
+
## Enforcement Allowlists and Exceptions
|
|
338
|
+
|
|
339
|
+
Allowlist broad cases:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
action-pinner enforce --allow-action "actions/*"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Add a narrow CLI exception:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
action-pinner enforce --exception "actions/upload-artifact@v3::**/legacy.yml"
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Config-driven exceptions are better for review history:
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"enforcement": {
|
|
356
|
+
"failOnUnpinned": true,
|
|
357
|
+
"allowActions": ["actions/*"],
|
|
358
|
+
"exceptions": [
|
|
359
|
+
{
|
|
360
|
+
"action": "actions/upload-artifact",
|
|
361
|
+
"ref": "v3",
|
|
362
|
+
"workflow": "**/legacy.yml",
|
|
363
|
+
"reason": "Legacy workflow still migrating",
|
|
364
|
+
"expiresAt": "2026-12-31"
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Rules:
|
|
372
|
+
|
|
373
|
+
- `allowActions` is pattern-based and broad.
|
|
374
|
+
- `exceptions` are specific and auditable.
|
|
375
|
+
- Expired or malformed exceptions fail closed.
|
|
376
|
+
|
|
377
|
+
## Security
|
|
378
|
+
|
|
379
|
+
- **Fail closed:** unresolved refs, invalid exceptions, and policy violations fail enforcement by default.
|
|
380
|
+
- **Token safe:** tokens are redacted from logs; use the smallest possible scopes.
|
|
381
|
+
- **Deterministic output:** scans, rewrites, and fingerprints are stable on the same input.
|
|
382
|
+
|
|
383
|
+
See [SECURITY.md](./SECURITY.md) for the security policy and [docs/ENTERPRISE.md](./docs/ENTERPRISE.md) for GHES guidance.
|
|
384
|
+
|
|
385
|
+
## Acknowledgments
|
|
386
|
+
|
|
387
|
+
This project was inspired by [mheap/pin-github-action](https://github.com/mheap/pin-github-action), which pioneered the idea of pinning GitHub Actions to commit SHAs. `action-pinner` is a completely new implementation built from scratch using modern Node.js and the GitHub REST API, designed to address long-standing community requests including:
|
|
388
|
+
|
|
389
|
+
- [Enterprise GitHub support](https://github.com/mheap/pin-github-action/issues/169)
|
|
390
|
+
- [Support netrc auth](https://github.com/mheap/pin-github-action/issues/168)
|
|
391
|
+
- [Published as a GitHub Action](https://github.com/mheap/pin-github-action/issues/141)
|
|
392
|
+
- [Default to `.github/workflows/`](https://github.com/mheap/pin-github-action/issues/201)
|
|
393
|
+
|
|
394
|
+
Thank you to [@mheap](https://github.com/mheap) and the contributors to that project for the inspiration.
|
|
395
|
+
|
|
396
|
+
## Contributing
|
|
397
|
+
|
|
398
|
+
Clone the repo, install dependencies, and run:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
npm test
|
|
402
|
+
npm run lint
|
|
403
|
+
npm run build
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Open an issue or PR at [github.com/jongalloway/action-pinner/issues](https://github.com/jongalloway/action-pinner/issues).
|
package/action.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: "action-pinner"
|
|
2
|
+
description: "Scan and pin unpinned GitHub Actions references to commit SHAs."
|
|
3
|
+
author: "action-pinner contributors"
|
|
4
|
+
inputs:
|
|
5
|
+
mode:
|
|
6
|
+
description: "Mode to run (scan|fix|enforce|pr)"
|
|
7
|
+
required: false
|
|
8
|
+
default: "scan"
|
|
9
|
+
config:
|
|
10
|
+
description: "Path to .action-pinner.json"
|
|
11
|
+
required: false
|
|
12
|
+
default: ".action-pinner.json"
|
|
13
|
+
path:
|
|
14
|
+
description: "Workflow file, directory, or glob to scan"
|
|
15
|
+
required: false
|
|
16
|
+
exclude_path:
|
|
17
|
+
description: "Workflow file, directory, or glob to exclude"
|
|
18
|
+
required: false
|
|
19
|
+
include_action:
|
|
20
|
+
description: "Comma-separated or newline-delimited action patterns to include"
|
|
21
|
+
required: false
|
|
22
|
+
exclude_action:
|
|
23
|
+
description: "Comma-separated or newline-delimited action patterns to exclude"
|
|
24
|
+
required: false
|
|
25
|
+
allow_actions:
|
|
26
|
+
description: "Comma-separated or newline-delimited enforcement allowlist patterns"
|
|
27
|
+
required: false
|
|
28
|
+
exception_rules:
|
|
29
|
+
description: "Comma-separated or newline-delimited enforcement exception rules (<action>[@ref][::workflow-glob])"
|
|
30
|
+
required: false
|
|
31
|
+
json:
|
|
32
|
+
description: "Emit JSON output"
|
|
33
|
+
required: false
|
|
34
|
+
default: "false"
|
|
35
|
+
outputs:
|
|
36
|
+
compliant:
|
|
37
|
+
description: "Whether enforcement completed without violations or invalid exceptions"
|
|
38
|
+
allowed_count:
|
|
39
|
+
description: "Number of unpinned refs allowed by allowlists or exceptions"
|
|
40
|
+
violation_count:
|
|
41
|
+
description: "Number of violating unpinned refs"
|
|
42
|
+
invalid_exception_count:
|
|
43
|
+
description: "Number of malformed or expired exceptions"
|
|
44
|
+
fingerprint:
|
|
45
|
+
description: "Deterministic run fingerprint"
|
|
46
|
+
config_hash:
|
|
47
|
+
description: "Deterministic config hash"
|
|
48
|
+
branding:
|
|
49
|
+
icon: "lock"
|
|
50
|
+
color: "blue"
|
|
51
|
+
runs:
|
|
52
|
+
using: "node20"
|
|
53
|
+
main: "dist/index.js"
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./src/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runActionMode(): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { appendFile } from "node:fs/promises";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { evaluateEnforcement } from "./enforcement.js";
|
|
4
|
+
import { buildRunFingerprint } from "./report.js";
|
|
5
|
+
import { scanWorkflows } from "./scanner.js";
|
|
6
|
+
import { getToolVersion } from "./version.js";
|
|
7
|
+
import { resolveWorkflowPatterns } from "./workflow-paths.js";
|
|
8
|
+
import { runCli } from "./cli.js";
|
|
9
|
+
export async function runActionMode() {
|
|
10
|
+
const mode = process.env.INPUT_MODE ?? "scan";
|
|
11
|
+
const configPath = process.env.INPUT_CONFIG ?? ".action-pinner.json";
|
|
12
|
+
const args = buildCliArgs(mode, configPath);
|
|
13
|
+
await runCli(args);
|
|
14
|
+
if (mode === "enforce") {
|
|
15
|
+
await writeEnforcementOutputs(configPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function buildCliArgs(mode, configPath) {
|
|
19
|
+
const args = [mode, "--config", configPath];
|
|
20
|
+
appendListFlag(args, "--path", parseListInput(process.env.INPUT_PATH));
|
|
21
|
+
appendListFlag(args, "--exclude-path", parseListInput(process.env.INPUT_EXCLUDE_PATH));
|
|
22
|
+
appendListFlag(args, "--include-action", parseListInput(process.env.INPUT_INCLUDE_ACTION));
|
|
23
|
+
appendListFlag(args, "--exclude-action", parseListInput(process.env.INPUT_EXCLUDE_ACTION));
|
|
24
|
+
if (mode === "enforce") {
|
|
25
|
+
appendListFlag(args, "--allow-action", parseListInput(process.env.INPUT_ALLOW_ACTIONS));
|
|
26
|
+
appendListFlag(args, "--exception", parseListInput(process.env.INPUT_EXCEPTION_RULES));
|
|
27
|
+
}
|
|
28
|
+
if (parseBooleanInput(process.env.INPUT_JSON)) {
|
|
29
|
+
args.push("--json");
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
async function writeEnforcementOutputs(configPath) {
|
|
34
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
35
|
+
if (!outputPath) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const config = await loadConfig(configPath);
|
|
39
|
+
const include = resolveWorkflowPatterns(parseListInput(process.env.INPUT_PATH) || config.include);
|
|
40
|
+
const excludeInput = parseListInput(process.env.INPUT_EXCLUDE_PATH) ?? config.exclude;
|
|
41
|
+
const exclude = excludeInput.length > 0 ? resolveWorkflowPatterns(excludeInput) : [];
|
|
42
|
+
const includeActions = parseListInput(process.env.INPUT_INCLUDE_ACTION) ?? [];
|
|
43
|
+
const excludeActions = parseListInput(process.env.INPUT_EXCLUDE_ACTION) ?? config.excludeActions;
|
|
44
|
+
const allowActions = parseListInput(process.env.INPUT_ALLOW_ACTIONS) ?? config.enforcement.allowActions;
|
|
45
|
+
const exceptions = [
|
|
46
|
+
...config.enforcement.exceptions,
|
|
47
|
+
...parseExceptionRules(parseListInput(process.env.INPUT_EXCEPTION_RULES))
|
|
48
|
+
];
|
|
49
|
+
const result = evaluateEnforcement(await scanWorkflows(include, process.cwd(), {
|
|
50
|
+
excludePatterns: exclude,
|
|
51
|
+
includeActions,
|
|
52
|
+
excludeActions
|
|
53
|
+
}), {
|
|
54
|
+
allowActions,
|
|
55
|
+
exceptions
|
|
56
|
+
});
|
|
57
|
+
const fingerprint = buildRunFingerprint(config, await getToolVersion());
|
|
58
|
+
const lines = [
|
|
59
|
+
`compliant=${result.compliant}`,
|
|
60
|
+
`allowed_count=${result.summary.allowedCount}`,
|
|
61
|
+
`violation_count=${result.summary.violationCount}`,
|
|
62
|
+
`invalid_exception_count=${result.summary.invalidExceptionCount}`,
|
|
63
|
+
`fingerprint=${fingerprint.fingerprint}`,
|
|
64
|
+
`config_hash=${fingerprint.configHash}`
|
|
65
|
+
];
|
|
66
|
+
await appendFile(outputPath, `${lines.join("\n")}\n`, "utf8");
|
|
67
|
+
}
|
|
68
|
+
function appendListFlag(args, flag, values) {
|
|
69
|
+
if (!values || values.length === 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
args.push(flag, ...values);
|
|
73
|
+
}
|
|
74
|
+
function parseListInput(value) {
|
|
75
|
+
if (!value) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const normalized = value.replace(/\r/g, "\n").trim();
|
|
79
|
+
if (!normalized) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const separator = normalized.includes("\n") ? /\n+/ : /,/;
|
|
83
|
+
const values = normalized
|
|
84
|
+
.split(separator)
|
|
85
|
+
.map((entry) => entry.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
return values.length > 0 ? values : undefined;
|
|
88
|
+
}
|
|
89
|
+
function parseBooleanInput(value) {
|
|
90
|
+
return value?.trim().toLowerCase() === "true";
|
|
91
|
+
}
|
|
92
|
+
function parseExceptionRules(values) {
|
|
93
|
+
if (!values || values.length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return values.map((rawRule) => {
|
|
97
|
+
const [actionAndRef, workflow] = rawRule.split("::", 2);
|
|
98
|
+
const [action, ref] = actionAndRef.split("@", 2);
|
|
99
|
+
if (!action) {
|
|
100
|
+
throw new Error(`Invalid enforcement exception rule '${rawRule}'. Expected <action>[@ref][::workflow-glob].`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
action,
|
|
104
|
+
ref: ref || undefined,
|
|
105
|
+
workflow: workflow || undefined
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=action-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-mode.js","sourceRoot":"","sources":["../../src/action-mode.ts"],"names":[],"mappings":"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"}
|