Skip to content

Commit a99f1d3

Browse files
aledbfclaude
andcommitted
refactor(ci): unify the two runtime parity matrices into one event-parameterized job
The push/PR scoped lane and the daily full lane were two near-identical sharded matrix jobs (plus two separate gate jobs and a separate coverage-report), differing only in scope, binary, and trigger. Collapse them into one `parity-runtime` matrix (4 shards) parameterized by event: push/PR — scoped to the affected commands, plain binary: a fast gate. daily — the full matrix with the instrumented binary, so one run gates parity AND measures coverage; still gated by daily-changes on quiet days. A single `parity-runtime-gate` absorbs the old push/PR aggregator, the daily report, and the per-PR coverage-report: it gates the matrix (and, on daily, publish) and merges every lane's covdata into the one cross-lane number, on all events. Runtime parity goes from 7 jobs to 4 (daily-changes, parity-runtime, parity-publish-full, parity-runtime-gate). main is not branch-protected, so the check renames are safe. The sharding/reuse/gating regression tests are updated for the unified job names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9a9381c commit a99f1d3

2 files changed

Lines changed: 94 additions & 175 deletions

File tree

.github/workflows/go-cli.yml

Lines changed: 80 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -133,94 +133,10 @@ jobs:
133133
path: artifacts/coverage/data/contract
134134
if-no-files-found: error
135135

