2020permissions :
2121 contents : read
2222
23+ # Cancel superseded runs for the same PR to save runner minutes. Never cancel
24+ # in-progress runs for `push` (main) or `merge_group` — those must complete so
25+ # the default branch and merge queue always have a definitive result.
26+ concurrency :
27+ group : ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
28+ cancel-in-progress : ${{ github.event_name == 'pull_request' }}
29+
2330jobs :
2431 # ---------------------------------------------------------------------------
2532 # LINT (gofmt via Go SDK, yamlfmt via go run)
2936 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
3037 runs-on : ubuntu-latest
3138 steps :
32- - uses : actions/checkout@v4
39+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
40+ with :
41+ # This job executes untrusted PR code (make build/test/lint). Don't
42+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
43+ persist-credentials : false
3344 - uses : ./.github/actions/setup
3445
3546 - name : Run linters
4354 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
4455 runs-on : ubuntu-latest
4556 steps :
46- - uses : actions/checkout@v4
57+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
58+ with :
59+ # This job executes untrusted PR code (make build/test/lint). Don't
60+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
61+ persist-credentials : false
4762 - uses : ./.github/actions/setup
4863
4964 - name : Check module files are tidy
6075 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
6176 runs-on : ubuntu-latest
6277 steps :
63- - uses : actions/checkout@v4
78+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
79+ with :
80+ # This job executes untrusted PR code (make build/test/lint). Don't
81+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
82+ persist-credentials : false
6483 - uses : ./.github/actions/setup
6584
6685 - name : Build project
7796 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
7897 runs-on : ubuntu-latest
7998 steps :
80- - uses : actions/checkout@v4
99+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
100+ with :
101+ # This job executes untrusted PR code (make build/test/lint). Don't
102+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
103+ persist-credentials : false
81104 - uses : ./.github/actions/setup
82105
83106 - name : Run E2E tests
@@ -88,7 +111,11 @@ jobs:
88111 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
89112 runs-on : ubuntu-latest
90113 steps :
91- - uses : actions/checkout@v4
114+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
115+ with :
116+ # This job executes untrusted PR code (make build/test/lint). Don't
117+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
118+ persist-credentials : false
92119 - uses : ./.github/actions/setup
93120
94121 - name : Run Gateway integration tests
@@ -99,7 +126,11 @@ jobs:
99126 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
100127 runs-on : ubuntu-latest
101128 steps :
102- - uses : actions/checkout@v4
129+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
130+ with :
131+ # This job executes untrusted PR code (make build/test/lint). Don't
132+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
133+ persist-credentials : false
103134 - uses : ./.github/actions/setup
104135
105136 - name : Run Orchestrator integration tests
@@ -113,7 +144,11 @@ jobs:
113144 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
114145 runs-on : ubuntu-latest
115146 steps :
116- - uses : actions/checkout@v4
147+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
148+ with :
149+ # This job executes untrusted PR code (make build/test/lint). Don't
150+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
151+ persist-credentials : false
117152 - uses : ./.github/actions/setup
118153 - uses : ./.github/actions/run-bazel-test
119154 with :
@@ -124,7 +159,11 @@ jobs:
124159 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
125160 runs-on : ubuntu-latest
126161 steps :
127- - uses : actions/checkout@v4
162+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
163+ with :
164+ # This job executes untrusted PR code (make build/test/lint). Don't
165+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
166+ persist-credentials : false
128167 - uses : ./.github/actions/setup
129168 - uses : ./.github/actions/run-bazel-test
130169 with :
@@ -135,7 +174,11 @@ jobs:
135174 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
136175 runs-on : ubuntu-latest
137176 steps :
138- - uses : actions/checkout@v4
177+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
178+ with :
179+ # This job executes untrusted PR code (make build/test/lint). Don't
180+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
181+ persist-credentials : false
139182 - uses : ./.github/actions/setup
140183 - uses : ./.github/actions/run-bazel-test
141184 with :
@@ -149,12 +192,71 @@ jobs:
149192 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
150193 runs-on : ubuntu-latest
151194 steps :
152- - uses : actions/checkout@v4
195+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
196+ with :
197+ # This job executes untrusted PR code (make build/test/lint). Don't
198+ # leave the GITHUB_TOKEN in the workspace git config while it runs.
199+ persist-credentials : false
153200 - uses : ./.github/actions/setup
154201 - uses : ./.github/actions/run-bazel-test
155202 with :
156203 target : //test/integration/submitqueue/core/consumer/...
157204
205+ # ---------------------------------------------------------------------------
206+ # WORKFLOW SECURITY LINT
207+ #
208+ # Guards against regressions in the workflows themselves: actionlint checks
209+ # general validity; zizmor audits for GitHub Actions security smells
210+ # (dangerous triggers, unpinned `uses:`, credential persistence, template
211+ # injection). Keeps the pull_request_target / SHA-pinning hardening from
212+ # silently eroding in future edits.
213+ # ---------------------------------------------------------------------------
214+ workflow-security :
215+ name : Workflow Security Lint
216+ if : ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
217+ runs-on : ubuntu-latest
218+ steps :
219+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
220+ with :
221+ persist-credentials : false
222+ - name : actionlint
223+ uses : raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2
224+ - name : zizmor
225+ uses : zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
226+ with :
227+ # Pin the zizmor TOOL version (distinct from the action tag above) for
228+ # reproducible audits matching local validation.
229+ version : " 1.25.2"
230+ # Fail the job on findings without requiring GitHub Advanced Security
231+ # / SARIF upload (which needs security-events: write and degrades on
232+ # fork PRs). Keeps the gate self-contained.
233+ advanced-security : false
234+
235+ # ci.yml and the composite actions it calls run untrusted PR code under
236+ # the `pull_request` trigger. A repository secret referenced anywhere on
237+ # that path is reachable by a malicious PR and can be exfiltrated on the
238+ # PR run (before any review), so this path must stay secret-free — route
239+ # any secret-bearing step through a SEPARATE trusted workflow
240+ # (workflow_run / pull_request_target) that does not execute PR code.
241+ #
242+ # This guard catches ACCIDENTAL reintroduction by honest contributors; a
243+ # malicious actor controlling ci.yml could delete the guard itself, so the
244+ # real defenses remain (a) secrets scoped to a main-only Environment so a
245+ # PR-triggered job cannot obtain them, and (b) CODEOWNERS review on
246+ # .github/. GITHUB_TOKEN (least-privilege, read-only here) is allowlisted.
247+ - name : Guard — no repository secrets on the untrusted-code path
248+ run : |
249+ hits="$(grep -rnE '\$\{\{[^}]*secrets\.' \
250+ .github/workflows/ci.yml .github/actions \
251+ | grep -vE 'secrets\.GITHUB_TOKEN' || true)"
252+ if [ -n "$hits" ]; then
253+ echo "::error::Repository secret referenced on the untrusted-code CI path (ci.yml / composite actions):" >&2
254+ echo "$hits" >&2
255+ echo "Move secret-bearing steps to a separate trusted workflow that does not run PR code." >&2
256+ exit 1
257+ fi
258+ echo "OK: no repository secrets referenced in ci.yml or its composite actions."
259+
158260 # ---------------------------------------------------------------------------
159261 # REQUIRED CHECKS GATE
160262 #
@@ -179,12 +281,17 @@ jobs:
179281 - queue-integration-test
180282 - storage-integration-test
181283 - consumer-integration-test
284+ - workflow-security
182285 steps :
183286 - name : Fail if any required check did not succeed
184287 if : ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}
288+ # Pass the job-results context via env (not inline ${{ }} in the script)
289+ # so there is no template expansion inside the run block.
290+ env :
291+ NEEDS_JSON : ${{ toJSON(needs) }}
185292 run : |
186293 echo "One or more required checks did not succeed:" >&2
187- echo '${{ toJSON(needs) }}' >&2
294+ echo "$NEEDS_JSON" >&2
188295 exit 1
189296
190297 - name : All required checks passed
0 commit comments