Repository navigation
1038 lines (1011 loc) · 55.8 KB
/
Copy pathci.yaml
File metadata and controls
1038 lines (1011 loc) · 55.8 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
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# CI path filter matrix (see docs/development/testing.md):
# - Any change under api/, internal/, charts/, cmd/, config/, hack/, test/, go.mod, or
# .github/workflows/ runs full CI. Mixed PRs (docs + code) run everything.
# - A documentation-only change (docs/**, mkdocs.yml, README.md, CHANGELOG.md,
# CONTRIBUTING.md, LICENSE, issue templates) skips the expensive jobs; other root Markdown
# files still run full CI. The Docs workflow runs lint-markdown + MkDocs build/deploy for
# its path-scoped inputs.
#
# CI-DOCSGATE-01: on `pull_request` that decision is made by the `changes` job below, NOT by a
# `paths-ignore` filter. protect-main requires the context `test` from this workflow, and a
# workflow skipped by `paths-ignore` does not report its contexts as skipped -- it does not
# report them at all. Every docs-only PR therefore sat at mergeStateStatus BLOCKED with a
# SUCCESS rollup and an empty failure list, and only `gh pr merge --admin` or the web UI's
# bypass prompt got through (PR #350, 2026-09-01; PR #321, 2026-08-23). A bypass required on
# every docs PR is the merge path, not an exception, and it trains the reflex that one day
# waves through a genuinely red required check.
#
# The `test` job at the bottom is now a REPORT: it runs `if: always()` so the required context
# always reaches a conclusion, and it fails unless `test-suite` actually succeeded whenever the
# PR touches anything outside the documentation path set. `paths-ignore` stays on `push`, where
# no required context is at stake. Locked by hack/test/ci_docs_gate_test.sh.
#
# The docs-only path is NOT free, and that is a deliberate trade. Where this workflow previously
# was not dispatched at all, a documentation-only PR now runs `changes`, `gitleaks`, `lint` and
# `tool-pins-selftest` (CFC-6 and DTP-7: the tool-pins and toolchain-pins self-tests, split out of
# lint for wall time) -- `lint` because two existing gates (dist_ci_wiring_test.sh,
# repo_root_links_wiring_test.sh) require it to be unconditional, and because it is where
# ci_docs_gate_test.sh runs, so leaving it ungated is the only thing that exercises the docs-only
# direction at all. What such a PR no longer pays for is the Go suite, the kind clusters,
# testcontainers, the fuzz matrix and the image builds -- and it no longer needs a ruleset bypass
# to merge.
name: CI
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "mkdocs.yml"
- "README.md"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "LICENSE"
- ".github/ISSUE_TEMPLATE/**"
pull_request:
permissions:
contents: read
# CWS-4: supersede a PR's own in-flight run on a new push, but never cancel a `push`-to-main run
# (each main SHA must keep its own complete run so release eligibility sees exact-SHA checks).
# The group is keyed by the PR number, falling back to the commit SHA on push -- never by
# `github.ref`, which would make two quick merges to main share one group and lose the middle
# commit's run. The `${{ github.workflow }}` prefix keeps `ci.yaml` and `e2e-smoke.yaml` in
# separate groups so neither cancels the other for the same PR. Locked by
# hack/test/ci_workflow_security_test.sh.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# CI-DOCSGATE-01: the per-PR verdict every expensive job below hangs off. `code=true` means
# the change touches something other than documentation. Fail-safe in one direction only:
# anything this job cannot classify (a non-PR event, a missing or unreachable SHA, an empty
# diff) yields `true`, so the worst case is a wasted CI run rather than a required context
# reported green without having run.
#
# That claim is only true because the diff is taken with `--no-renames`; see the step body.
# A verdict is a claim about the WHOLE diff, so the classifier must see deletions and both
# halves of a rename, not just the paths that still exist -- the gate covers each of those
# with a real `git rm` / `git mv` case.
#
# Keep the `filter` step body byte-identical with the one in .github/workflows/e2e-smoke.yaml
# -- hack/test/ci_docs_gate_test.sh compares them, because two copies that drift are two
# different answers to "is this docs-only?" and `test` and `kind-smoke` would then disagree
# about the same PR.
changes:
name: changes (ci)
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
# `fetch-depth: 0` is required, not defensive: BASE_SHA is the base branch tip at event
# time and can be an arbitrary ancestor, and a shallow clone has neither it nor a
# merge base, so `git diff` would fail. That fails CLOSED (code=true), which is safe but
# would silently run full CI on every PR and destroy the point of this job. `filter:
# blob:none` keeps that full commit history while skipping the file contents -- a
# `--name-only --no-renames` diff compares tree entries by object id and never reads a
# blob -- so this costs a blobless clone, not a full one.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
filter: blob:none
persist-credentials: false
- id: filter
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# The documentation path set. Mirrors the `paths-ignore` list kept on `push` above.
docs_only_path() {
case "$1" in
docs/* | mkdocs.yml | README.md | CHANGELOG.md | CONTRIBUTING.md | LICENSE | .github/ISSUE_TEMPLATE/*)
return 0
;;
*)
return 1
;;
esac
}
emit() {
echo "code=$1" >>"${GITHUB_OUTPUT}"
echo "changed-code=$1"
}
if [ "${EVENT_NAME}" != "pull_request" ]; then
emit true
exit 0
fi
if [ -z "${BASE_SHA}" ] || [ -z "${HEAD_SHA}" ]; then
emit true
exit 0
fi
# `--no-renames` is load-bearing, not tidiness. Rename detection is ON by default
# (diff.renames since git 2.9) and `git diff --name-only` then prints ONLY the
# DESTINATION of a detected rename. Without it, `git mv internal/x.go docs/x.go`
# reports the single path `docs/x.go`, this job answers "documentation-only", and
# both reporters report green for a change that deleted a Go file from the tree.
# With `--no-renames` the same diff reports both paths, and the loop below sees the
# source. Locked by hack/test/ci_docs_gate_test.sh (rename-into-docs case + mutant).
# `core.quotepath` is left at its default, so a path containing non-ASCII, a newline or
# a tab comes back C-quoted and matches no documentation pattern below -- that change
# gets full CI. Wasteful, but it is the safe direction, so it is left alone.
if ! changed="$(git diff --name-only --no-renames "${BASE_SHA}" "${HEAD_SHA}" 2>/dev/null)"; then
emit true
exit 0
fi
if [ -z "${changed}" ]; then
emit true
exit 0
fi
while IFS= read -r file; do
[ -n "${file}" ] || continue
if ! docs_only_path "${file}"; then
emit true
exit 0
fi
done <<<"${changed}"
emit false
gitleaks:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install gitleaks
env:
GITLEAKS_VERSION: "8.30.1"
run: |
bash hack/install-gitleaks.sh "${RUNNER_TEMP}"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Scan repository
run: gitleaks detect --source . --config .github/gitleaks.toml --verbose --redact --no-git
# CWS-1/CWS-2: static analysis of the workflows themselves. A pinned zizmor release is
# downloaded and SHA256-verified (hack/install-zizmor.sh), then run offline over .github/ at a
# pinned minimum severity with the committed .github/zizmor.yml. It is deliberately NOT gated
# on the changes verdict: it must run on every PR, including a docs-only one, so a workflow
# edit cannot slip past it. The job is a required check (CWS-5/CWS-6).
workflow-security:
name: workflow-security
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install zizmor
env:
ZIZMOR_VERSION: "1.30.1"
run: |
bash hack/install-zizmor.sh "${RUNNER_TEMP}"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Audit workflows (zizmor, offline)
# --offline forbids every online audit and remote-repository input, so the job needs no
# token and cannot be affected by network state. --min-severity=high is the pinned
# threshold: it catches template injection, app-token scope and cache poisoning. Lower
# severities are filtered by this threshold, not suppressed; the measured breakdown is
# recorded in openspec/changes/ci-workflow-hardening/tasks.md.
run: zizmor --offline --no-progress --min-severity=high --config .github/zizmor.yml .github/
# CWS-3: review the dependencies a PR adds. It runs on pull_request only -- it does not run on
# main, so it is deliberately NOT a release-eligibility check (see verify-eligibility.sh). The
# severity threshold stays at the action's default (`low`); the licence policy is an
# allow-list of the SPDX identifiers actually present in the module graph, and a dependency
# whose licence cannot be detected is reported without failing (the action's default).
dependency-review:
name: dependency-review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
# Derived from `go-licenses csv ./...` over the module graph (2026-10-05):
# 141 Apache-2.0, 60 MIT, 42 BSD-3-Clause, 9 BSD-2-Clause, 1 ISC. Unknown licences do
# not fail. MIT-CMU (added 2026-10-26) is the permissive HPND-style licence Pillow uses
# from 13.0.0; Pillow is a docs-site build tool only (docs/requirements-docs.txt).
allow-licenses: Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, MIT-CMU
# One exception: from v0.60.0 the scanner reports golang.org/x/net as "BSD-3-Clause AND
# LicenseRef-scancode-google-patent-license-golang" — the Go PATENTS file, an extra
# patent grant that every golang.org/x module ships and go-licenses reads as plain
# BSD-3-Clause. Scoped to this one package, not the licence, so a new dependency
# under that ref still has to be looked at.
# Second exception: cairosvg (LGPL-3.0-or-later) is a docs-site build tool only, pulled in
# by docs/requirements-docs.txt for mkdocs-material's image rendering. It never ships in
# the kollect binary or image, so the LGPL does not reach a release artefact. Scoped to
# the package for the same reason as above.
# Third exception: go-task/setup-task (reported GPL-3.0) is a CI-only action that
# downloads the Task binary on runners. It is never linked, vendored or shipped in the
# kollect binary, image or chart, and every workflow already uses it at the same pinned
# sha. Scoped to the package, not the licence, so another GPL action still fails.
# Added 2026-10-30 (operator): golang.org/x/sys and golang.org/x/tools now carry the same Go
# PATENTS ref as x/net above. certifi (MPL-2.0) is the CA bundle the docs-site build's
# requests stack uses; docs only, never shipped. SonarSource/sonarqube-scan-action
# (reported LGPL-3.0) is a CI-only action like setup-task. All scoped to the package.
allow-dependencies-licenses: pkg:golang/golang.org/x/net, pkg:golang/golang.org/x/sys, pkg:golang/golang.org/x/tools, pkg:pypi/cairosvg, pkg:pypi/certifi, pkg:githubactions/go-task/setup-task, pkg:githubactions/SonarSource/sonarqube-scan-action
verify:
name: verify
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Verify generated artifacts
run: task verify
env:
CI: "true"
audit-rbac:
name: audit-rbac
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: RBAC security audit (Polaris + kubeaudit)
run: bash hack/audit-rbac.sh
env:
POLARIS_VERSION: "9.6.0"
KUBEAUDIT_VERSION: "0.22.2"
vulncheck:
name: vulncheck
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: govulncheck
run: task vulncheck
# lint job runs `task lint` → golangci-lint v2 (depguard, gomodguard, …) + go-arch-lint
# (internal/ package graph; see .go-arch-lint.yml). No separate arch-lint step needed.
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# CWS-6: the docs-side guards wired below verify anchors that reach back into main's
# history (security_architecture_docs_test.sh resolves a pinned security-review
# commit), so the lint job must have full history -- a depth-1 checkout makes every
# history-dependent guard fail-closed here while passing under docs:verify's
# fetch-depth: 0 checkout.
fetch-depth: 0
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Check formatting
run: task format:check
- name: Install ShellCheck
env:
SHELLCHECK_VERSION: "0.11.0"
run: |
bash hack/install-shellcheck.sh "${RUNNER_TEMP}"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Lint shell scripts
run: task lint:shell
- name: Verify scanner download checksums (unit + negative)
run: |
bash hack/test/verify-sha256_test.sh
bash hack/test/install-checksum-negative_test.sh
- name: Verify demo-up/demo-down task aliases
run: bash hack/test/demo_task_aliases_test.sh
- name: Verify default samples are credential-free (DEMO-01)
run: bash hack/test/demo_01_default_samples_test.sh
- name: Verify demo sample Kustomizations render (DOC-04)
run: bash hack/test/demo_04_samples_kustomize_test.sh
# CI-FETCHLIB-01: the shared fetch helper (hack/lib/fetch.sh) that every hack/install-*.sh
# and the kind-e2e-setup action now download through. It gets its OWN step rather than being
# invoked from the CI-HELMDL-01 gate below, which is how it first reached CI: that route was
# circular (delete the nested call and this gate silently stops running) and it filed every
# fetch-lib failure under a step named "install-helm hardening". Not matched by the dist_* or
# sonar_ko_* globs, hence the explicit run. It goes BEFORE the install-helm gate because
# install-helm's hardening is now a property of the helper, so "the helper is broken" is the
# more fundamental diagnosis and the one an operator should read first.
- name: Verify shared fetch helper hardening (CI-FETCHLIB-01)
run: bash hack/test/ci_fetch_lib_hardening_test.sh
# CI-HELMDL-01: hack/install-helm.sh is what the kind-e2e-setup composite action runs to
# get helm, and a single transient `curl: (7)` to get.helm.sh reddened the REQUIRED
# kind-smoke check on PR #351. This gate locks in the retry/protocol-pinning hardening
# without letting it erode the SHA256 verification. It runs BEFORE the Install Helm step
# below, so a broken installer is reported as a gate failure rather than as a mystery
# download error. Not matched by the dist_* or sonar_ko_* globs, hence the explicit run.
- name: Verify install-helm download hardening (CI-HELMDL-01)
run: bash hack/test/ci_install_helm_hardening_test.sh
- name: Install Helm
env:
HELM_VERSION: v3.22.1
run: bash hack/install-helm.sh
- name: Ensure yq is available
run: |
# ubuntu-latest images generally ship yq, but don't assume it --
# some sonar_ko_* meta-tests (chart-rendering ones) need it.
command -v yq >/dev/null 2>&1 || sudo snap install yq
yq --version
# DIST-OH-02: dist_olm_bundle_test.sh hard-requires operator-sdk for the OperatorHub
# validator gate (operatorhubv2 / capabilities / categories). Installing it here is what
# makes that gate actually run in CI instead of skipping -- a skipped gate would read as
# coverage that does not exist. The gate resolves ./bin/operator-sdk first, so install there.
- name: Install operator-sdk (OLM bundle validators)
env:
OPERATOR_SDK_VERSION: v1.42.3
run: bash hack/install-operator-sdk.sh ./bin
- name: Verify nightly race job contract (HY-07)
run: bash hack/test/hyg_07_nightly_race_test.sh
- name: Verify nightly race job contract mutants (HY-07 --self-test)
run: bash hack/test/hyg_07_nightly_race_test.sh --self-test
- name: Verify manager RBAC grants core Events (not events.k8s.io)
run: bash hack/test/core_events_rbac_test.sh
- name: Verify manager RBAC can read KollectClusterScope
run: bash hack/test/cluster_scope_rbac_test.sh
- name: Verify changelog-sync release guard (fbb5196a3 regression lock)
run: bash hack/test/changelog_sync_release_guard_test.sh
# LAB-DEKIND: this suite is the enforcement mechanism for the lab substrate allowlist
# (default-deny on kube contexts) and the non-Kind image-delivery policy. It was only
# reachable via hack/docs/verify.sh in the path-filtered docs workflow, whose filter
# does not include hack/lab/** — so a change to the harness itself never triggered it.
# Run it here, unconditionally, where the guard actually protects something.
- name: Lab harness offline meta-tests (substrate allowlist, image delivery, pprof)
run: bash hack/test/lab_harness_meta_suite.sh
# CI-CONTRIBFILTER-01: same shape as LAB-DEKIND above. This gate resolves relative links in
# the repo-ROOT Markdown (README.md, CONTRIBUTING.md), which `mkdocs build --strict` cannot
# see -- it reads only `docs_dir: docs`. It was reachable only via hack/docs/verify.sh in the
# path-filtered Docs workflow, whose filter listed neither CONTRIBUTING.md nor this gate
# script, so a PR weakening or deleting the gate triggered nothing that would notice.
# hack/** is not in this workflow's paths-ignore, so running it here closes that half.
# The CONTRIBUTING.md half cannot be closed here -- CONTRIBUTING.md is in paths-ignore
# above -- and is closed by the docs.yaml filter entry instead.
- name: Verify repo-root Markdown links resolve (CI-CONTRIBFILTER-01)
run: bash hack/test/repo_root_links_test.sh
- name: Verify the repo-root link gate stays wired into a job that runs
run: bash hack/test/repo_root_links_wiring_test.sh
# Mirrors the "Ensure yq is available" step above, for the same reason: the lint job
# otherwise installs only Go tooling and shellcheck. hack/gen-glossary.py parses the CRD
# YAML with PyYAML, and the gate below runs it, so a missing module would red the job for
# a reason that has nothing to do with the glossary.
- name: Ensure PyYAML is available
run: |
if ! python3 -c "import yaml" 2>/dev/null; then
sudo apt-get update
sudo apt-get install -y python3-yaml
fi
python3 -c "import yaml; print('PyYAML', yaml.__version__)"
# GLOSSARY-MARKER-01, same defect class as LAB-DEKIND above. This gate regenerates
# docs/GLOSSARY.md and diffs it, so it is the only thing standing between a contributor
# running the documented regeneration command and the curated rows inside the AUTO-CRD
# markers being silently overwritten. Composing it into hack/docs/verify.sh alone left it
# unreachable for its own motivating change class: the Docs workflow path filter lists
# sibling hack/test scripts individually, so a commit touching only hack/gen-glossary.py
# -- which is exactly what 2eef5a453 was -- triggered no run at all, and neither did an
# edit to the gate itself. Run it here, unconditionally, next to the sonar_ko_* glob that
# already carries the other hack/gen-glossary.py check.
- name: Glossary generator drift meta-tests (AUTO-CRD block, curated rows, emitted links)
run: bash hack/test/glossary_drift_test.sh
# DOC-MAP-01: same shape as LAB-DEKIND and CI-CONTRIBFILTER-01 above. This gate asserts the
# label-to-page contract of the Documentation map in docs/index.md -- that no label names a
# deleted page, no two labels claim one page, and every label matches its destination's nav
# label or H1. `mkdocs build --strict` structurally cannot catch this: the stale links all
# RESOLVED, they just misinformed, and a link checker only sees resolution.
# The gate would otherwise be reachable only via hack/docs/verify.sh in the path-filtered
# Docs workflow, so a PR weakening or deleting the gate SCRIPT would trigger nothing that
# notices. hack/** is not in this workflow's paths-ignore, so running it here closes that
# half; docs.yaml's docs/** filter covers a change to the map itself.
- name: Verify the docs map labels the pages it points at (DOC-MAP-01)
run: bash hack/test/docs_map_contract_test.sh
# DOC-ADRREFS-01: every ADR-\d{4} token in tracked files must resolve to a committed ADR.
# ADRs get renumbered and deleted; code comments, CRD descriptions (user-visible via
# `kubectl explain`), the chart values schema, and docs keep citing the retired numbers
# for years. The gate also runs under hack/docs/verify.sh in the Docs workflow, but that
# check is not a required context, so CWS-6 requires this required-job row -- the same
# wiring convention as the other docs-side guards above. Run it here, unconditionally:
# hack/** is outside this workflow's paths-ignore, so every change class that can break
# the contract triggers this job.
- name: Verify every ADR citation resolves to a committed ADR (DOC-ADRREFS-01)
run: bash hack/test/docs_adr_refs_test.sh
# CI-DOCSGATE-01: asserts that all four required contexts (preflight, test, kind-smoke,
# Analyze (Go)) report a conclusion on EVERY PR, and that the no-op reporting path cannot
# stand in for the real job when code changed. It runs here because `lint` is the one job
# in this workflow that is deliberately NOT gated on the changes verdict, so this gate
# also runs on the docs-only PRs whose direction it protects -- which no PR touching
# .github/** can ever exercise. Not matched by the dist_* or sonar_ko_* globs, hence the
# explicit step; it needs the yq installed above.
- name: Verify every required context reports on a docs-only PR (CI-DOCSGATE-01)
run: bash hack/test/ci_docs_gate_test.sh
# CI-WFS-01 (openspec change ci-workflow-hardening, CWS-1..CWS-7): the meta-test for the
# workflow-security gate itself. It reads ci.yaml, e2e-smoke.yaml, .github/zizmor.yml,
# hack/install-zizmor.sh and hack/release/verify-eligibility.sh, and asserts the exact
# invocation shape, the version pin, the suppression policy, the concurrency contract,
# the push trigger and the required-checks wiring. It runs here for the same reason
# CI-DOCSGATE-01 does: lint is not gated on the changes verdict, so it runs on docs-only
# PRs too, where a workflow edit that weakens the gate would otherwise never trigger it.
# Plain and --self-test are separate steps so a red names its direction: the first reds
# when the gate's wiring drifts, the second reds when the gate can no longer fail (CWS-7
# mutants). hack/test/ci_workflow_security_test.sh enforces this wiring in both
# directions (CWS-6: a gate CI does not run guards nothing).
- name: Verify the workflow-security gate wiring (CWS-1..CWS-6)
run: bash hack/test/ci_workflow_security_test.sh
- name: Verify the workflow-security gate can still fail (CWS-7)
run: bash hack/test/ci_workflow_security_test.sh --self-test
# TCE-1..TCE-4 (openspec change task-check-entrypoint): the guard pins task check's
# wiring -- hack/check.sh runs every required gate that can run locally, sweeps the
# hack/test guards in every mode via the glob, and prints the exclusions with reasons.
# Plain and --self-test as separate steps (TCE-4), same convention as the CWS-7 gate
# above: the first reds when the wiring drifts, the second when the gate can no longer
# fail. hack/test/task_check_test.sh enforces this wiring in both directions.
- name: Verify the task check gate wiring (TCE-1..TCE-4)
run: bash hack/test/task_check_test.sh
- name: Verify the task check gate can still fail (TCE-4)
run: bash hack/test/task_check_test.sh --self-test
# CWS-6: this guard asserted its scenario's wiring from the nightly and extended e2e
# suites, neither of which is a required check -- so its verdict never gated a merge and
# a PR could silently break the finalizer-cleanup e2e scenario it pins. The lint job is
# the required home for the same pattern (HY-07 below): the nightly runs keep it covered
# on the cluster, this step gives every PR a required verdict.
- name: Verify finalizer-cleanup e2e assert + workflow wiring
run: bash hack/test/finalizer_cleanup_e2e_test.sh
# CWS-6: these three guards pin e2e scenario wiring from jobs that are NOT required
# checks -- the extended/nightly e2e suites and a smoke-only job -- so their verdicts
# never gated a merge, and a PR could silently break the scenario contracts they pin.
# All three are pure static contract tests (no kind cluster), so they take their
# required home here, the same pattern as HY-07: the suites keep running them for
# cluster coverage, this step gives every PR a required verdict.
- name: Verify multitenant e2e repro hardening contract
run: bash hack/test/e2e_mt_repro_harden_test.sh
- name: Verify webhook-existing-cluster e2e contract
run: bash hack/test/e2e_webhook_existing_cluster_test.sh
- name: Verify the hero demo smoke contract (DEMO-03)
run: bash hack/test/demo_03_hero_smoke_test.sh
# CWS-6: the docs-side guards' only CI invocation is inside hack/docs/verify.sh, which
# the Docs workflow reaches through `task docs:verify` in a NON-required job, so their
# verdicts never gated a merge. They are static contract tests (no mkdocs build, no
# chrome), so they take their required home here; docs:verify keeps running them for the
# docs-pipeline side. One grouped step with a named group per script, because the list
# is long and stable -- a red names the failing guard in its group.
- name: Docs-side contract meta-tests (required side of docs:verify, CWS-6)
run: |
shopt -s nullglob
scripts=(
hack/test/docs_adr_kollectsink_retcon_test.sh
hack/test/docs_coverage_floor_drift_test.sh
hack/test/docs_lab_doc_01_runbook_test.sh
hack/test/docs_lab_doc_03_scale_claims_test.sh
hack/test/docs_lab_doc_04_fidelity_test.sh
hack/test/docs_lab_doc_05_scenario_matrix_test.sh
hack/test/docs_lab_evidence_contract_test.sh
hack/test/docs_launch_truth_test.sh
hack/test/docs_map_wiring_test.sh
hack/test/docs_pages_concurrency_test.sh
hack/test/docs_removed_api_fields_test.sh
hack/test/hyg_ui_gitignore_test.sh
hack/test/lab_adr_0707_indexed_test.sh
hack/test/security_architecture_docs_test.sh
hack/test/ui_removal_reference_test.sh
)
if [ "${#scripts[@]}" -eq 0 ]; then
echo "no docs-side guard scripts wired"
exit 1
fi
for script in "${scripts[@]}"; do
echo "::group::${script}"
bash "${script}"
echo "::endgroup::"
done
- name: Run Sonar SECURITY remediation meta-tests
run: |
shopt -s nullglob
scripts=(hack/test/sonar_ko_*_test.sh)
if [ "${#scripts[@]}" -eq 0 ]; then
echo "no hack/test/sonar_ko_*_test.sh scripts yet"
fi
for script in "${scripts[@]}"; do
echo "::group::${script}"
bash "${script}"
echo "::endgroup::"
done
- name: Hub distribution meta-tests
run: |
shopt -s nullglob
scripts=(hack/test/dist_*_test.sh)
if [ "${#scripts[@]}" -eq 0 ]; then
echo "no hack/test/dist_*_test.sh scripts yet"
exit 1
fi
for script in "${scripts[@]}"; do
echo "::group::${script}"
bash "${script}"
echo "::endgroup::"
done
# CFC-1..CFC-6 (openspec change cross-file-consistency-gates): the cross-file consistency
# gates -- the Go toolchain agrees across go.mod, the Dockerfiles and the devcontainer
# (CFC-1), every pinned tool resolves and its analysis tools run under go.mod's Go (CFC-2),
# the chart's RBAC covers config/rbac/role.yaml in both install modes (CFC-3), registered
# metrics match the docs and rules (CFC-4), and the go-patch-lag sensor keeps its verdicts
# and its workflow shape (CFC-5). Plain and --self-test are separate steps (CFC-6): the
# first reds when the tree drifts, the second when a gate can no longer fail. They run here,
# unconditionally, because lint is a required context that also runs on docs-only PRs.
# Tools: jq (runner image), yq, helm and PyYAML installed above; Go from go.mod through
# go-cache (the tool-pins gate forces GOTOOLCHAIN=go<go.mod version> itself).
- name: Verify the Go toolchain agrees across go.mod, images and devcontainer (CFC-1)
run: bash hack/test/consistency_go_toolchain_test.sh
- name: Verify the Go toolchain gate can still fail (CFC-6)
run: bash hack/test/consistency_go_toolchain_test.sh --self-test
- name: Verify the chart RBAC covers role.yaml in both install modes (CFC-3)
run: bash hack/test/consistency_rbac_test.sh
- name: Verify the chart RBAC gate can still fail (CFC-6)
run: bash hack/test/consistency_rbac_test.sh --self-test
- name: Verify registered metrics match the docs and alert rules (CFC-4)
run: bash hack/test/consistency_metrics_test.sh
- name: Verify the metrics gate can still fail (CFC-6)
run: bash hack/test/consistency_metrics_test.sh --self-test
# DTP-1/DTP-2/DTP-3/DTP-5/DTP-6/DTP-7 (openspec change developer-toolchain-pins): Task (every
# setup-task version: input and mise.toml), gitleaks, git-cliff and shellcheck agree across
# their pin sites and meet their floors, shellcheck has digests for its pin, task vulncheck
# runs the Renovate-annotated GOVULNCHECK_VERSION, and the installer-checksum exemptions
# only shrink. DEV-MISE-01: mise.toml pins task, its
# node/python equal every setup-node/setup-python pin under .github/ and Go stays read from
# go.mod. Renovate's github-actions manager extracts those node/python inputs too, but a
# renovate.json rule disables them (their bump would leave mise.toml behind and red this
# check), so they are bumped by hand and this comparison keeps mise.toml in step; the
# guard's Renovate model checks the rule. DTP-5: a mise.toml or Taskfile.yml comment states
# a version only directly above (or on) the pin it names, at its value, and never counts
# pin sites.
# Its --self-test (DTP-7) runs in the tool-pins-selftest job below.
- name: Verify Task, gitleaks, git-cliff, shellcheck, govulncheck and mise.toml pins agree (DTP-1, DTP-2, DTP-3, DTP-5, DTP-6, DEV-MISE-01)
run: bash hack/test/consistency_toolchain_pins_test.sh
- name: Verify the go-patch-lag sensor verdicts and workflow shape (CFC-5)
run: bash hack/test/go_patch_lag_test.sh
- name: Verify the go-patch-lag test can still fail (CFC-6)
run: bash hack/test/go_patch_lag_test.sh --self-test
# DUA-1, DUA-2, DUA-3, DUA-5, DUA-7 and DUA-8 (openspec change dependency-update-automation):
# the self-hosted Renovate policy -- its token, fail-closed check, permissions, schedule,
# concurrency, pinned image, environment, the config validator step in this job and no
# merging workflow. Plain and --self-test as separate steps; the self-test mutates a
# known-good fixture, not the real workflow. CWS-6/CFC-6 checks each mode runs in a
# required job. Needs yq (installed above).
- name: Verify the Renovate workflow policy (DUA-1, DUA-2, DUA-3, DUA-5, DUA-7, DUA-8)
run: bash hack/test/renovate_policy_test.sh
- name: Verify the Renovate policy test can still fail (DUA-1, DUA-2, DUA-3, DUA-5, DUA-7, DUA-8)
run: bash hack/test/renovate_policy_test.sh --self-test
# DUA-5: renovate-config-validator --strict from the digest-pinned renovate-image of
# renovate.yaml, renovate.json as repo config and .github/renovate-config.json as global
# config, no network. Unconditional: the guard above (T8) refuses an if: or
# continue-on-error: here. Needs yq (installed above) and the runner's docker.
- name: Validate the Renovate config (DUA-5)
run: bash hack/ci/renovate-config-validate.sh
# CFR-1 to CFR-7 (openspec change ci-failure-reporting): the CI failure issue body, the
# reporter's decisions against a fake gh, and the ci-failure-report.yaml wiring. Plain and
# --self-test as separate steps; the test checks ci.yaml runs it in both modes, and CWS-6
# checks each mode runs in a required job.
- name: Verify the CI failure reporter, its issue body and its workflow (CFR-1 to CFR-7)
run: bash hack/test/ci_failure_report_test.sh
- name: Verify the CI failure reporter test can still fail (CFR-7)
run: bash hack/test/ci_failure_report_test.sh --self-test
# OFS-1 to OFS-5 (openspec change operatorhub-fork-sync): hack/operatorhub-pr.sh against fake
# gh, git, curl and make -- a fork behind upstream is synced before any push, a diverged or
# ahead fork fails closed with no write, and DRY_RUN makes no write. Plain and --self-test as
# separate steps; CWS-6/CFC-6 checks each mode runs in a required job. Needs jq (runner image).
- name: Verify the OperatorHub submission syncs its forks safely (OFS-1 to OFS-4)
run: bash hack/test/operatorhub_fork_sync_test.sh
- name: Verify the OperatorHub fork sync test can still fail (OFS-5)
run: bash hack/test/operatorhub_fork_sync_test.sh --self-test
# BMC-1 to BMC-5 (openspec change breaking-marker-check): hack/check-breaking-marker.sh on
# throwaway repositories from fixtures, and the shape of breaking-marker.yaml. Plain and
# --self-test as separate steps; CWS-6/CFC-6 checks each mode runs in a required job.
- name: Verify the breaking-marker check and its workflow (BMC-1 to BMC-4)
run: bash hack/test/breaking_marker_test.sh
- name: Verify the breaking-marker test can still fail (BMC-5)
run: bash hack/test/breaking_marker_test.sh --self-test
# TDS-1 (openspec change test-depth-signals): the fuzz job's runner against a fake go
# replaying captured CI output, and its wiring. Plain and --self-test as separate steps;
# CWS-6/CFC-6 checks each mode runs in a required job. Needs git and yq (installed above).
- name: Verify the fuzz runner retries only the deadline flake (TDS-1)
run: bash hack/test/fuzz_retry_test.sh
- name: Verify the fuzz runner test can still fail (TDS-1)
run: bash hack/test/fuzz_retry_test.sh --self-test
# TDS-2 (openspec change test-depth-signals): the nightly shuffle script against a fake make
# and go (an order-dependent fixture through the real go), and test-shuffle.yaml's wiring.
# Plain and --self-test as separate steps; CWS-6/CFC-6 checks each mode runs in a required
# job. Needs go and yq (installed above).
- name: Verify the nightly shuffle runs every unit package under one seed (TDS-2)
run: bash hack/test/test_shuffle_test.sh
- name: Verify the nightly shuffle test can still fail (TDS-2)
run: bash hack/test/test_shuffle_test.sh --self-test
# TDS-3 (openspec change test-depth-signals): every task, kubectl apply -k path and kubectl get
# name in the README's Quick start sh fences exists in the tree. Static (bash, awk), no
# cluster. Plain and --self-test as separate steps; CWS-6/CFC-6 checks each mode runs in a
# required job.
- name: Verify the README quick start names only existing tasks and resources (TDS-3)
run: bash hack/test/readme_quickstart_test.sh
- name: Verify the README quick-start test can still fail (TDS-3)
run: bash hack/test/readme_quickstart_test.sh --self-test
# MUT-1 to MUT-4 (openspec change mutation-testing-signal): the nightly mutation runner and
# formatter against a fake gremlins, and mutation.yaml's wiring, install chain and threshold
# sources. Static: this job never installs or runs gremlins. Plain and --self-test as
# separate steps; CWS-6/CFC-6 checks each mode runs in a required job. Needs git, jq and yq.
- name: Verify the nightly mutation measurement reports and cannot gate (MUT-1 to MUT-4)
run: bash hack/test/mutation_signal_test.sh
- name: Verify the mutation signal test can still fail (MUT-1 to MUT-4)
run: bash hack/test/mutation_signal_test.sh --self-test
# Evidence under openspec/changes/**/evidence/ (archive included) is Markdown only: a probe
# script committed there is analysed by SonarCloud as uncovered new code although CI never
# runs it (decision 2026-10-16: inline probes in the evidence .md instead of widening
# sonar.exclusions). Plain and --self-test as separate steps, like the CFC gates above.
- name: Verify openspec evidence is Markdown only
run: bash hack/test/openspec_evidence_markdown_only_test.sh
- name: Verify the openspec evidence guard can still fail
run: bash hack/test/openspec_evidence_markdown_only_test.sh --self-test
# The docs lock's uv header writes every option that takes a value as --opt=value:
# Renovate's pip-compile manager rejects a space-separated value and then extracts no
# dependency, with only a WARN line in a green run. Plain and --self-test as separate steps.
- name: Verify the docs lock header is Renovate-parseable
run: bash hack/test/docs_lock_header_test.sh
- name: Verify the docs lock header guard can still fail
run: bash hack/test/docs_lock_header_test.sh --self-test
# PAC-1 to PAC-6 (openspec change public-agent-contract): the tracked AGENTS.md names only
# existing tasks, lists exactly the gates hack/check.sh runs, keeps its checklist paths and
# symbols real, and carries no scrub-pattern match, home or harness path, internal host or
# private header; .gitignore and the gitleaks allowlist keep the agent files public and
# scanned. The guard needs rg (hack/scrub.sh semantics), which the runner image does not
# ship: install the runner image archive's ripgrep when it is absent (no version pin: a
# pinned Debian revision exists in one Ubuntu release only, and ubuntu-latest moves on).
# Plain and --self-test as separate steps; CWS-6/CFC-6 checks each mode runs in a required
# job.
- name: Ensure ripgrep is available
run: |
if ! command -v rg >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends ripgrep
fi
rg --version
- name: Verify the public agent contract (PAC-1 to PAC-6)
run: bash hack/test/agent_contract_test.sh
- name: Verify the agent contract guard can still fail (PAC-5)
run: bash hack/test/agent_contract_test.sh --self-test
# Network: the module proxy, golangci-lint's custom build and the vuln DB (about 30 s warm).
- name: Verify every pinned tool resolves and runs under go.mod's Go (CFC-2)
run: bash hack/test/consistency_tool_pins_test.sh
- name: Lint
run: task lint
# CFC-6 for CFC-2 (openspec change cross-file-consistency-gates): the tool-pins gate's
# --self-test builds each pinned analysis tool at its own and at a too-old version (about
# 7 minutes warm), so it runs in its own job, in parallel with lint, instead of doubling
# lint's wall time (operator decision 2026-10-16). Like lint it is NOT gated on the changes
# verdict: it runs on every PR, docs-only included, and is a required context
# (hack/release/verify-eligibility.sh). hack/test/ci_workflow_security_test.sh (CFC-6 rule)
# requires the self-test in this job and hack/test/ci_docs_gate_test.sh keeps the job
# unconditional. The toolchain-pins self-test (DTP-7, openspec change developer-toolchain-pins)
# runs here too, for the same reason: about 8 minutes, half of it the all-pins bump control's
# nested self-test. It also removes the toolchain-pins guard's steps from a copy and expects
# the CWS-6/CFC-6 meta-test to red, so it needs yq. The timeout bounds a stalled module or
# vuln-DB download; local runs of the two took 15 to 25 minutes.
tool-pins-selftest:
name: tool-pins-selftest
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- name: Verify the tool-pins gate can still fail (CFC-6)
run: bash hack/test/consistency_tool_pins_test.sh --self-test
- name: Ensure yq is available
run: |
# ubuntu-latest images generally ship yq, but don't assume it (same step as lint's).
command -v yq >/dev/null 2>&1 || sudo snap install yq
yq --version
- name: Verify the toolchain-pins gate can still fail (DTP-7)
run: bash hack/test/consistency_toolchain_pins_test.sh --self-test
# CI-DOCSGATE-01: the REAL suite. It is deliberately NOT named `test` -- the required context
# of that name is produced by the `test` job at the end of this file, which reports on every
# PR. A job that legitimately skips must never carry a required context name; that is exactly
# the starvation this lane removed.
test-suite:
name: test-suite
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Codecov OIDC upload; GitHub App still required for reliable PR comments
env:
COVERAGE_MIN: "90"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
with:
envtest: true
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Unit tests and coverage floor
run: task coverage
env:
CI: "true"
- name: Coverage summary
run: go tool cover -func=coverage.out | tee coverage-summary.txt
- name: Job summary
run: |
echo "### Coverage" >> "${GITHUB_STEP_SUMMARY}"
echo '```' >> "${GITHUB_STEP_SUMMARY}"
tail -20 coverage-summary.txt >> "${GITHUB_STEP_SUMMARY}"
echo '```' >> "${GITHUB_STEP_SUMMARY}"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage
path: |
coverage.out
coverage-summary.txt
if-no-files-found: error
- name: Upload coverage to Codecov
if: success()
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: coverage.out
fail_ci_if_error: false
use_oidc: true
build:
name: build
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Build manager
run: task build
test-integration:
name: test-integration
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Verify Forgejo pre-pull safeguards
run: bash test/ci/forgejo_prepull_test.sh
- name: Pre-pull Forgejo image
run: bash hack/ci/pull-image-with-retry.sh "codeberg.org/forgejo/forgejo:11"
- name: Integration tests (testcontainers)
run: task test-integration
env:
CI: "true"
# Docker is promised on this runner: fail, never skip, when a container cannot start.
KOLLECT_REQUIRE_DOCKER: "true"
# Ryuk only reaps containers a crashed test leaks, and this runner VM is discarded after
# the job. Waiting for it timed out Forgejo starts four times (PRs #394, #395, #405).
TESTCONTAINERS_RYUK_DISABLED: "true"
helm:
name: helm
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Install Helm
env:
HELM_VERSION: v3.22.1
run: bash hack/install-helm.sh
- name: Install helm-unittest plugin
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v0.5.1
- name: Helm lint and unittest
run: task helm-test
perf-report:
name: perf-report
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
continue-on-error: true
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
with:
envtest: true
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Performance snapshot (optional gate)
run: task perf-report
env:
CI: "true"
- name: Upload perf snapshot
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: perf-snapshot
path: artifacts/perf-snapshot.md
retention-days: 14
if-no-files-found: warn
- name: Job summary
if: always()
run: |
{
echo "### perf-report (optional)"
echo "Runs \`task perf-report\` (bench excerpt + unit pass). Does not block merge."
if [[ -f artifacts/perf-snapshot.md ]]; then
echo ""
echo "Snapshot excerpt:"
echo '```'
sed -n '1,24p' artifacts/perf-snapshot.md
echo '```'
fi
echo "Download \`perf-snapshot\` artifact for full report. Regenerate locally before scale work — see [PERFORMANCE.md](https://github.com/platformrelay/kollect/blob/main/docs/PERFORMANCE.md)."
} >> "${GITHUB_STEP_SUMMARY}"
fuzz:
name: fuzz
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- target: FuzzContentHash
pkg: ./internal/aggregate/...
- target: FuzzExtractJSONPath
pkg: ./internal/collect/...
- target: FuzzExtractCEL
pkg: ./internal/collect/...
- target: FuzzValidateAttributePath
pkg: ./internal/collect/...
- target: FuzzOwnedPrune
pkg: ./internal/sink/git # exact package: -fuzz rejects a pattern matching several
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
with:
cache-suffix: fuzz-${{ matrix.target }}
# Its own step, so a cold module cache's `go: downloading` lines never reach the runner's
# exact flake match below (hack/test/fuzz_retry_test.sh F16 pins it here).
- name: Download Go modules
run: go mod download
- name: Native Go fuzz (30s)
env:
FUZZ_TARGET: ${{ matrix.target }}
FUZZ_PKG: ${{ matrix.pkg }}
# TDS-1 (openspec change test-depth-signals): one retry, and only for the exact
# "context deadline exceeded" flake at the fuzztime boundary with no crash corpus;
# a deny marker, unknown failure text or a leg that fuzzed nothing fails at once.
# Locked by hack/test/fuzz_retry_test.sh (F11 pins this step).
run: hack/ci/fuzz-retry.sh "${FUZZ_TARGET}" "${FUZZ_PKG}"
# SonarCloud: GitHub secret SONAR_TOKEN (SonarCloud analysis token). Local dev uses
# SONARCLOUD_TOKEN in .envrc — same value, different name (see tooling-setup.md).
sonarcloud:
name: sonarcloud
runs-on: ubuntu-latest
# The `coverage` artifact comes from test-suite, so this follows the real job, not the
# `test` report. A docs-only PR skips test-suite and therefore skips this too.
needs: [test-suite]
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: coverage
- name: Gate on SONAR_TOKEN
id: sonar
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
if [ -z "${SONAR_TOKEN}" ]; then
echo "has_token=false" >> "${GITHUB_OUTPUT}"
echo "::warning::SONAR_TOKEN secret is unset; SonarCloud scan skipped (configure org token — see docs/development/tooling-setup.md)."
else
echo "has_token=true" >> "${GITHUB_OUTPUT}"
fi
- uses: SonarSource/sonarqube-scan-action@4d1dc0c91ee400dcf8a6dddf2f705ac36542e304 # v8.4.0
if: steps.sonar.outputs.has_token == 'true'
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
docker-build:
name: docker-build
needs: [changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/go-cache
- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: 3.54.0
- name: Build controller image
run: task docker:build
# kollect-pipeline image (ADR-0801 P-006). release.yaml only builds it on v* tags, so this
# PR-time build+run smoke is the first place a broken Dockerfile.pipeline or entrypoint is
# caught. Single-arch (native runner) to stay fast; the release job builds multi-arch.
- name: Build kollect-pipeline image
run: task docker:build:pipeline
- name: Smoke test kollect-pipeline image (--help exits 0)
run: docker run --rm kollect-pipeline:dev --help
- name: Assert kollect-pipeline image runs as nonroot (UID 65532)
run: |
user="$(docker inspect --format '{{.Config.User}}' kollect-pipeline:dev)"
echo "image user: ${user}"
if [ "${user}" != "65532:65532" ]; then
echo "::error::kollect-pipeline image must run as 65532:65532, got '${user}'" >&2
exit 1
fi
# CI-DOCSGATE-01: the required context `test` (protect-main lists it by exact string).
#
# This job is a REPORT and nothing else -- it exists so that the context reaches a conclusion