Repository navigation
Expand file tree
/
Copy pathTaskfile-build.yml
More file actions
779 lines (739 loc) · 34.4 KB
/
Copy pathTaskfile-build.yml
File metadata and controls
779 lines (739 loc) · 34.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
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
# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: '3'
vars:
IMG: '{{.IMG | default "controller:latest"}}'
CONTAINER_TOOL: '{{.CONTAINER_TOOL | default "docker"}}'
HELM: '{{.HELM | default "helm"}}'
CONTROLLER_GEN: '{{.CONTROLLER_GEN | default "controller-gen"}}'
ENVTEST: '{{.ENVTEST | default "setup-envtest"}}'
GOLANGCI_LINT: '{{.GOLANGCI_LINT | default "golangci-lint"}}'
SCC: '{{.SCC | default "scc"}}'
INSTALL_NAME: '{{.INSTALL_NAME | default "gitops-reverser"}}'
NAMESPACE: '{{.NAMESPACE | default "gitops-reverser"}}'
DEFAULT_AUDIT_REDIS_ADDR: '{{.DEFAULT_AUDIT_REDIS_ADDR | default "valkey:6379"}}'
ENVTEST_K8S_VERSION:
sh: |
go list -m -f '{{"{{"}} .Version {{"}}"}}' k8s.io/api \
| awk -F'[v.]' '{printf "1.%d", $3}'
ENVTEST_STAMP: '.stamps/envtest-{{.ENVTEST_K8S_VERSION}}.ready'
# Fuzzing (dynamic analysis) targets, one "package:FuzzTarget" pair per line.
# `go test -fuzz` fuzzes exactly one target per invocation, so the fuzz tasks
# loop over these. See docs/finished/dynamic-analysis-fuzzing-plan.md.
FUZZ_TARGETS: >-
./internal/git/manifestedit:FuzzManifestEdit
./internal/webhook:FuzzDecodeEventList
FUZZ_SMOKE_TIME: '{{.FUZZ_SMOKE_TIME | default "15s"}}'
FUZZ_TIME: '{{.FUZZ_TIME | default "2m"}}'
# Which markdown files the docs linters see: `gated` (the files .docs-lint-scope
# lists) or `all` (every tracked file). hack/docs-files.sh resolves it and is the
# only place that builds the list.
#
# The two linters default differently because their backlogs are two orders of
# magnitude apart. Structure (markdownlint) is clean tree-wide, so it gates `all`.
# Prose (Vale) still has ~1,100 errors across the tree, so it stays `gated` and the
# .docs-lint-scope list grows as files are cleaned. `DOCS_SCOPE` overrides both:
# task lint-markdown DOCS_SCOPE=gated
# task lint-prose DOCS_SCOPE=all # the backlog view
MARKDOWN_SCOPE: '{{.DOCS_SCOPE | default "all"}}'
PROSE_SCOPE: '{{.DOCS_SCOPE | default "gated"}}'
tasks:
manifests:
desc: Generate ClusterRole and CustomResourceDefinition objects
deps:
- generate
sources:
- api/**/*.go
- internal/**/*.go
- cmd/**/*.go
- exclude: api/**/*_test.go
- exclude: internal/**/*_test.go
- exclude: cmd/**/*_test.go
- exclude: api/**/zz_generated.deepcopy.go
- exclude: internal/**/zz_generated.deepcopy.go
- exclude: cmd/**/zz_generated.deepcopy.go
generates:
- config/crd/bases/configbutler.ai*.yaml
- config/rbac/role.yaml
cmds:
- |
set -e
mkdir -p config/crd/bases config/rbac
# Write CRDs straight into the bases dir, overwriting in place. The
# marker predates the run, so afterwards any base controller-gen did
# not (re)write — i.e. a renamed/removed type — is older than it and
# gets pruned. Avoiding the old up-front rm keeps file watchers (e.g.
# Tilt) from seeing missing CRDs mid-regeneration.
marker="$(mktemp)"
trap 'rm -f "$marker"' EXIT
{{.CONTROLLER_GEN}} \
rbac:roleName=gitops-reverser \
crd \
paths=./api/... \
paths=./internal/controller/... \
paths=./internal/watch/... \
paths=./cmd/... \
output:crd:artifacts:config=config/crd/bases
find config/crd/bases -maxdepth 1 -name '*.yaml' ! -newer "$marker" -delete
helm-sync:
desc: Sync CRDs and roles from config/crd/bases into the Helm chart
deps:
- manifests
sources:
- api/**/*.go
- internal/**/*.go
- cmd/**/*.go
- exclude: api/**/*_test.go
- exclude: internal/**/*_test.go
- exclude: cmd/**/*_test.go
- exclude: api/**/zz_generated.deepcopy.go
- exclude: internal/**/zz_generated.deepcopy.go
- exclude: cmd/**/zz_generated.deepcopy.go
generates:
- charts/gitops-reverser/crds/configbutler.*.yaml
- charts/gitops-reverser/config/role.yaml
cmds:
- |
mkdir -p charts/gitops-reverser/crds charts/gitops-reverser/config
rm -f charts/gitops-reverser/crds/*.yaml charts/gitops-reverser/config/*.yaml
cp config/crd/bases/*.yaml charts/gitops-reverser/crds/
cp config/rbac/role.yaml charts/gitops-reverser/config/role.yaml
generate:
desc: Generate DeepCopy implementations
sources:
- api/**/*.go
- hack/boilerplate.go.txt
- exclude: api/**/*_test.go
- exclude: api/**/zz_generated.deepcopy.go
generates:
- api/v1alpha3/zz_generated.deepcopy.go
cmds:
- '{{.CONTROLLER_GEN}} object:headerFile="hack/boilerplate.go.txt" paths=./api/...'
fmt:
desc: Run go fmt against code
cmds:
- go fmt ./...
vet:
desc: Run go vet against code
cmds:
- go vet ./...
setup-envtest:
desc: Setup envtest binaries for unit tests
method: timestamp
sources:
- go.mod
generates:
- '{{.ENVTEST_STAMP}}'
cmds:
- |
echo "Setting up envtest binaries for Kubernetes version {{.ENVTEST_K8S_VERSION}}..."
mkdir -p "$(pwd)/bin" "$(dirname '{{.ENVTEST_STAMP}}')"
{{.ENVTEST}} use {{.ENVTEST_K8S_VERSION}} --bin-dir "$(pwd)/bin" -p path
touch '{{.ENVTEST_STAMP}}'
link-envtest-assets:
desc: Point bin/k8s/current at the envtest binaries, which is what the IDE sets KUBEBUILDER_ASSETS to
# .devcontainer/devcontainer.json sets go.testEnvVars.KUBEBUILDER_ASSETS to
# ${workspaceFolder}/bin/k8s/current, so a controller test run from the Test
# Explorer or the CodeLens resolves through this symlink. Nothing used to create
# it, and envtest does not say "assets missing" when the path is wrong: the specs
# just fail. That cost an investigation once already.
#
# `task test` does NOT depend on this symlink — it exports the path from
# setup-envtest directly, so the gate keeps working if the link is stale or gone.
# This exists for the editor.
#
# There is deliberately no status check. The obvious one — does bin/k8s/current
# resolve? — is true of a link left pointing at a previous version, because
# setup-envtest keeps every version it has ever downloaded, so the old directory
# is still there after a k8s.io/api bump moves ENVTEST_K8S_VERSION. The link
# would then keep resolving to the old apiserver and the editor would silently
# test against it. A status that compared the link against the selected path
# would have to run setup-envtest to learn that path, which is the whole cost of
# just relinking, so relinking every time is both cheaper and correct.
deps:
- setup-envtest
cmds:
- |
set -eu
assets="$({{.ENVTEST}} use {{.ENVTEST_K8S_VERSION}} --bin-dir "$(pwd)/bin" -p path)"
ln -sfn "${assets}" "$(pwd)/bin/k8s/current"
echo "bin/k8s/current -> ${assets}"
test:
desc: Run unit tests
deps:
- manifests
- fmt
- vet
- setup-envtest
- link-envtest-assets
# cmd's chart-args test runs `helm template`, which evaluates every template —
# including rbac.yaml, which reads the gitignored chart/config/role.yaml this
# syncs. Without it a fresh checkout fails the unit suite.
- helm-sync
cmds:
- |
export KUBEBUILDER_ASSETS="$({{.ENVTEST}} use {{.ENVTEST_K8S_VERSION}} --bin-dir "$(pwd)/bin" -p path)"
go test $(go list ./... | grep -Ev '/test/e2e$') -coverprofile cover.out
- task: cover-check
cover-check:
desc: Self-ratcheting local coverage gate; fails if total drops below the high-water baseline
cmds:
- |
set -eu
if [ ! -f cover.out ]; then
echo "cover.out not found; run 'task test' first" >&2
exit 1
fi
# .coverage-baseline is a committed high-water mark (local equivalent of
# Codecov's project ratchet). The gate fails only on a drop beyond TOL%
# (noise slack); any improvement auto-raises the baseline so it climbs on
# its own. Commit the bumped file to advance the floor for everyone.
tol=0.5
baseline="$(cat .coverage-baseline 2>/dev/null || echo 0)"
total="$(go tool cover -func=cover.out | awk '/^total:/ {gsub("%","",$3); print $3}')"
if awk -v t="${total}" -v b="${baseline}" -v tol="${tol}" 'BEGIN{exit !(t+0 < b-tol)}'; then
printf "❌ unit coverage %s%% dropped more than %s%% below baseline %s%%\n" "${total}" "${tol}" "${baseline}"
exit 1
fi
if awk -v t="${total}" -v b="${baseline}" 'BEGIN{exit !(t+0 > b+0)}'; then
printf '%s\n' "${total}" > .coverage-baseline
printf "✅ unit coverage %s%% — raised baseline %s%% → %s%% (commit .coverage-baseline)\n" "${total}" "${baseline}" "${total}"
else
printf "✅ unit coverage %s%% (baseline %s%%, tolerance %s%%)\n" "${total}" "${baseline}" "${tol}"
fi
fuzz-smoke:
desc: Short fuzzing smoke — briefly active-fuzz each target under the race detector
cmds:
- |
set -eu
for entry in {{.FUZZ_TARGETS}}; do
pkg="${entry%%:*}"; target="${entry##*:}"
echo "==> fuzz-smoke ${target} (${pkg}) for {{.FUZZ_SMOKE_TIME}}"
go test "${pkg}" -run '^$' -fuzz="^${target}$" -fuzztime={{.FUZZ_SMOKE_TIME}} -race
done
fuzz:
desc: >-
Release-time dynamic analysis — longer fuzz discovery run per target under the race
detector. Crash reproducers land under <pkg>/testdata/fuzz/<Target>/ (commit them).
See docs/finished/dynamic-analysis-fuzzing-plan.md.
cmds:
- |
set -eu
for entry in {{.FUZZ_TARGETS}}; do
pkg="${entry%%:*}"; target="${entry##*:}"
echo "==> fuzz ${target} (${pkg}) for {{.FUZZ_TIME}}"
go test "${pkg}" -run '^$' -fuzz="^${target}$" -fuzztime={{.FUZZ_TIME}} -race
done
lint:
desc: Run all linters (Go, Dockerfiles, workflows, Helm chart, docs); deps run in parallel where safe
deps:
- lint-golang
- lint-gomod
- lint-toolchain-pins
- lint-dockerfiles
- lint-actions
- lint-helm
- lint-docs
lint-docs:
desc: All documentation checks — references, markdown structure, prose, and metric names
# The four checks do not overlap and each is bad at the others' job, so they stay
# four tasks and this aggregates them. See docs/design/docs-linting.md.
#
# lint-doc-links references resolve, including doc paths inside Go comments
# lint-markdown markdown structure: fences, headings, lists, blank lines
# lint-prose English against docs/style-guide.md
# lint-metric-names every gitopsreverser_ metric named in the tree is registered
# lint-settings-index the settings table matches the CRDs it claims to describe
#
# This kept the name `lint-docs` because AGENTS.md, docs/style-guide.md, and the
# CI cache assertion all name it. It is now a superset of what it used to do, so
# every existing instruction to "run task lint-docs" is still true.
deps:
- lint-doc-links
- lint-markdown
- lint-prose
- lint-metric-names
- lint-settings-index
lint-doc-links:
desc: Check that every doc reference resolves — markdown links, and docs/**.md cited in Go comments
# Re-run whenever a doc moves or a Go comment changes. Task fingerprints these
# under .task/ and skips the run otherwise.
#
# This exists because nothing off the shelf covers the second surface: markdownlint
# does not check links, and link checkers (lychee, markdown-link-check) extract URLs
# from markdown -- they never look inside a Go comment for a repo-relative doc path.
# Those are the references that rot silently, and seventeen of them were dangling
# before this check was written.
# Deliberately un-fingerprinted: this task has no `sources:`, so it always runs.
#
# Its real input is "every git-tracked .md, .go, .yml, .yaml and .sh", which Task
# cannot express. Approximating it with globs went wrong twice. An unrooted
# '**/*.go' makes Task walk every path under the repo root -- including the 1.2 GB
# of upstream checkouts in external-sources/ -- and fingerprinting that costs tens
# of GB inside the Task process, enough to OOM the host. (`exclude:` does not help:
# Task expands the glob first and filters afterwards, so the walk still happens.)
# A hand-maintained per-tree list avoids that, but then silently drifts out of sync
# with the file types doccheck scans, and a stale "up to date" means the check
# never runs -- which is how a non-compiling file reached a commit here once.
#
# The whole check takes ~0.2s. That is not worth a cache that can be wrong.
cmds:
- go run ./hack/doccheck
settings-index:
desc: Regenerate the settings index in docs/configuration.md from the CRD schemas
# The writer half of lint-settings-index. Run it after `task manifests` changes an
# API, then commit docs/configuration.md with the change that caused it.
cmds:
- go run ./hack/crdfields
lint-settings-index:
desc: Check that the settings index in docs/configuration.md matches the CRDs
deps:
# Task runs deps in parallel, and this one reaches `lint` through lint-docs
# while lint-golang reaches manifests through its own branch. Without this,
# the check can read config/crd/bases while controller-gen is rewriting it
# and pass against the previous CRDs — in the one run that matters, the one
# where an API field was just added.
- manifests
# Unlike its four neighbours this one CAN be fingerprinted: its inputs are three
# named paths, not "every tracked file", so there is no unrooted `**` to walk and
# no hand-maintained list to drift. That keeps it out of the CI cache exemption.
#
# It exists because the table it guards was written by hand once and got three
# defaults backwards, each in the direction that costs a reader an outage:
# accessFrom omitted DENIES every namespace, serializeNamespace omitted INFERS
# per document rather than meaning false, and qps/burst fall back to the
# operator-wide flags. The schema knew all three. Nothing was asking it.
sources:
- config/crd/bases/*.yaml
- hack/crdfields/*.go
- hack/crdfields/fields.yaml
- docs/configuration.md
cmds:
- go run ./hack/crdfields -check
lint-metric-names:
desc: Check that every gitopsreverser_ metric named anywhere in the tree is actually registered
# A renamed metric leaves its callers behind silently: operational queries are written
# `... or vector(0)`, so a dead name reads as a confident zero rather than an error.
# That is how `hack/attribution-diagnostics.sh` came to report "audit events INGESTED
# by us = 0" on a healthy cluster after the metric surface was rebuilt.
#
# Deliberately un-fingerprinted, for the same reason as lint-doc-links: the real input
# is "every git-tracked file", which Task cannot express without an unrooted glob, and
# an unrooted '**' walk over external-sources/ has OOM'd the host here before.
# The check takes ~0.3s.
cmds:
- ./hack/metricnames.sh
lint-markdown:
desc: 'Markdown structure (markdownlint-cli2), over every tracked file. DOCS_SCOPE=gated narrows it'
# Scope is the whole tree. It started as the .docs-lint-scope list because 102 of
# the 174 linted files failed; that residue has since been cleared, so structure
# is gated everywhere and a new document cannot regress it. `DOCS_SCOPE=gated`
# narrows it back to the list, which is only useful when reproducing what the
# prose gate sees. See docs/design/docs-linting.md.
#
# No `sources:`. Same reasoning as lint-doc-links above: the real input is "every
# tracked .md", which Task cannot express without an unrooted `**` that walks
# external-sources/ and has OOM-killed the host. Both tools finish the whole tree
# in under three seconds, so this always runs. CI's cache assertion exempts it.
cmds:
- ./hack/docs-files.sh {{.MARKDOWN_SCOPE}} | xargs -r markdownlint-cli2
lint-markdown-fix:
desc: 'Apply the safe, mechanical markdown fixes (markdownlint-cli2 --fix)'
# Deterministic and reviewable rule by rule: blank lines, bullet style, hard tabs.
# It does NOT clear everything -- over the whole tree it fixes 466 of 723 findings
# and leaves 257 that need a human, led by MD013 line-length (119), MD040 missing
# fence languages (54), and MD036 emphasis-used-as-heading (43). There is no
# equivalent for prose: Vale can suggest, but nothing here rewrites English.
#
# Not `xargs`, unlike the two checking tasks, because this one has to tell the
# exit codes apart and xargs collapses every one of them to 123. Measured:
# 0 clean, 1 findings remain, 2 config or usage error. The word split is safe
# because git tracks no markdown path containing a space.
cmds:
- |
files="$(./hack/docs-files.sh {{.MARKDOWN_SCOPE}})"
[ -n "${files}" ] || exit 0
rc=0
# shellcheck disable=SC2086
markdownlint-cli2 --fix ${files} || rc=$?
# Exit 1 means findings remain, which is the normal outcome: --fix cannot
# repair a long line, a missing fence language, or bold used as a heading.
# Tolerated so `task lint-fix` still reaches the Go half. Never silent: the
# note below says work is left, and `task lint-markdown` names it.
[ "${rc}" -le 1 ] || exit "${rc}"
[ "${rc}" -eq 0 ] || echo "note: findings remain that --fix cannot repair; run 'task lint-markdown' to see them" >&2
lint-prose:
desc: 'Prose against docs/style-guide.md (Vale), over .docs-lint-scope. DOCS_SCOPE=all shows the backlog'
# Still scoped by .docs-lint-scope, unlike lint-markdown: 145 of the 167 linted
# files carry at least one error, almost all of them em dashes, and
# docs/style-guide.md says that cleanup must not land as one sweeping commit. The
# list grows as files are cleaned; `DOCS_SCOPE=all` shows the whole backlog.
#
# Vale's exit code tracks errors only, so the gate is EmDash, Spelling, and
# ProductName. Headings, WordsToCut, and Correctives print and do not block, which
# is what .vale.ini's level table is for.
cmds:
- ./hack/docs-files.sh {{.PROSE_SCOPE}} | xargs -r vale
lint-golang:
desc: Run golangci-lint
deps:
# The Helm lint branch also reaches manifests -> generate via helm-sync.
# Wait for generated Go/CRD files to settle before golangci-lint scans api/**/*.go.
- manifests
# golangci-lint's real inputs: the module's Go sources (including tests, which it
# lints), the lint config, and the dependency graph. The external-sources/* trees
# are separate modules and are not linted, so they are deliberately not listed.
sources:
- api/**/*.go
- cmd/**/*.go
- hack/**/*.go
- internal/**/*.go
- pkg/**/*.go
- test/**/*.go
- .golangci.yml
- go.mod
- go.sum
cmds:
- '{{.GOLANGCI_LINT}} run'
lint-gomod:
desc: Check that go.mod and go.sum are tidy, in every module
# Only `go mod tidy` knows whether the module graph is tidy, so ask it rather than
# re-deriving the answer. `-diff` reports what tidying WOULD change and exits non-zero,
# without writing anything, so the check never mutates the tree it is checking.
#
# Nothing gated this before, and it drifted: k8s.io/apiextensions-apiserver sat marked
# `// indirect` while internal/controller imported it directly in a test.
#
# EVERY module, not just the root one. `go mod tidy` operates on the module containing
# the working directory and does not recurse, so a nested module is invisible to a check
# that only runs here — and hack/sops-build is dependency surface the image is built
# from, where a silent drift is exactly what this repository is trying not to have.
# A module added later must be added to MODULES below; there is no discovery step,
# because `find`ing go.mod files would also walk the untracked clones in external-sources/.
vars:
MODULES: ". hack/sops-build"
sources:
- api/**/*.go
- cmd/**/*.go
- hack/**/*.go
- internal/**/*.go
- pkg/**/*.go
- test/**/*.go
- go.mod
- go.sum
- hack/sops-build/go.mod
- hack/sops-build/go.sum
cmds:
- |
set -eu
rc=0
for mod in {{.MODULES}}; do
if ! (cd "${mod}" && go mod tidy -diff); then
echo "❌ ${mod}/go.mod and ${mod}/go.sum are not tidy — run 'go mod tidy' there and commit the result" >&2
rc=1
fi
done
exit "${rc}"
lint-toolchain-pins:
desc: Check that the CI image's Go tool pins match the versions go.mod resolves
# Three of the binaries .devcontainer/Dockerfile installs are the command form of
# a module go.mod already resolves, so the two have to move together:
#
# ginkgo <- github.com/onsi/ginkgo/v2
# setup-envtest <- sigs.k8s.io/controller-runtime
# goimports <- golang.org/x/tools
#
# Nothing connected them before, and nothing could: Dependabot's gomod updates
# cannot reach a `go install ...@version` string in a Dockerfile, so a bumped
# go.mod quietly left the image a release behind.
#
# How much that costs differs per tool, and only one of the three is load-bearing:
#
# setup-envtest is invoked as the image's binary (the ENVTEST var defaults to
# the bare name), so a stale one is a stale one in every `task test` run.
#
# ginkgo and goimports are convenience binaries. The e2e tasks deliberately
# run ginkgo via `go run` so the CLI always matches the imported library
# (see test/e2e/Taskfile-e2e.yml), and golangci-lint uses its own bundled
# goimports -- so a drifted pin here breaks no task. It bites the human who
# types `ginkgo` in the container and gets a version-mismatch refusal instead
# of a test run.
#
# Keeping all three under one check is the cheap way to keep that distinction
# from having to be remembered. Note the consequence: a gomod bump to any of
# these three now fails lint until the Dockerfile moves with it. That is the
# intent -- the pair lands in one commit rather than drifting until someone
# notices.
#
# `go list -m` is asked for the version rather than grepping go.mod, because the
# build list is what a build actually uses -- an indirect requirement (x/tools is
# one) can be raised by the module graph without its go.mod line changing.
#
# controller-gen and scc are deliberately absent: neither module is in go.mod, so
# their pins answer to upstream releases alone and there is nothing to agree with.
vars:
DOCKERFILE: .devcontainer/Dockerfile
# module <-> the cmd path .devcontainer/Dockerfile installs it as
PAIRS: >-
github.com/onsi/ginkgo/v2=github.com/onsi/ginkgo/v2/ginkgo
sigs.k8s.io/controller-runtime=sigs.k8s.io/controller-runtime/tools/setup-envtest
golang.org/x/tools=golang.org/x/tools/cmd/goimports
sources:
- .devcontainer/Dockerfile
- go.mod
- go.sum
cmds:
- |
set -eu
rc=0
for pair in {{.PAIRS}}; do
mod="${pair%%=*}"
cmd="${pair#*=}"
want="$(go list -m -f '{{`{{.Version}}`}}' "${mod}")"
# Fixed-string, and every reachable install rather than the first. A module
# path interpolated into a regex makes its dots wildcards, and `head -n1`
# let a correct first install mask a later one that had drifted -- this
# Dockerfile installs golang.org/x/tools twice, as goimports and as gopls.
got_list="$(awk -v needle="go install ${cmd}@" '
{
s = $0
while ((p = index(s, needle)) > 0) {
s = substr(s, p + length(needle))
v = s
sub(/[ \\].*$/, "", v)
if (v != "") print v
}
}' {{.DOCKERFILE}})"
if [ -z "${got_list}" ]; then
echo "❌ {{.DOCKERFILE}} installs no ${cmd} — add 'go install ${cmd}@${want}' or drop ${mod} from PAIRS" >&2
rc=1
continue
fi
for got in ${got_list}; do
if [ "${got}" != "${want}" ]; then
echo "❌ {{.DOCKERFILE}} pins ${cmd}@${got} but go.mod resolves ${mod} ${want} — bump the Dockerfile" >&2
rc=1
fi
done
done
exit "${rc}"
lint-actions:
desc: Lint GitHub Actions workflows with actionlint
# Re-lint whenever any workflow changes (glob covers new pipelines too).
sources:
- .github/workflows/*.yml
- .github/workflows/*.yaml
# No path arg: actionlint auto-discovers every workflow under
# .github/workflows/, so newly added pipelines are checked automatically.
cmds:
- actionlint
scan-image:
desc: Scan a container image for vulnerabilities — full report, then the CI gate
# This task IS the gate CI runs. Both live here rather than in the workflow so the
# severities, the ignore file and the exit codes have one definition: they were
# duplicated across four workflow steps in two jobs, which is four places to keep in
# agreement and no way to run any of them before pushing.
#
# It scans in two passes on purpose. The first reports everything a human should see
# and never fails. The second is the gate, and it fails ONLY on CRITICAL findings that
# have a fix — actionable signal, not noise from unfixed CVEs. Only the gate reads
# .trivyignore.yaml, so a suppressed finding still appears in the report above it: it
# stops blocking without disappearing. Trivy does not read that file from the working
# directory on its own, which is why --ignorefile is passed explicitly.
#
# A missing .trivyignore.yaml is a FATAL error rather than a silent no-op, which is
# the behaviour to want from a security control: the gate cannot quietly stop
# honouring its own suppressions. It is a relative path, so run this from the
# repository root (CI mounts the workspace and sets -w).
#
# Point it at either a docker-archive tarball or an image reference:
# task scan-image SCAN_ARCHIVE=project-image.tar
# task scan-image SCAN_IMAGE=ghcr.io/configbutler/gitops-reverser@sha256:...
# A tarball needs no docker daemon and no registry pull, so CI prefers it for the
# image it just built. SCAN_SARIF=<path> makes the report pass write SARIF for GitHub
# code scanning instead of a table; the gate is unaffected either way.
vars:
SCAN_ARCHIVE: '{{.SCAN_ARCHIVE | default ""}}'
SCAN_IMAGE: '{{.SCAN_IMAGE | default ""}}'
SCAN_SARIF: '{{.SCAN_SARIF | default ""}}'
SCAN_TARGET: '{{if .SCAN_ARCHIVE}}--input {{.SCAN_ARCHIVE}}{{else}}{{.SCAN_IMAGE}}{{end}}'
SCAN_REPORT_FORMAT: '{{if .SCAN_SARIF}}--format sarif --output {{.SCAN_SARIF}}{{else}}--format table{{end}}'
preconditions:
- sh: '[ -n "{{.SCAN_ARCHIVE}}{{.SCAN_IMAGE}}" ]'
msg: "set SCAN_ARCHIVE=<tarball> or SCAN_IMAGE=<ref> — there is nothing to scan otherwise"
cmds:
- trivy image {{.SCAN_TARGET}} {{.SCAN_REPORT_FORMAT}} --severity CRITICAL,HIGH,MEDIUM --exit-code 0
- trivy image {{.SCAN_TARGET}} --format table --severity CRITICAL --ignore-unfixed
--ignorefile .trivyignore.yaml --exit-code 1
lint-dockerfiles:
desc: Lint the project Dockerfiles with hadolint
# Only re-lint when a Dockerfile or the hadolint config changes; Task
# fingerprints these (checksum) under .task/ and skips the run otherwise.
sources:
- .devcontainer/Dockerfile
- Dockerfile
- .hadolint.yaml
cmds:
- hadolint .devcontainer/Dockerfile Dockerfile
lint-helm:
desc: Lint the Helm chart with helm lint
# Depends on helm-sync so the chart's generated CRDs/role are present and current
# before linting -- a complete check that also works on a fresh checkout (e.g. CI),
# where those files are gitignored and absent. This pulls in manifests/controller-gen,
# which re-runs on every invocation, so `task lint` is not a strict no-op; that is an
# accepted trade for a complete check. The generated CRDs are gitignored, so the
# resync never produces a stray git diff. Fingerprint every chart YAML/template so an
# edit to a template *or* a regenerated CRD re-triggers the lint.
deps:
- helm-sync
sources:
- charts/gitops-reverser/**/*.yaml
- charts/gitops-reverser/**/*.tpl
cmds:
- helm lint charts/gitops-reverser
lint-fix:
desc: Apply every safe automatic fix (Go and markdown)
# Folds in lint-markdown-fix so there is one "fix what can be fixed" entry point.
# Both halves are deterministic. Neither touches prose: Vale findings are always
# hand-edits, by design, because a machine rewriting English changes meaning.
deps:
- lint-markdown-fix
cmds:
- '{{.GOLANGCI_LINT}} run --fix'
lint-config:
desc: Verify golangci-lint configuration
cmds:
- '{{.GOLANGCI_LINT}} config verify'
loc:
desc: Count lines of project code with scc (excludes vendored external-sources)
cmds:
- '{{.SCC}} --exclude-dir external-sources,vendor,bin,dist {{.CLI_ARGS}}'
gitops-layouts-baseline:
desc: Regenerate the GitOps layout corpus behavioural baseline (test/fixtures/gitops-layouts/support-today.md)
# The baseline is generated, never hand-edited, so that a change to the
# acceptance boundary shows up in review as a diff of exactly which fixtures
# moved and in which direction. See test/fixtures/gitops-layouts/README.md.
sources:
- test/fixtures/gitops-layouts/**/*
- internal/manifestanalyzer/**/*.go
- cmd/manifest-analyzer/**/*.go
- hack/gitops-layouts-baseline.sh
generates:
- test/fixtures/gitops-layouts/support-today.md
cmds:
- bash hack/gitops-layouts-baseline.sh
build:
desc: Build manager binary
deps:
- manifests
- fmt
- vet
cmds:
- go build -o bin/manager ./cmd
run:
desc: Run a controller from your host
deps:
- manifests
- fmt
- vet
cmds:
- go run ./cmd
docker-build:
desc: Build docker image with the manager
cmds:
- |
GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
GIT_DIRTY=0
[ -z "$(git status --porcelain 2>/dev/null)" ] || GIT_DIRTY=1
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)"
VERSION="${VERSION#gitops-reverser-v}"
{{.CONTAINER_TOOL}} build \
--build-arg VERSION="${VERSION}" \
--build-arg GIT_COMMIT="${GIT_COMMIT}" \
--build-arg GIT_DIRTY="${GIT_DIRTY}" \
--build-arg BUILD_DATE="${BUILD_DATE}" \
-t {{.IMG}} .
docker-push:
desc: Push docker image with the manager
cmds:
- '{{.CONTAINER_TOOL}} push {{.IMG}}'
docker-buildx:
desc: Build and push docker image for cross-platform support
vars:
PLATFORMS: '{{.PLATFORMS | default "linux/arm64,linux/amd64,linux/s390x,linux/ppc64le"}}'
cmds:
- |
GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
GIT_DIRTY=0
[ -z "$(git status --porcelain 2>/dev/null)" ] || GIT_DIRTY=1
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)"
VERSION="${VERSION#gitops-reverser-v}"
sed -e '1 s/\(^FROM\)/FROM --platform=\${BUILDPLATFORM}/; t' \
-e '1,// s//FROM --platform=\${BUILDPLATFORM}/' Dockerfile > Dockerfile.cross
{{.CONTAINER_TOOL}} buildx create --name gitops-reverser-builder || true
{{.CONTAINER_TOOL}} buildx use gitops-reverser-builder
{{.CONTAINER_TOOL}} buildx build --push --platform={{.PLATFORMS}} \
--build-arg VERSION="${VERSION}" \
--build-arg GIT_COMMIT="${GIT_COMMIT}" \
--build-arg GIT_DIRTY="${GIT_DIRTY}" \
--build-arg BUILD_DATE="${BUILD_DATE}" \
--tag {{.IMG}} -f Dockerfile.cross .
{{.CONTAINER_TOOL}} buildx rm gitops-reverser-builder || true
rm -f Dockerfile.cross
docker-push-dev:
desc: Build and push the image to your dev registry (DEV_IMG, set in .env)
requires:
vars:
- DEV_IMG
cmds:
- task: docker-build
vars:
IMG: '{{.DEV_IMG}}'
- task: docker-push
vars:
IMG: '{{.DEV_IMG}}'
dist-install:
desc: Generate the split installer bundle (dist/crds.yaml + dist/install.yaml) from the Helm chart
deps:
- helm-sync
sources:
- api/**/*.go
- internal/**/*.go
- cmd/**/*.go
- charts/gitops-reverser/Chart.yaml
- charts/gitops-reverser/values.yaml
- charts/gitops-reverser/templates/**
- charts/gitops-reverser/crds/*.yaml
- exclude: api/**/*_test.go
- exclude: internal/**/*_test.go
- exclude: cmd/**/*_test.go
- exclude: api/**/zz_generated.deepcopy.go
- exclude: internal/**/zz_generated.deepcopy.go
- exclude: cmd/**/zz_generated.deepcopy.go
generates:
- dist/crds.yaml
- dist/install.yaml
cmds:
- |
mkdir -p dist
# CRDs ship as their OWN file, applied FIRST. The bundle contains the reserved `default`
# ClusterProvider — a custom resource that cannot be applied in the same `kubectl apply` as
# the CRD defining it: kubectl builds its RESTMapper up front and fails with
# `no matches for kind "ClusterProvider"`. So: kubectl apply -f dist/crds.yaml, then
# kubectl apply -f dist/install.yaml. `helm install` is unaffected — it installs
# charts/gitops-reverser/crds/ in its own first phase, which is why --include-crds is gone
# here. Each chart CRD file already begins with its own `---`, so a plain cat is valid
# multi-doc YAML.
cat charts/gitops-reverser/crds/*.yaml > dist/crds.yaml
{{.HELM}} template {{.INSTALL_NAME}} charts/gitops-reverser \
--namespace {{.NAMESPACE}} \
--set labels.managedBy=kubectl \
--set createNamespace=true \
--set queue.redis.addr={{.DEFAULT_AUDIT_REDIS_ADDR}} \
--set queue.redis.auth.existingSecret=valkey-auth \
> dist/install.yaml
clean:
desc: Remove build artifacts, coverage, generated dist files, and stamps
cmds:
- rm -rf bin/ cover.out dist/ .stamps/