-
Notifications
You must be signed in to change notification settings - Fork 108
Expand file tree
/
Copy pathTaskfile.yml
More file actions
370 lines (337 loc) · 16.1 KB
/
Copy pathTaskfile.yml
File metadata and controls
370 lines (337 loc) · 16.1 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
version: "3"
vars:
# Tracked scripts only, NUL-separated so a path with a space survives.
#
# A walk of the working tree would also reach the shell completions that the
# release build generates into an ignored directory. Cobra writes those in its
# own style, so `task lint` failed for anyone who had run `task build` in the
# same checkout, and `task fmt` rewrote generated files. The same applies to any
# scratch directory or second worktree living under this one.
SHELL_FILES: git ls-files -z '*.sh'
# -r keeps shfmt from being run with no file arguments if the list is ever empty,
# in which case it would read stdin instead of checking anything. BSD xargs
# already behaves that way; GNU needs telling.
SHFMT_FLAGS: -i 2 -ci -sr
# The build tags the integration suites sit behind. `go fix` only sees the files
# its tags select, so without these it silently skips those suites, while
# golangci-lint reads the same list from its own config and does not.
BUILD_TAGS: integration,integration_cloud
# Every check runs in the toolchain image built from hack/dev.Dockerfile, so
# the host needs only docker, buildx and task, and CI runs the exact same tool
# versions. Targets that write into the working tree (the formatters, the
# generators, the fuzz corpus, the snapshot build) bind-mount it instead of
# running as a stage, because a stage has no way to hand its output back.
#
# The container runs as the invoking user so nothing comes back root-owned,
# and a named volume keeps the Go and linter caches between runs.
#
# The git directory is mounted at its own absolute host path as well as inside
# the tree. In a linked worktree the checkout's .git is a file naming a common
# directory that lives outside the checkout, so without that second mount git
# inside the container reports the tree is not a repository. Two targets read
# the repository through git, the tracked-file test and the snapshot build, and
# both would fail in a worktree. For a primary checkout the mount is redundant
# and harmless.
TOOLS: >-
docker run --rm -u "$(id -u):$(id -g)"
-e GOFLAGS=-mod=mod -e HOME=/tmp
-e GOCACHE=/tmp/go-build -e GOMODCACHE=/tmp/go-mod
-e GOLANGCI_LINT_CACHE=/tmp/golangci-lint
-v pv-migrate-tools-cache:/tmp
-v "$(git rev-parse --path-format=absolute --git-common-dir)":"$(git rev-parse --path-format=absolute --git-common-dir)"
-v "$PWD":/src -w /src pv-migrate-tools:local
tasks:
# builds the bind-mount image the write-in-place targets share
tools-image:
internal: true
cmds:
- >-
docker buildx build -f hack/dev.Dockerfile
--build-arg GO_VERSION="$(sed -n 's/^go //p' go.mod)"
--target fmt --load --tag pv-migrate-tools:local .
# runs one check as its own buildx stage
stage:
internal: true
cmds:
- >-
docker buildx build -f hack/dev.Dockerfile
--build-arg GO_VERSION="$(sed -n 's/^go //p' go.mod)"
--target {{.TARGET}} .
fmt:
desc: format code and shell scripts
cmds:
- task: tools-image
- "{{.TOOLS}} go mod tidy"
- "{{.TOOLS}} go fix -tags {{.BUILD_TAGS}} ./..."
- "{{.TOOLS}} golangci-lint run --fix --issues-exit-code 0 ./..."
- "{{.TOOLS}} golangci-lint fmt ./..."
- '{{.SHELL_FILES}} | xargs -0 -r {{.TOOLS}} shfmt -l -w {{.SHFMT_FLAGS}}'
# Split into named checks so that a failure names the check that failed. Run as
# one list of commands, `task` could only report the whole thing as exit status 1.
lint:
desc: lint everything (go, chart, shell scripts, release config)
cmds:
- task: lint:go
- task: lint:chart
- task: lint:shell
- task: lint:release
lint:go:
desc: lint go code and module tidiness
cmds:
- task: stage
vars: {TARGET: lint-go-mod-tidy}
- task: stage
vars: {TARGET: lint-golangci-lint}
lint:chart:
desc: lint the embedded helm chart
cmds:
- task: stage
vars: {TARGET: lint-chart}
lint:shell:
desc: check shell script formatting
cmds:
- task: tools-image
- |
if ! {{.SHELL_FILES}} | xargs -0 -r {{.TOOLS}} shfmt -d {{.SHFMT_FLAGS}}; then
echo "==> the shell scripts above are not formatted. Run: task fmt" >&2
exit 1
fi
lint:release:
desc: validate the release configuration
cmds:
- task: stage
vars: {TARGET: lint-release}
test:
desc: run the unit tests (COVERPROFILE=coverage.txt to also write a coverage profile)
cmds:
# bind-mounted rather than staged: one test lists the repository's tracked
# files through git, and .git is deliberately kept out of the build
# context because it changes on every commit and would invalidate every
# stage that copies the tree.
- task: tools-image
# the profile covers the whole module, not only the packages that have
# tests, so the number CI reports means the same thing as the one from
# the integration suites
- "{{.TOOLS}} go test {{if .COVERPROFILE}}-coverpkg=./... -coverprofile={{.COVERPROFILE}} -covermode=atomic {{end}}./..."
test:integration:
desc: run the migration integration tests against the current kube context
cmds:
- go test -tags integration -timeout 30m -v ./integration/ -run TestMigrate
# `task test` already replays every fuzz target's seed corpus, which is where
# the regression value sits. This drives the actual search instead, one target
# at a time because -fuzz only ever fuzzes one. A discovered failure is written
# to the package's testdata/fuzz directory, so it lands in the working tree.
test:fuzz:
desc: search for new fuzz failures (FUZZTIME=30s per target)
vars:
FUZZTIME: '{{.FUZZTIME | default "30s"}}'
cmds:
# bind-mounted rather than staged: a discovered failure is written to the
# package's testdata/fuzz directory and has to survive into the tree,
# which a stage that copies the sources cannot do. The cache volume also
# keeps the generated corpus between runs.
- task: tools-image
- |
set -eu
for pkg in $({{.TOOLS}} go list ./...); do
# a listing failure means the package does not compile, which has to
# surface rather than read as "no fuzz targets here"
listed="$({{.TOOLS}} go test -list 'Fuzz' "$pkg")"
for target in $(printf '%s\n' "$listed" | grep '^Fuzz' || true); do
echo "==> $pkg $target ({{.FUZZTIME}})"
{{.TOOLS}} go test "$pkg" -run '^$' -fuzz "^${target}\$" -fuzztime '{{.FUZZTIME}}'
done
done
clean:
desc: clean
cmds:
- rm -rf {{.ROOT_DIR}}/dist/
generate-all:
desc: update all generated files
cmds:
- task: generate-cli-reference
- task: generate-helm-chart-docs
generate-cli-reference:
desc: update generated CLI reference docs
cmds:
# one container for the whole job: the help text is produced by the same
# toolchain that renders it, and a single invocation avoids paying the
# container and link cost once per command.
- task: tools-image
- >-
{{.TOOLS}} sh -c 'set -eu;
ROOT_USAGE="$(go run ./cmd/pv-migrate --help)";
BACKUP_USAGE="$(go run ./cmd/pv-migrate backup --help)";
RESTORE_USAGE="$(go run ./cmd/pv-migrate restore --help)";
STATUS_USAGE="$(go run ./cmd/pv-migrate status --help)";
CLEANUP_USAGE="$(go run ./cmd/pv-migrate cleanup --help)";
COMPLETION_USAGE="$(go run ./cmd/pv-migrate completion --help)";
export ROOT_USAGE BACKUP_USAGE RESTORE_USAGE STATUS_USAGE CLEANUP_USAGE COMPLETION_USAGE;
go run ./hack/gen-cli-reference'
generate-helm-chart-docs:
desc: update generated helm chart docs
cmds:
- task: tools-image
- "{{.TOOLS}} helm-docs --chart-search-root internal/helm"
build:
desc: build
cmds:
- task: tools-image
- "{{.TOOLS}} goreleaser build --snapshot --clean --single-target"
# The demo recordings in the README come from the targets below. They are
# regenerated on demand and never on a schedule, since each run records a real
# migration and a periodic one would commit another megabyte of GIF that shows
# nothing new.
demo:fixtures:
desc: create the demo namespace and its data in the current kube context
# Seeding a gigabyte takes minutes, so a cluster that already has it is left
# alone and re-recording stays quick. The marker is written only once both
# jobs have finished. A bound claim proves storage was allocated and says
# nothing about whether anything was written to it.
status:
- kubectl get configmap demo-fixtures-ready -n demo
cmds:
- kubectl apply -f demo/fixtures.yaml
- kubectl wait --for=condition=complete job/demo-seed job/demo-bind -n demo --timeout=15m
- kubectl delete job demo-seed demo-bind -n demo --cascade=foreground
- kubectl create configmap demo-fixtures-ready -n demo
demo:record:
desc: re-record the README demo GIFs (needs vhs and a cluster)
deps: [demo:fixtures]
cmds:
- go build -o "{{.DEMO_BIN}}" ./cmd/pv-migrate
- task: demo:reset
- task: demo:record-tape
vars: {TAPE: demo/success.tape, DEMO_BIN: '{{.DEMO_BIN}}', VHS: '{{.VHS}}'}
- task: demo:reset
- task: demo:record-tape
vars: {TAPE: demo/failure.tape, DEMO_BIN: '{{.DEMO_BIN}}', VHS: '{{.VHS}}'}
vars:
DEMO_BIN: '{{.ROOT_DIR}}/dist/demo/pv-migrate'
# How the recorder is invoked, so that this list of steps stays the only
# description of what recording means. CI overrides it with the official
# image, whose fonts include the colour emoji the tool prints and which a
# bare runner may not have.
VHS: '{{.VHS | default "vhs"}}'
demo:record-tape:
internal: true
cmds:
# Two things have to be true afterwards, and neither is obvious from a
# zero exit code. The recorder honours a setting only where nothing
# precedes it and otherwise warns and carries on with its own default, and
# a run that records nothing at all leaves the previous recording in place
# looking like a fresh one. Both have reached a pull request already.
- |
set -eu
marker="$(mktemp)"
trap 'rm -f "$marker"' EXIT
output="$(awk '$1 == "Output" { print $2; exit }' "{{.TAPE}}")"
want="$(awk '$1 == "Set" && $2 == "Width" { w = $3 }
$1 == "Set" && $2 == "Height" { h = $3 }
END { printf "%dx%d", w, h }' demo/common.tape)"
status=0
recorded="$(PATH="$(dirname "{{.DEMO_BIN}}"):$PATH" {{.VHS}} "{{.TAPE}}" 2>&1)" || status=$?
printf '%s\n' "$recorded"
if [ "$status" -ne 0 ]; then
echo "==> {{.TAPE}}: the recorder failed, see its output above" >&2
exit "$status"
fi
case "$recorded" in
*"has been ignored"*)
echo "==> {{.TAPE}}: the recorder ignored a directive, so this recording is not what the tape asked for" >&2
exit 1
;;
esac
if [ ! "$output" -nt "$marker" ]; then
echo "==> {{.TAPE}}: $output was not written, so whatever is there now is older than this run" >&2
exit 1
fi
# The size lives in the GIF's own header, which needs no tools beyond
# the ones every shell has: width and height are two little-endian pairs
# starting at the seventh byte.
got="$(od -An -tu1 -j6 -N4 "$output" | awk '{ printf "%dx%d", $1 + $2 * 256, $3 + $4 * 256 }')"
if [ "$got" != "$want" ]; then
echo "==> $output came out $got where the tape asks for $want" >&2
exit 1
fi
demo:reset:
desc: empty the demo destination claim, so the next recording has work to do
cmds:
# A recording that was cut short leaves its release behind, since the tape
# ending kills the CLI before it can clean up after itself.
- helm list -n demo -q --filter '^pv-migrate-' | xargs -r helm uninstall -n demo
- kubectl delete job demo-reset -n demo --ignore-not-found --cascade=foreground
- kubectl apply -f demo/reset.yaml
- kubectl wait --for=condition=complete job/demo-reset -n demo --timeout=5m
# Deleted in the foreground so that the job's pod goes with it: a completed
# pod still counts as a consumer of the claim, which the pre-flight check
# reports as a mounted volume.
- kubectl delete job demo-reset -n demo --cascade=foreground
demo:clean:
desc: delete the whole demo namespace, fixtures and data included
cmds:
- kubectl delete namespace demo --ignore-not-found
# Pushing the tag is what starts the release, so every check below happens
# before anything leaves this machine.
release:
desc: tag the next version and push it, or VERSION when one is given
vars:
NUM_LAST_TAGS: 3
LAST_TAGS:
sh: git tag --sort=-version:refname | head -n {{.NUM_LAST_TAGS}} | xargs echo
# svu proposes a version only when a commit since the last tag calls for
# one, and otherwise answers with the version that is already released.
# Pinned like every other tool: a different svu can choose a different
# version from the same history, and this is the one calculation whose
# answer cannot be taken back once it has been tagged and pushed.
NEXT_TAG:
# renovate: depName=caarlos0/svu datasource=docker
# run from the primary checkout: a linked worktree's .git points at a
# common directory outside this mount
sh: docker run --rm -v "$PWD":/src -w /src caarlos0/svu:3.4.1 next
TAG: '{{.VERSION | default .NEXT_TAG}}'
cmds:
# releases are cut from the tip of main, with a clean tree. Tags are
# fetched too, so the next version is computed against all of them.
- git fetch --tags --force origin main
- test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" || { echo "HEAD is not the tip of origin/main" >&2; exit 1; }
- test -z "$(git status --porcelain)" || { echo "the working tree is not clean (staged or untracked files included)" >&2; exit 1; }
- |
set -eu
echo "Last {{.NUM_LAST_TAGS}} tags: {{.LAST_TAGS}}"
echo "Releasing: {{.TAG}}"
# The release only runs for tags of this shape, so a version of any
# other one is tagged and then silently never released.
case '{{.TAG}}' in
v[0-9]*.[0-9]*.[0-9]*) ;;
*)
echo "==> {{.TAG}} is not a vX.Y.Z version, and nothing else starts a release" >&2
exit 1
;;
esac
if git rev-parse -q --verify 'refs/tags/{{.TAG}}' >/dev/null; then
echo "==> {{.TAG}} is already released" >&2
echo "==> Nothing since it calls for a new version, so that is what svu answered with" >&2
echo "==> Name one to release it anyway, for example: task release VERSION=vX.Y.Z" >&2
exit 1
fi
# prove that signing works and that GitHub verifies it, on a throwaway
# tag the release rules do not cover, before a version number is at
# stake: a v* tag that fails the check cannot be deleted without
# disabling the ruleset first
- |
set -eu
t="sigcheck-$(date +%s)"
# removed again whatever happens below, so an aborted check leaves no tag behind
trap 'git push -q origin --delete "$t" >/dev/null 2>&1 || true; git tag -d "$t" >/dev/null 2>&1 || true' EXIT
git tag -s "$t" -m "$t"
git push -q origin "$t"
sha="$(gh api "repos/utkuozdemir/pv-migrate/git/ref/tags/$t" -q .object.sha)"
verified="$(gh api "repos/utkuozdemir/pv-migrate/git/tags/$sha" -q .verification.verified)"
test "$verified" = "true" || { echo "==> GitHub does not verify tags signed here. Is the signing key registered on the GitHub account as a signing key?" >&2; exit 1; }
# signed tag: the release workflow refuses a tag GitHub cannot verify,
# so this has to be -s. Release tags are immutable by ruleset, so a
# tag that fails there is fixed by disabling the ruleset briefly, not by
# force-pushing over it.
- git tag -s '{{.TAG}}' -m '{{.TAG}}'
- git push origin '{{.TAG}}'