Skip to content

Commit 8cd76c2

Browse files
build(deps): bump and pin multiple github actions (#221)
1 parent 64c9bf8 commit 8cd76c2

4 files changed

Lines changed: 186 additions & 11 deletions

File tree

.github/workflows/ci-build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ jobs:
2020

2121
steps:
2222
- name: Checkout
23-
uses: actions/checkout@v6
23+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2424

25-
- uses: pnpm/action-setup@v6
25+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
2626
name: Install pnpm
2727
with:
2828
run_install: false
2929

3030
- name: Install Node.js
31-
uses: actions/setup-node@v6
31+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3232
with:
3333
node-version-file: .nvmrc
3434
cache: 'pnpm'
@@ -62,7 +62,7 @@ jobs:
6262

6363
steps:
6464
- name: Checkout
65-
uses: actions/checkout@v6
65+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
6666

6767
- uses: ./.github/actions/containerize
6868
with:

.github/workflows/ci-lint.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ jobs:
1919

2020
steps:
2121
- name: Checkout
22-
uses: actions/checkout@v6
22+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2323

24-
- uses: pnpm/action-setup@v6
24+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
2525
name: Install pnpm
2626
with:
2727
run_install: false
2828

2929
- name: Install Node.js
30-
uses: actions/setup-node@v6
30+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3131
with:
3232
node-version-file: .nvmrc
3333
cache: 'pnpm'

.github/workflows/codeql.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ jobs:
2626

2727
steps:
2828
- name: Checkout repository
29-
uses: actions/checkout@v6
29+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
3030

3131
# Initializes the CodeQL tools for scanning.
3232
- name: Initialize CodeQL
33-
uses: github/codeql-action/init@v4
33+
uses: github/codeql-action/init@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6
3434
with:
3535
languages: ${{ matrix.language }}
3636

3737
- name: Autobuild
38-
uses: github/codeql-action/autobuild@v4
38+
uses: github/codeql-action/autobuild@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6
3939

4040
- name: Perform CodeQL Analysis
41-
uses: github/codeql-action/analyze@v4
41+
uses: github/codeql-action/analyze@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6
4242
with:
4343
category: '/language:${{matrix.language}}'

test.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Pin every GitHub Action referenced in .github/workflows/*.yml to the full commit
4+
SHA that its version tag currently resolves to, keeping the tag as a trailing
5+
comment (e.g. `uses: actions/checkout@<sha> # v6`).
6+
7+
Pinning to an immutable SHA stops a compromised or retagged action from silently
8+
running new code in our pipelines, while the trailing comment keeps the intended
9+
version readable and lets Dependabot keep bumping it.
10+
11+
The script is re-runnable: for entries that are already pinned it re-resolves the
12+
SHA from the trailing-comment tag, so running it again simply refreshes the SHAs.
13+
14+
With --latest, each action is first upgraded to its newest published release tag
15+
(falling back to the tag already in the file if the action has no releases), so
16+
the script doubles as an updater.
17+
18+
Requires the `gh` CLI (authenticated) and `git`.
19+
20+
Usage:
21+
python .github/pin_actions.py # re-pin to the current tags
22+
python .github/pin_actions.py --latest # upgrade to latest releases, then pin
23+
"""
24+
25+
import argparse
26+
import re
27+
import subprocess
28+
import sys
29+
import pathlib
30+
31+
# matches `[- ]uses: owner/repo@ref` with an optional trailing `# tag` comment
32+
USES_RE = re.compile(
33+
r'^(?P<pre>\s*-?\s*uses:\s*)(?P<action>[^@\s]+)@(?P<ref>\S+)(?P<rest>.*)$'
34+
)
35+
COMMENT_TAG_RE = re.compile(r'#\s*(?P<tag>\S+)')
36+
SHA_RE = re.compile(r'^[0-9a-f]{40}$')
37+
38+
_sha_cache: dict[tuple[str, str], str] = {}
39+
_latest_cache: dict[str, str | None] = {}
40+
41+
42+
def resolve_sha( action: str, tag: str ) -> str:
43+
44+
key = ( action, tag )
45+
46+
if key not in _sha_cache:
47+
48+
# actions may reference a subdirectory (owner/repo/path); the API wants owner/repo
49+
repo = '/'.join( action.split( '/' )[ :2 ] )
50+
51+
_sha_cache[ key ] = subprocess.check_output(
52+
[ 'gh', 'api', f'repos/{repo}/commits/{tag}', '--jq', '.sha' ],
53+
text = True
54+
).strip()
55+
56+
57+
return _sha_cache[ key ]
58+
59+
60+
def latest_tag( action: str ) -> str | None:
61+
62+
if action not in _latest_cache:
63+
64+
try:
65+
66+
repo = '/'.join( action.split( '/' )[ :2 ] )
67+
68+
tag = subprocess.run(
69+
[ 'gh', 'api', f'repos/{repo}/releases/latest', '--jq', '.tag_name' ],
70+
capture_output = True, text = True, check = True
71+
).stdout.strip()
72+
73+
_latest_cache[ action ] = tag or None
74+
75+
except subprocess.CalledProcessError:
76+
77+
# no published releases (or no access); caller falls back to the existing tag
78+
_latest_cache[ action ] = None
79+
80+
81+
82+
return _latest_cache[ action ]
83+
84+
85+
def pin_line( line: str, use_latest: bool ) -> str:
86+
87+
m = USES_RE.match( line )
88+
89+
if m is None:
90+
91+
return line
92+
93+
94+
action = m[ 'action' ]
95+
ref = m[ 'ref' ]
96+
rest = m[ 'rest' ]
97+
98+
comment = COMMENT_TAG_RE.search( rest )
99+
100+
if SHA_RE.match( ref ):
101+
102+
# already pinned; the intended version lives in the `# tag` comment
103+
current_tag = comment[ 'tag' ] if comment is not None else None
104+
105+
else:
106+
107+
current_tag = ref
108+
109+
110+
if use_latest:
111+
112+
tag = latest_tag( action ) or current_tag
113+
114+
else:
115+
116+
tag = current_tag
117+
118+
119+
if tag is None:
120+
121+
# pinned to a bare SHA with no comment and nothing to upgrade to; leave it be
122+
return line
123+
124+
125+
sha = resolve_sha( action, tag )
126+
127+
print( f'{action}@{tag} -> {sha}' )
128+
129+
return f'{m[ "pre" ]}{action}@{sha} # {tag}'
130+
131+
132+
def main() -> int:
133+
134+
parser = argparse.ArgumentParser( description = 'Pin GitHub Actions in our workflows to commit SHAs.' )
135+
parser.add_argument(
136+
'--latest', action = 'store_true',
137+
help = 'upgrade each action to its newest published release tag before pinning'
138+
)
139+
args = parser.parse_args()
140+
141+
root = pathlib.Path(
142+
subprocess.check_output( [ 'git', 'rev-parse', '--show-toplevel' ], text = True ).strip()
143+
)
144+
145+
workflow_dir = root / '.github' / 'workflows'
146+
147+
for path in sorted( workflow_dir.glob( '*.yml' ) ):
148+
149+
original = path.read_text( encoding = 'utf-8' )
150+
151+
newline = '\r\n' if '\r\n' in original else '\n'
152+
153+
lines = original.splitlines()
154+
155+
pinned = [ pin_line( line, args.latest ) for line in lines ]
156+
157+
updated = newline.join( pinned )
158+
159+
if original.endswith( ( '\n', '\r' ) ):
160+
161+
updated += newline
162+
163+
164+
if updated != original:
165+
166+
path.write_text( updated, encoding = 'utf-8', newline = '' )
167+
168+
169+
170+
return 0
171+
172+
173+
if __name__ == '__main__':
174+
175+
sys.exit( main() )

0 commit comments

Comments
 (0)