136-
# Runtime lane of the parity matrix (creates real containers via Docker) on
137-
# push/PR, SCOPED to the commands affected by the change: the full matrix takes
138-
# ~45 min, so the devtool parity-affected mapping reduces the diff to a
139-
# PARITY_COMMAND allowlist (or "all"/"none"). The daily parity-runtime-full job
140-
# is the backstop that always runs the whole thing.
141-
parity-runtime-shard:
142-
if: github.event_name == 'push' || github.event_name == 'pull_request'
143-
runs-on: ubuntu-latest
144-
needs: lint-and-test
145-
strategy:
146-
# Split the affected runtime cases across independent runners (each with its
147-
# own docker daemon). fail-fast off so one shard's failure still reports the
148-
# others. The harness selects this shard's slice via PARITY_SHARD_INDEX.
149-
fail-fast: false
150-
matrix:
151-
shard: [0, 1]
152-
env:
153-
PARITY_RUNTIME_TIMEOUT: "10m"
154-
# Two shards, each running two cases at a time: the per-case isolation
155-
# (unique id-labels / COMPOSE_PROJECT_NAME / BUILDX_BUILDER) makes -parallel 2
156-
# safe on a 2-core runner now that each runner only owns half the cases.
157-
PARITY_PARALLEL: "2"
158-
PARITY_SHARD_INDEX: ${{ matrix.shard }}
159-
PARITY_SHARD_TOTAL: "2"
160-
steps:
161-
- uses: actions/checkout@v7
162-
with:
163-
submodules: recursive
164-
# Full history so the diff base (PR base / previous push) is present.
165-
fetch-depth: 0
166-
# devtool (the affected-commands mapping) runs before the composite setup,
167-
# so it needs the Go toolchain here.
168-
- uses: actions/setup-go@v6
169-
with:
170-
go-version-file: go.mod
171-
- name: Determine affected parity commands
172-
id: affected
173-
run: |
174-
if [ "${{ github.event_name }}" = "pull_request" ]; then
175-
base="${{ github.event.pull_request.base.sha }}"; head="${{ github.event.pull_request.head.sha }}"
176-
else
177-
base="${{ github.event.before }}"; head="${{ github.sha }}"
178-
fi
179-
go run ./cmd/devtool parity-affected-commands "$base" "$head" >> "$GITHUB_OUTPUT"
180-
# Toolchain + runner prep + reference oracle, shared with parity-runtime-full.
181-
- if: steps.affected.outputs.run == 'true'
182-
uses: ./.github/actions/setup-parity-runtime
183-
with:
184-
repo-token: ${{ secrets.GITHUB_TOKEN }}
185-
- if: steps.affected.outputs.run == 'true'
186-
env:
187-
PARITY_COMMAND: ${{ steps.affected.outputs.commands }}
188-
run: task parity:runtime
189-
- if: always() && steps.affected.outputs.run == 'true'
190-
run: |
191-
mkdir -p artifacts
192-
git -C reference rev-parse HEAD > artifacts/reference-commit.txt
193-
- uses: actions/upload-artifact@v7
194-
if: always() && steps.affected.outputs.run == 'true'
195-
with:
196-
name: parity-runtime-v0.88.0-shard-${{ matrix.shard }}
197-
path: artifacts/
198-
if-no-files-found: error
199-
- if: always() && steps.affected.outputs.run == 'true'
200-
run: task clean
201-
202-
# Stable required-check name aggregating the sharded runtime lane, so sharding
203-
# does not change the check name branch protection depends on.
204-
parity-runtime:
205-
if: always() && (github.event_name == 'push' || github.event_name == 'pull_request')
206-
runs-on: ubuntu-latest
207-
needs: parity-runtime-shard
208-
steps:
209-
- name: Gate on all runtime shards
210-
run: |
211-
result="${{ needs.parity-runtime-shard.result }}"
212-
echo "parity-runtime-shard aggregate result: $result"
213-
case "$result" in
214-
success|skipped) exit 0 ;;
215-
*) echo "one or more runtime shards failed"; exit 1 ;;
216-
esac
217-
218-
# Gate the expensive daily lanes on "did the repo change today?". A manual dispatch
219-
# always runs; a scheduled run only proceeds if HEAD has a commit within the last
220-
# day (devtool daily-changed). On a private/billed repo this skips the full
221-
# runtime matrix + publish on quiet days. Runs on schedule/dispatch so it is never
222-
# skipped for those events — the heavy jobs `needs:` it, and a skipped need would
223-
# skip them regardless of its output.
136+
# "Did the repo change today?" gate for the expensive daily lanes. Manual dispatch
137+
# always runs; a scheduled run only proceeds when HEAD has a commit within the last
138+
# day (devtool daily-changed) — on a private/billed repo this skips the daily matrix
139+
# on quiet days. Runs on schedule/dispatch so the daily lanes' `needs:` is satisfied.
224140
daily-changes:
225141
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
226142
runs-on: ubuntu-latest
@@ -240,66 +156,101 @@ jobs:
240156
go run ./cmd/devtool daily-changed >> "$GITHUB_OUTPUT"
241157
fi
242158
243-
# Full runtime parity matrix. Runs once a day (schedule, only when the repo changed
244-
# — see daily-changes) and on manual dispatch — the exhaustive backstop for the
245-
# scoped push/PR job above. Sharded across independent runners (same
246-
# PARITY_SHARD_INDEX/TOTAL harness as the push/PR lane) so the 175-case matrix no
247-
# longer contends for one runner's CPU/disk/daemon, which was starving the
248-
# container setups and failing downstream exec cases. PARITY_SHARD_TOTAL MUST equal
249-
# the length of matrix.shard — parity_sharding_workflow_test.go enforces that, so a
250-
# mismatch can't silently drop cases.
251-
parity-runtime-full:
159+
# Single sharded runtime parity matrix, parameterized by event:
160+
# push/PR — SCOPED to the commands the diff can affect (devtool parity-affected),
161+
# run with the plain binary: a fast gate (the full matrix is ~45 min).
162+
# daily — the FULL matrix with the instrumented binary, so one run gates parity
163+
# AND measures coverage; daily-changes keeps it off on quiet days.
164+
# 4 shards across independent runners (each its own docker daemon) so the matrix
165+
# never contends for one runner's CPU/disk/daemon. PARITY_SHARD_TOTAL MUST equal the
166+
# length of matrix.shard — parity_sharding_workflow_test.go enforces it so a mismatch
167+
# can't silently drop cases.
168+
parity-runtime:
252169
needs: [lint-and-test, daily-changes]
253-
if: needs.daily-changes.outputs.run == 'true'
170+
# Every push/PR, and on schedule/dispatch only when the repo changed. always() so a
171+
# skipped daily-changes (push/PR) doesn't skip this job; the explicit lint-and-test
172+
# check restores the normal "needs succeeded" gate.
173+
if: >-
174+
always()
175+
&& needs.lint-and-test.result == 'success'
176+
&& (github.event_name == 'push' || github.event_name == 'pull_request'
177+
|| needs.daily-changes.outputs.run == 'true')
254178
runs-on: ubuntu-latest
255179
strategy:
256-
# fail-fast off so one shard's failure still reports the others.
257180
fail-fast: false
258181
matrix:
259182
shard: [0, 1, 2, 3]
260183
env:
261184
PARITY_RUNTIME_TIMEOUT: "10m"
262-
# Each shard owns ~a quarter of the matrix, so -parallel 2 per runner matches
263-
# the push/PR shard lane's proven-safe load instead of the old single-runner 3.
185+
# Each shard owns ~a quarter of the selected cases; -parallel 2 per 2-core runner
186+
# is the proven-safe load (per-case id-label / COMPOSE_PROJECT_NAME isolation).
264187
PARITY_PARALLEL: "2"
265188
PARITY_SHARD_INDEX: ${{ matrix.shard }}
266189
PARITY_SHARD_TOTAL: "4"
267190
steps:
268191
- uses: actions/checkout@v7
269192
with:
270193
submodules: recursive
271-
- uses: ./.github/actions/setup-parity-runtime
194+
# Full history so the push/PR diff base is present for the affected mapping.
195+
fetch-depth: 0
196+
# devtool (the affected mapping) runs before the composite setup, so Go here.
197+
- uses: actions/setup-go@v6
198+
with:
199+
go-version-file: go.mod
200+
- name: Plan this run
201+
id: plan
202+
run: |
203+
if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "pull_request" ]; then
204+
if [ "${{ github.event_name }}" = "pull_request" ]; then
205+
base="${{ github.event.pull_request.base.sha }}"; head="${{ github.event.pull_request.head.sha }}"
206+
else
207+
base="${{ github.event.before }}"; head="${{ github.sha }}"
208+
fi
209+
go run ./cmd/devtool parity-affected-commands "$base" "$head" >> "$GITHUB_OUTPUT"
210+
else
211+
# daily: the full matrix (this job only runs when daily-changes said so).
212+
echo "run=true" >> "$GITHUB_OUTPUT"
213+
echo "commands=all" >> "$GITHUB_OUTPUT"
214+
fi
215+
- if: steps.plan.outputs.run == 'true'
216+
uses: ./.github/actions/setup-parity-runtime
272217
with:
273218
repo-token: ${{ secrets.GITHUB_TOKEN }}
274-
# Instrumented binary: measures coverage AND gates parity on this shard's slice.
275-
- run: task coverage:parity-runtime
219+
# push/PR: scoped, plain binary — a fast gate.
220+
- name: Run parity (scoped, plain)
221+
if: steps.plan.outputs.run == 'true' && (github.event_name == 'push' || github.event_name == 'pull_request')
222+
env:
223+
PARITY_COMMAND: ${{ steps.plan.outputs.commands }}
224+
run: task parity:runtime
225+
# daily: full matrix, instrumented binary — gates parity AND measures coverage.
226+
- name: Run parity (full, instrumented)
227+
if: steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
228+
run: task coverage:parity-runtime
276229
- name: Coverage summary
277-
if: always()
230+
if: always() && steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
278231
run: go run ./cmd/devtool coverage-report artifacts/coverage/data/runtime "parity:runtime shard ${{ matrix.shard }}" >> "$GITHUB_STEP_SUMMARY"
279-
- if: always()
232+
- if: always() && steps.plan.outputs.run == 'true'
280233
run: |
281234
mkdir -p artifacts
282235
git -C reference rev-parse HEAD > artifacts/reference-commit.txt
283236
- uses: actions/upload-artifact@v7
284-
if: always()
237+
if: always() && steps.plan.outputs.run == 'true'
285238
with:
286-
name: parity-runtime-full-v0.88.0-shard-${{ matrix.shard }}
239+
name: parity-runtime-v0.88.0-shard-${{ matrix.shard }}
287240
path: artifacts/
288241
if-no-files-found: error
289-
# covdata slice for the cross-lane merge (distinct name per shard so the merge
290-
# job unions them instead of clobbering).
242+
# daily only: covdata slice for the cross-lane merge (distinct name per shard).
291243
- uses: actions/upload-artifact@v7
292-
if: always()
244+
if: always() && steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
293245
with:
294246
name: covdata-runtime-shard-${{ matrix.shard }}
295247
path: artifacts/coverage/data/runtime
296248
if-no-files-found: error
297-
- if: always()
249+
- if: always() && steps.plan.outputs.run == 'true'
298250
run: task clean
299251

