-
Notifications
You must be signed in to change notification settings - Fork 4
280 lines (245 loc) · 10.4 KB
/
Copy pathci.yml
File metadata and controls
280 lines (245 loc) · 10.4 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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
name: CI check
run-name: >-
${{ github.event_name == 'pull_request' && format('PR-CI #{0}', github.event.pull_request.number)
|| github.event_name == 'merge_group' && 'MergeQueue-CI'
|| github.event_name == 'push' && 'Main-CI'
|| 'CI' }}
on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
merge_group:
# Least privilege: jobs only read the repo. Any job needing more must opt in
# explicitly at the job level.
permissions:
contents: read
# Cancel superseded runs for the same PR to save runner minutes. Never cancel
# in-progress runs for `push` (main) or `merge_group` — those must complete so
# the default branch and merge queue always have a definitive result.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-and-test:
name: Build and Test
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# This job executes untrusted PR code (make build/test). Don't leave
# the GITHUB_TOKEN in the workspace git config while it runs.
persist-credentials: false
- name: Build all targets
run: make build
- name: Run all tests
run: make test
- name: Display test logs on failure
if: failure()
run: |
echo "=== Test logs ==="
find . -name "test.log" -exec echo "--- {} ---" \; -exec cat {} \; || echo "No test logs found"
integration:
name: Integration Tests
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 0
# Persist Bazel's disk cache (see .bazelrc: --disk_cache) across runs. The
# integration test drives nested `bazel query` against the repo itself; a
# cold cache makes the first GetChangedTargets pay the full analysis cost
# and pushes the run against its deadline. Warming it removes that spike
# and speeds every run. restore-keys lets a stale cache seed a new one.
- name: Cache Bazel disk cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/bazel-disk-cache
key: bazel-disk-integration-${{ hashFiles('MODULE.bazel', 'go.mod', '.bazelrc') }}
restore-keys: |
bazel-disk-integration-
- name: Run integration tests
run: make test-integration
- name: Display test logs on failure
if: failure()
run: |
echo "=== Integration test logs ==="
find . -name "test.log" -exec echo "--- {} ---" \; -exec cat {} \; || echo "No test logs found"
coverage:
name: Coverage
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- name: Run tests with coverage
run: make cover
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
with:
file: cover.out
verbose: true
dependencies:
name: Dependencies
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# This job executes untrusted PR code (go mod tidy, bazel/gazelle).
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- name: Run go mod tidy and bazel mod tidy
run: |
go mod tidy
./tools/bazel mod tidy
- name: Verify no uncommitted changes from mod tidy
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Dependencies are out of date. Please run 'go mod tidy' and 'bazel mod tidy' locally and commit the results."
git diff
exit 1
fi
- name: Run gazelle
run: make gazelle
- name: Verify no uncommitted changes from gazelle
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::BUILD.bazel files are out of date. Please run 'make gazelle' locally and commit the results."
git diff
exit 1
fi
lint:
name: Lint
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# This job executes untrusted PR code (gazelle, gofmt, goimports).
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- name: Run gazelle
run: make gazelle
- name: Run linters
run: |
gofmt -w .
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .
- name: Verify no uncommitted changes from linters
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Code is not formatted. Please run 'make gazelle', 'gofmt -w .', and 'goimports -w .' locally and commit the results."
git diff
exit 1
fi
# ---------------------------------------------------------------------------
# WORKFLOW SECURITY LINT
#
# Guards against regressions in the workflows themselves: actionlint checks
# general validity; zizmor audits for GitHub Actions security smells
# (dangerous triggers, unpinned `uses:`, credential persistence, template
# injection). Keeps the SHA-pinning / persist-credentials hardening from
# silently eroding in future edits.
# ---------------------------------------------------------------------------
workflow-security:
name: Workflow Security Lint
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: actionlint
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2
- name: zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
# Pin the zizmor TOOL version (distinct from the action tag above) for
# reproducible audits matching local validation.
version: "1.25.2"
# Fail the job on findings without requiring GitHub Advanced Security
# / SARIF upload (which needs security-events: write and degrades on
# fork PRs). Keeps the gate self-contained.
advanced-security: false
# ci.yml runs untrusted PR code under the `pull_request` trigger. A
# repository secret referenced anywhere on that path is reachable by a
# malicious PR and can be exfiltrated on the PR run (before any review),
# so this path must stay secret-free — route any secret-bearing step
# through a SEPARATE trusted workflow (workflow_run / pull_request_target)
# that does not execute PR code.
#
# This guard catches ACCIDENTAL reintroduction by honest contributors; a
# malicious actor controlling ci.yml could delete the guard itself, so the
# real defense remains CODEOWNERS review on .github/. GITHUB_TOKEN
# (least-privilege, read-only here) is allowlisted.
- name: Guard — no repository secrets on the untrusted-code path
run: |
hits="$(grep -rnE '\$\{\{[^}]*secrets\.' \
.github/workflows/ci.yml \
| grep -vE 'secrets\.GITHUB_TOKEN' || true)"
if [ -n "$hits" ]; then
echo "::error::Repository secret referenced on the untrusted-code CI path (ci.yml):" >&2
echo "$hits" >&2
echo "Move secret-bearing steps to a separate trusted workflow that does not run PR code." >&2
exit 1
fi
echo "OK: no repository secrets referenced in ci.yml."
# ---------------------------------------------------------------------------
# REQUIRED CHECKS GATE
#
# Fan-in aggregator that must turn RED when any required job fails, is
# cancelled, or is skipped. `if: always()` is critical: without it, this job
# is skipped when any `needs` dependency fails, and GitHub treats a skipped
# required status check as "not failed" — which would let a PR merge through
# the merge queue despite failing checks.
# ---------------------------------------------------------------------------
required-checks:
name: Required Checks
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-latest
needs:
- build-and-test
- integration
- coverage
- dependencies
- lint
- workflow-security
steps:
- name: Fail if any required check did not succeed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}
# Pass the job-results context via env (not inline ${{ }} in the script)
# so there is no template expansion inside the run block.
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
echo "One or more required checks did not succeed:" >&2
echo "$NEEDS_JSON" >&2
exit 1
- name: All required checks passed
run: echo "All required checks passed!"