-
Notifications
You must be signed in to change notification settings - Fork 0
607 lines (585 loc) · 30.4 KB
/
Copy pathci.yml
File metadata and controls
607 lines (585 loc) · 30.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
name: CI
on:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
GOLANGCI_LINT: github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
GOVULNCHECK: golang.org/x/vuln/cmd/govulncheck@v1.3.0
# benchstat lives in golang.org/x/perf, which ships only pseudo-versions.
# Pinned to the latest published pseudo-version so every run uses the same tool.
BENCHSTAT: golang.org/x/perf/cmd/benchstat@v0.0.0-20260512194132-3cf34090a3db
jobs:
# Detect whether anything other than docs changed. Docs-only PRs skip the Go
# lint/test/vuln/coverage/bench jobs entirely; the `gate` job below still runs
# and is the single required status check, so such PRs stay mergeable.
changes:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.detect.outputs.code }}
docs: ${{ steps.detect.outputs.docs }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Detect non-docs changes
id: detect
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# --quiet exits 0 (no diff) only when every changed path is excluded —
# i.e. the PR touches only docs/markdown or build-inert files →
# code=false. Any other path (Go, go.mod/sum, go.work, .golangci.yml,
# workflows) yields a diff → code=true. If the diff errors for any
# reason, control falls through to code=true (fail-safe: run CI).
# NOTE: the excluded set is deliberately limited to files that cannot
# affect a Go build or the CI itself — do NOT add go.work, workflows,
# or lint config here.
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- \
':(exclude)*.md' ':(exclude)docs/**' ':(exclude)LICENSE' ':(exclude)NOTICE' \
':(exclude).gitignore' ':(exclude).gitattributes' ':(exclude).env.example'; then
echo "code=false" >> "$GITHUB_OUTPUT"
echo "Docs/config-only change; skipping Go lint/test/vuln/coverage/bench." >> "$GITHUB_STEP_SUMMARY"
else
echo "code=true" >> "$GITHUB_OUTPUT"
fi
# Inverse detection for the docs site. --quiet exits 1 (diff present)
# when any docs/** path changed → docs=true (build the site). Exit 0
# (no docs diff) → docs=false (skip the docs job; gate treats skip as
# pass). Failsafe: any error path leaves docs=true so the build runs.
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- 'docs/**'; then
echo "docs=false" >> "$GITHUB_OUTPUT"
else
echo "docs=true" >> "$GITHUB_OUTPUT"
echo "docs/** changed; running the docs site build." >> "$GITHUB_STEP_SUMMARY"
fi
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
cache-dependency-path: "**/go.sum"
- name: golangci-lint (state)
working-directory: state
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (state/expr)
working-directory: state/expr
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (durable)
working-directory: durable
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (cluster)
working-directory: cluster
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (transport)
working-directory: transport
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (wasm)
working-directory: wasm
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (e2e)
working-directory: e2e
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (examples/fooddelivery)
working-directory: examples/fooddelivery
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (examples/dispatch)
working-directory: examples/dispatch
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (telemetry)
working-directory: telemetry
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (telemetry/slog)
working-directory: telemetry/slog
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (telemetry/otel)
working-directory: telemetry/otel
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (telemetry/datadog)
working-directory: telemetry/datadog
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink)
working-directory: sink
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink/bridge)
working-directory: sink/bridge
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink/file)
working-directory: sink/file
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink/http)
working-directory: sink/http
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink/prometheus)
working-directory: sink/prometheus
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (sink/slog)
working-directory: sink/slog
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (source)
working-directory: source
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (source/statemachine)
working-directory: source/statemachine
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
- name: golangci-lint (tools/docsgen)
working-directory: tools/docsgen
run: go run "$GOLANGCI_LINT" run --config "$GITHUB_WORKSPACE/.golangci.yml" ./...
# The state-machine race-test matrix lives in a reusable workflow so its legs
# render as a single collapsible "state machine tests / …" tree in the checks
# UI rather than as flat top-level checks. The sink-domain legs live in their
# own "sink" tree (see the `sink` job below). Keep this module list in sync
# with the lint, vuln, and coverage jobs below.
tests:
name: state machine tests
needs: changes
if: needs.changes.outputs.code == 'true'
uses: ./.github/workflows/test.yml
with:
modules: "state state/expr durable cluster transport wasm e2e examples/fooddelivery examples/dispatch telemetry telemetry/slog telemetry/otel telemetry/datadog"
# Every sink-domain check (in-workspace race tests, the SDK-backed destination
# matrix, and the integration leg) groups under a single collapsible "sink / …"
# tree via this reusable workflow.
sink:
needs: changes
if: needs.changes.outputs.code == 'true'
uses: ./.github/workflows/sink.yml
# Every source-domain check (in-workspace race tests, the SDK-backed source
# module matrix, and the integration leg) groups under a single collapsible
# "source / …" tree via this reusable workflow.
source:
needs: changes
if: needs.changes.outputs.code == 'true'
uses: ./.github/workflows/source.yml
vuln:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
cache-dependency-path: "**/go.sum"
- name: govulncheck (state)
working-directory: state
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (state/expr)
working-directory: state/expr
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (durable)
working-directory: durable
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (cluster)
working-directory: cluster
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (transport)
working-directory: transport
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (wasm)
working-directory: wasm
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (e2e)
working-directory: e2e
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (examples/fooddelivery)
working-directory: examples/fooddelivery
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (examples/dispatch)
working-directory: examples/dispatch
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (telemetry)
working-directory: telemetry
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (telemetry/slog)
working-directory: telemetry/slog
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (telemetry/otel)
working-directory: telemetry/otel
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (telemetry/datadog)
working-directory: telemetry/datadog
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink)
working-directory: sink
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink/bridge)
working-directory: sink/bridge
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink/file)
working-directory: sink/file
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink/http)
working-directory: sink/http
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink/prometheus)
working-directory: sink/prometheus
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (sink/slog)
working-directory: sink/slog
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (source)
working-directory: source
run: go run "$GOVULNCHECK" ./...
- name: govulncheck (source/statemachine)
working-directory: source/statemachine
run: go run "$GOVULNCHECK" ./...
coverage:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
env:
THRESHOLD: "80"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
cache-dependency-path: "**/go.sum"
- name: coverage gate (state)
working-directory: state
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (state/expr)
working-directory: state/expr
# The rich expression tier carries a tighter coverage floor than the suite
# default: its CEL compile/lower/marshal seams are the bulk of its surface.
# Floor is 88 (not 90): the rich-assign reducer is a total AssignFn with no
# error channel, so it must defensively swallow marshal/convert failures that
# are unreachable given authoring-time type-checking; those branches cannot be
# exercised, and contorting the reducer to cover them would degrade it.
env:
THRESHOLD: "88"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (durable)
working-directory: durable
# The durable runtime carries a tighter coverage floor than the suite
# default: the Store contract and its reference impl are the bulk of its
# surface and are exercised directly by the contract tests.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (cluster)
working-directory: cluster
# The distribution runtime carries a tighter coverage floor than the suite
# default: its routing, supervision, and migration seams are the bulk of its
# surface and are exercised directly by multi-node tests.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (wasm)
working-directory: wasm
# Floor is 88 (not 90): the WASM ABI's remaining uncovered branches are
# adversarial-only — host-side alloc/write failures and malformed-response
# decoding that require a broken guest or runtime to reach; the happy path,
# the guard-through-Fire path, and the out-of-range-response defense are all
# covered. wazero is pure-Go, so this module adds no CGo.
env:
THRESHOLD: "88"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (transport)
working-directory: transport
# The gRPC transport carries the suite's tighter floor: its codec, client,
# and server seams are exercised directly over an in-memory gRPC connection.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (examples/fooddelivery)
working-directory: examples/fooddelivery
# The flagship example carries a tighter floor than the suite default: it is
# the showcase, exercised end to end by scenario and Example tests.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (examples/dispatch)
working-directory: examples/dispatch
# The flagship showcase carries a tighter floor than the suite default: it is
# the showcase, exercised end to end by scenario and Example tests.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (telemetry)
working-directory: telemetry
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (telemetry/slog)
working-directory: telemetry/slog
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (telemetry/otel)
working-directory: telemetry/otel
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (telemetry/datadog)
working-directory: telemetry/datadog
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink)
working-directory: sink
# The fan-out core holds a tighter floor than the suite default: Manifold,
# Registry, Op, Emitter, Reservoir, and Poller are the load-bearing surface.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink/bridge)
working-directory: sink/bridge
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink/file)
working-directory: sink/file
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink/http)
working-directory: sink/http
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink/prometheus)
working-directory: sink/prometheus
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (sink/slog)
working-directory: sink/slog
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (source)
working-directory: source
# The ingestion core holds a tighter floor than the suite default: the
# Hopper, Handler, codec, and capability seams are the load-bearing surface.
env:
THRESHOLD: "90"
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
- name: coverage gate (source/statemachine)
working-directory: source/statemachine
run: |
go test -covermode=atomic -coverprofile=coverage.out ./...
pct=$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/,"",$3); print $3}')
echo "total coverage: ${pct}% (threshold ${THRESHOLD}%)"
awk -v p="$pct" -v t="$THRESHOLD" 'BEGIN { exit (p+0 < t+0) ? 1 : 0 }' \
|| { echo "::error::coverage ${pct}% is below the ${THRESHOLD}% threshold"; exit 1; }
# Benchmark regression gate. Runs the state benchmarks on the PR head and on
# the PR base on the SAME runner, then benchstat-diffs them. Running both refs
# on one machine cancels machine-to-machine variance, so a real regression is
# distinguishable from runner jitter. The benchstat table is written to the
# job's step summary for visibility, and the job fails if any gated metric
# (sec/op, allocs/op) regresses past the threshold below.
bench:
# Skipped entirely on docs-only PRs (see the `changes` job); on code PRs the
# inner step further narrows to Go-source changes before benchmarking.
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
env:
# Maximum allowed head/base ratio before the gate fails (1.20 = +20%).
# Deliberately generous to absorb shared-runner and micro-benchmark jitter;
# tighten once the benchmark history on CI is shown to be stable.
BENCH_THRESHOLD: "1.20"
# -count balances stability against wall-clock time. 8 runs per benchmark,
# twice (base + head), keeps the job well under a few minutes for state.
BENCH_COUNT: "8"
# SHAs come from the trusted github context (not from any user-controlled
# title/body), and are consumed as env vars rather than interpolated into
# the shell, so there is no script-injection surface.
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need both base and head commits present for the worktree diff.
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
cache-dependency-path: "**/go.sum"
# Skip the (relatively expensive) bench run on docs-only PRs. The gate only
# cares about Go sources and module files; anything else can't move the
# benchmarks. Keeping this as an in-job guard (rather than a workflow-level
# paths-ignore) avoids skipping the lint/test/vuln jobs on the same PR.
- name: Detect Go-code changes
id: changes
run: |
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- '*.go' 'go.mod' 'go.sum'; then
echo "go_changed=false" >> "$GITHUB_OUTPUT"
echo "No Go-code changes; skipping benchmark gate." >> "$GITHUB_STEP_SUMMARY"
else
echo "go_changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Benchmark head
if: steps.changes.outputs.go_changed == 'true'
working-directory: state
run: |
go test -run='^$' -bench=. -benchmem -count="$BENCH_COUNT" ./... \
| tee "$RUNNER_TEMP/head.txt"
- name: Benchmark base
if: steps.changes.outputs.go_changed == 'true'
run: |
# Check the base commit out into a sibling worktree and bench it there,
# so the head working tree is left untouched.
git worktree add --detach "$RUNNER_TEMP/base" "$BASE_SHA"
go test -C "$RUNNER_TEMP/base/state" -run='^$' -bench=. -benchmem \
-count="$BENCH_COUNT" ./... | tee "$RUNNER_TEMP/base.txt"
git worktree remove --force "$RUNNER_TEMP/base"
- name: Compare (benchstat) and gate
if: steps.changes.outputs.go_changed == 'true'
run: |
# Render the comparison table to the step summary for visibility.
{
echo '### Benchmark comparison (base → head)'
echo
echo "Threshold: head/base ratio must stay ≤ ${BENCH_THRESHOLD} for sec/op and allocs/op."
echo
echo '```'
go run "$BENCHSTAT" "$RUNNER_TEMP/base.txt" "$RUNNER_TEMP/head.txt"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Gate on the machine-readable CSV form. New/removed benchmarks are
# skipped by the parser, never failed.
go run "$BENCHSTAT" -format csv "$RUNNER_TEMP/base.txt" "$RUNNER_TEMP/head.txt" \
> "$RUNNER_TEMP/compare.csv"
# Capture the verdict so it lands in the summary, then propagate the
# gate's real exit status (tee would otherwise mask a failure).
set -o pipefail
awk -v THRESHOLD="$BENCH_THRESHOLD" \
-f "$GITHUB_WORKSPACE/.github/scripts/bench-gate.awk" \
"$RUNNER_TEMP/compare.csv" | tee -a "$GITHUB_STEP_SUMMARY"
# Build the Starlight docs site. Runs only when docs/** changed (mirrors the
# Go jobs' code-gating); skipped otherwise, which the gate treats as a pass.
docs:
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
cache-dependency-path: "**/go.sum"
# Regenerate the API reference (gomarkdoc) and Mermaid diagram partials from
# the Go source before the site build, so the PR docs build validates the
# generated content. Runs from the repo root, not the docs/ default dir.
- name: Generate API reference + diagrams
working-directory: .
run: go run ./tools/docsgen
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
# Single required status check. Branch protection requires only `gate`, which
# aggregates the required jobs: it fails if any of them failed or was
# cancelled, and treats a skipped job (docs-only PRs, or PRs that touch no
# docs) as a pass — so such PRs stay mergeable without running every matrix.
# `bench` is advisory and is deliberately excluded so a benchmark regression
# never blocks a merge.
gate:
if: always()
needs: [changes, lint, tests, sink, source, vuln, coverage, docs]
runs-on: ubuntu-latest
steps:
- name: Fail if a required job did not pass
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "::error::A required job failed or was cancelled: ${{ join(needs.*.result, ', ') }}"
exit 1
- name: Required jobs passed
run: echo "Required jobs passed or were skipped (${{ join(needs.*.result, ', ') }})."
# As modules land (broker, store, sink, source), add their dirs to each job.