Repository navigation
222 lines (210 loc) · 10 KB
/
Copy pathtest-workflow-pins.yml
File metadata and controls
222 lines (210 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
name: Test workflow pins
# Regression lint (BE-5546): no reusable workflow here may give its
# `workflows_ref` input a `default:`. A default lets a caller SHA-pin `uses:`
# and still load this repo's scripts from a MUTABLE branch, into jobs holding
# write permissions — the pin then proves nothing. Removing that default is a
# one-line edit to undo, so it is guarded by CI rather than by convention.
#
# It also lints the OTHER pin this repo depends on (BE-15255): every `uses:`
# under `.github/workflows/` must name a full 40-hex commit SHA, not a tag.
# That half covers every workflow file, `ci-*` and `test-*` callers included —
# they declare no `workflows_ref` and so are skipped by the checks above, which
# is precisely where two `actions/*` refs sat floating on major tags for months.
#
# Runs on every workflow-file change (not just the three fixed ones) so a
# workflow added later is covered the day it lands.
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/workflow-pins/**'
# The lint also cross-checks each reusable's caller guide (BE-6508), so a
# docs-only edit that reintroduces `| workflows_ref | main | … |` must
# trigger it too — otherwise the guard never runs on the drift it exists
# to catch.
- 'docs/callers/**'
# The two directory READMEs with knob tables, which sit outside
# docs/callers/**: a README-only edit that deletes a knob row must run the
# drift harness.
- '.github/cursor-review/README.md'
- '.github/refresh-reviewers/README.md'
# The public catalog, scanned by SharedDocExampleCallersTest for
# `uses:`-anchored `with:` fences. It matches none of the globs above, so
# without this entry a catalog-only edit that adds a phantom `with:` key
# to the most-copied caller example in the repo runs no job and lands
# green — the #31 failure. (`docs/callers/README.md`, the other doc that
# test scans, is already covered by `docs/callers/**`.)
- 'README.md'
push:
branches: [main]
paths:
- '.github/workflows/**'
- '.github/workflow-pins/**'
- 'docs/callers/**'
# Same two directory READMEs as above: a README-only edit that deletes a
# knob row must run the drift harness.
- '.github/cursor-review/README.md'
- '.github/refresh-reviewers/README.md'
# Same public catalog as above.
- 'README.md'
permissions:
contents: read
# One lint run per ref: a push that supersedes an in-flight run should cancel
# it, not race it for a runner.
concurrency:
group: test-workflow-pins-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: unittest + lint this repo
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Run workflow-pins unit tests
run: python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py' -v
- name: Lint this repo's workflows
run: python3 .github/workflow-pins/check_workflow_pins.py
- name: Smoke test — a reintroduced default fails the lint
run: |
# Proves the lint is wired to FAIL, not just to run: the unit tests
# exercise the parser, this exercises the CLI's exit status.
dir="$(mktemp -d)"
cat > "$dir/regressed.yml" <<'YAML'
name: Regressed
on:
workflow_call:
inputs:
workflows_ref:
type: string
required: false
default: main
YAML
if python3 .github/workflow-pins/check_workflow_pins.py --workflows-dir "$dir"; then
echo "::error::lint passed a workflow with a workflows_ref default — expected a non-zero exit"
exit 1
fi
echo "lint correctly rejected the reintroduced default"
- name: Smoke test — an unguarded ref checkout fails the lint
run: |
# The other half of the hole: no default to find, but a job that
# checks out at the ref without the fail-fast guard still lets an
# omitted input reach `ref: ''` and take the default branch.
dir="$(mktemp -d)"
# The ref expression goes in via a placeholder, for two reasons: a
# literal Actions expression written here would be evaluated when this
# run block is rendered (and `inputs` is empty in this workflow), so
# the fixture would silently lose the very line under test; and the
# lint's own "this file uses the input, so it must declare it" check
# would then read this workflow as a consumer it failed to parse.
#
# The fixture's `uses:` is SHA-pinned for a THIRD reason (BE-15255):
# the lint now also rejects a tag-pinned `uses:`, so a fixture left on
# `@abc` would fail this step for a reason it does not test — and the
# step, which only asserts a non-zero exit, would then pass even if the
# unguarded-checkout check it exists for stopped firing entirely.
D='$'
sed "s|__REF__|${D}{{ inputs.workflows_ref }}|" > "$dir/unguarded.yml" <<'YAML'
name: Unguarded
on:
workflow_call:
inputs:
workflows_ref:
type: string
required: true
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: __REF__
YAML
grep -q 'inputs.workflows_ref' "$dir/unguarded.yml" # fixture intact
if python3 .github/workflow-pins/check_workflow_pins.py --workflows-dir "$dir"; then
echo "::error::lint passed an unguarded workflows_ref checkout — expected a non-zero exit"
exit 1
fi
echo "lint correctly rejected the unguarded checkout"
- name: Smoke test — an unreadable `steps:` warns but does not fail
run: |
# The third state (BE-9045): a job whose `steps:` the pre-scan cannot
# read keeps its fail-open drop — a fail-CLOSED escape would
# manufacture false CI failures — but it must SAY so. Both halves are
# pinned here because either one alone is a silent regression: a
# warning that started failing the run, or a drop that went quiet
# again.
dir="$(mktemp -d)"
# Same placeholder trick as the step above, for the same two reasons,
# and the fixture's `uses:` is SHA-pinned for the same third one —
# sharper here, because this step asserts exit 0. A tag-pinned fixture
# ref would fail the pin check (BE-15255), and this step would then go
# red over a ref that is not what it is testing. The flow-mapping
# spelling is deliberate and the pin check reads it too.
D='$'
sed "s|__REF__|${D}{{ steps.typo.outputs.ref }}|" > "$dir/escaped.yml" <<'YAML'
name: Escaped
on:
workflow_call:
inputs:
workflows_ref:
type: string
required: true
jobs:
run:
runs-on: ubuntu-latest
steps: [{id: resolve, run: echo hi}, {uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: {ref: "__REF__"}}]
YAML
grep -q 'steps.typo.outputs.ref' "$dir/escaped.yml" # fixture intact
status=0
python3 .github/workflow-pins/check_workflow_pins.py --workflows-dir "$dir" > "$dir/out.txt" 2>&1 || status=$?
cat "$dir/out.txt"
if [ "$status" -ne 0 ]; then
echo "::error::lint FAILED on a job whose steps: it could not read — the escape must stay fail-open (exit $status)"
exit 1
fi
if ! grep -q '::warning' "$dir/out.txt"; then
echo "::error::lint dropped a checkout in an unreadable steps: job and said nothing — expected a ::warning annotation"
exit 1
fi
echo "lint warned about the unjudged job without failing the run"
- name: Smoke test — a tag-pinned `uses:` fails the lint
run: |
# BE-15255: the pin half, wired to FAIL at the CLI like the two above.
# The fixture declares no `workflows_ref` at all, so nothing but the
# pin check can account for a non-zero exit here — a tag-pinned ref
# must fail even in a file the other two checks skip entirely, which
# is the case that let two `actions/*` refs float for months.
dir="$(mktemp -d)"
cat > "$dir/floating.yml" <<'YAML'
name: Floating
on: [push]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
YAML
grep -q 'actions/checkout@v7' "$dir/floating.yml" # fixture intact
status=0
python3 .github/workflow-pins/check_workflow_pins.py --workflows-dir "$dir" > "$dir/out.txt" 2>&1 || status=$?
cat "$dir/out.txt"
if [ "$status" -eq 0 ]; then
echo "::error::lint passed a uses: ref pinned to a tag — expected a non-zero exit"
exit 1
fi
# A non-zero exit alone is not the assertion: a traceback, or any
# unrelated future failure, satisfies it just as well, so the step
# would stay green with the pin check no longer firing at all. Tie
# it to the error this test exists for.
if ! grep -q 'by tag, not by commit SHA' "$dir/out.txt"; then
echo "::error::lint exited non-zero but never reported the tag-pinned uses: — see the output above"
exit 1
fi
echo "lint correctly rejected the tag-pinned uses:"