300-
# Publish parity (features/templates → ephemeral OCI registry). Independent of the
301-
# runtime matrix, so it runs as its own job rather than stealing a runtime shard's
302-
# runner. Was folded into the old monolithic parity-runtime-full.
252+
# Publish parity (features/templates → ephemeral OCI registry). Daily only, its own
253+
# job so it doesn't steal a runtime shard's runner.
303254
parity-publish-full:
304255
needs: [lint-and-test, daily-changes]
305256
if: needs.daily-changes.outputs.run == 'true'
@@ -324,14 +275,15 @@ jobs:
324275
- if: always()
325276
run: task clean
326277

327-
# Daily gate + cross-lane coverage. Aggregates the sharded runtime lane and the
328-
# publish lane into one pass/fail (mirrors the push/PR parity-runtime gate) and
329-
# merges every lane's covdata — unit + e2e + contract (which also run on schedule)
330-
# plus the runtime shards + publish — into the one true cross-lane number.
331-
parity-runtime-full-report:
332-
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
278+
# Single gate + cross-lane coverage for ALL events (absorbs the old push/PR
279+
# parity-runtime aggregator, the daily report, and the per-PR coverage-report):
280+
# gates the runtime matrix (and, on daily, publish) into one pass/fail, and merges
281+
# every lane's covdata (unit + e2e + contract, plus the daily runtime shards +
282+
# publish) into the one true cross-lane number.
283+
parity-runtime-gate:
284+
if: always()
333285
runs-on: ubuntu-latest
334-
needs: [lint-and-test, e2e, parity, parity-runtime-full, parity-publish-full]
286+
needs: [lint-and-test, e2e, parity, parity-runtime, parity-publish-full]
335287
steps:
336288
- uses: actions/checkout@v7
337289
- uses: actions/setup-go@v6
@@ -357,56 +309,23 @@ jobs:
357309
run: task coverage:merge | tee -a "$GITHUB_STEP_SUMMARY"
358310
- uses: actions/upload-artifact@v7
359311
with:
360-
name: covdata-merged-full
312+
name: covdata-merged
361313
path: artifacts/coverage/data/merged.out
362314
if-no-files-found: warn
363-
# Final verdict last, so the coverage report is posted even when a shard failed.
364-
- name: Gate on all runtime shards + publish
315+
# Final verdict last, so coverage is posted even when a shard failed. A skipped
316+
# lane (publish on push/PR, or the whole matrix on a quiet daily) counts as pass.
317+
- name: Gate on runtime + publish
365318
run: |
366-
runtime="${{ needs.parity-runtime-full.result }}"
319+
runtime="${{ needs.parity-runtime.result }}"
367320
publish="${{ needs.parity-publish-full.result }}"
368-
echo "runtime shards: $runtime, publish: $publish"
321+
echo "runtime: $runtime, publish: $publish"
369322
for r in "$runtime" "$publish"; do
370323
case "$r" in
371324
success|skipped) ;;
372325
*) echo "runtime/publish parity failed"; exit 1 ;;
373326
esac
374327
done
375328
376-
# Cross-lane coverage: merge the per-PR lanes' covdata (unit + e2e + contract)
377-
# into the one number no single lane shows, and post it to the run summary.
378-
coverage-report:
379-
if: always() && (github.event_name == 'push' || github.event_name == 'pull_request')
380-
runs-on: ubuntu-latest
381-
needs: [lint-and-test, e2e, parity]
382-
steps:
383-
- uses: actions/checkout@v7
384-
- uses: actions/setup-go@v6
385-
with:
386-
go-version-file: go.mod
387-
- uses: go-task/setup-task@v2
388-
with:
389-
version: 3.x
390-
repo-token: ${{ secrets.GITHUB_TOKEN }}
391-
- name: Download lane covdata
392-
uses: actions/download-artifact@v8
393-
with:
394-
pattern: covdata-*
395-
path: artifacts/coverage/data
396-
# download-artifact nests each artifact under covdata-<lane>/; flatten to <lane>/.
397-
- name: Normalize lane dirs
398-
run: |
399-
cd artifacts/coverage/data
400-
for d in covdata-*; do [ -d "$d" ] && mv "$d" "${d#covdata-}"; done
401-
ls -R . || true
402-
- name: Merged coverage report
403-
run: task coverage:merge | tee -a "$GITHUB_STEP_SUMMARY"
404-
- uses: actions/upload-artifact@v7
405-
with:
406-
name: covdata-merged
407-
path: artifacts/coverage/data/merged.out
408-
if-no-files-found: warn
409-
410329
cross-compile:
411330
runs-on: ubuntu-latest
412331
needs: lint-and-test

internal/cli/parity_sharding_workflow_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,25 +109,24 @@ func TestWorkflowShardMatrixMatchesShardTotal(t *testing.T) {
109109
}
110110
}
111111

112-
// Both the push/PR (parity-runtime-shard) and the daily (parity-runtime-full)
113-
// lanes are sharded; if neither is found the parsing (or the jobs) regressed.
114-
if sharded < 2 {
115-
t.Errorf("found %d sharded parity jobs, want at least 2 (push/PR + daily full)", sharded)
112+
// The unified runtime lane (parity-runtime) is sharded; if it isn't found the
113+
// parsing (or the job) regressed.
114+
if sharded < 1 {
115+
t.Errorf("found %d sharded parity jobs, want at least 1 (parity-runtime)", sharded)
116116
}
117117
}
118118

119-
// TestWorkflowRuntimeJobsShareCompositeAction locks in the reuse between the two
120-
// runtime lanes: both must set up their runner through the shared composite action
121-
// (toolchain + ci:prepare-runner + reference) so the push/PR and daily environments
122-
// can't drift apart. (A lane may also add a bare setup-go before the composite —
123-
// parity-runtime-shard needs the Go toolchain for the devtool affected-commands
124-
// step that decides whether the composite even runs — but node/task setup belongs
125-
// only in the shared action, never re-inlined.)
119+
// TestWorkflowRuntimeJobsShareCompositeAction locks in the reuse for the runtime
120+
// lanes: they must set up their runner through the shared composite action
121+
// (toolchain + ci:prepare-runner + reference) so the environments can't drift apart.
122+
// (A lane may also add a bare setup-go before the composite — parity-runtime needs
123+
// the Go toolchain for the devtool plan step that decides whether the composite even
124+
// runs — but node/task setup belongs only in the shared action, never re-inlined.)
126125
func TestWorkflowRuntimeJobsShareCompositeAction(t *testing.T) {
127126
wf := loadWorkflow(t)
128127
const action = "./.github/actions/setup-parity-runtime"
129128

130-
for _, name := range []string{"parity-runtime-shard", "parity-runtime-full"} {
129+
for _, name := range []string{"parity-runtime", "parity-publish-full"} {
131130
job, ok := wf.Jobs[name]
132131
if !ok {
133132
t.Errorf("job %q missing", name)
@@ -152,14 +151,15 @@ func TestWorkflowRuntimeJobsShareCompositeAction(t *testing.T) {
152151
// TestWorkflowDailyLanesGatedOnChanges asserts the expensive daily lanes only run
153152
// when the repo changed that day: each must `needs: daily-changes` and gate on its
154153
// output. Dropping either turns the change-detection into a no-op and the matrix
155-
// runs (and bills) every day regardless.
154+
// runs (and bills) every day regardless. (parity-runtime also runs on push/PR; its
155+
// `if` gates the daily path on the same output.)
156156
func TestWorkflowDailyLanesGatedOnChanges(t *testing.T) {
157157
wf := loadWorkflow(t)
158158

159159
if _, ok := wf.Jobs["daily-changes"]; !ok {
160160
t.Fatal("daily-changes job missing — nothing gates the daily lanes")
161161
}
162-
for _, name := range []string{"parity-runtime-full", "parity-publish-full"} {
162+
for _, name := range []string{"parity-runtime", "parity-publish-full"} {
163163
job, ok := wf.Jobs[name]
164164
if !ok {
165165
t.Errorf("job %q missing", name)

0 commit comments

Comments
 (0)