-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTaskfile.yml
More file actions
401 lines (359 loc) · 18.2 KB
/
Copy pathTaskfile.yml
File metadata and controls
401 lines (359 loc) · 18.2 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
version: "3"
vars:
BINARY: devcontainer
MODULE: github.com/devcontainers/cli
# CalVer: YYYYMMDD.<build>. BUILD_NUMBER (e.g. a daily counter or CI run number)
# defaults to 01 for local builds. Tagged releases use the git tag verbatim.
VERSION:
sh: echo "$(git describe --tags --exact-match 2>/dev/null || echo "$(date -u +%Y%m%d).${BUILD_NUMBER:-01}")"
LDFLAGS: -s -w -X {{.MODULE}}/internal/product.version={{.VERSION}} -X {{.MODULE}}/internal/product.name={{.BINARY}}
# Minimum hermetic-unit statement coverage the `coverage` gate enforces. Set as a
# ratchet just below the current level; raise it as coverage grows (never lower).
COVERAGE_MIN: "52"
# Package ratchets prevent well-covered parsing packages from masking a
# regression in the orchestration layers. Keep these just below the measured
# hermetic baseline and only move them upward.
COVERAGE_CLI_MIN: "32"
COVERAGE_DOCKER_MIN: "77"
COVERAGE_LIFECYCLE_MIN: "73"
COVERAGE_OCI_MIN: "85"
# Coverage artifact layout. Every lane writes binary covdata under COVER_DATA/<lane>
# so `coverage:merge` can union them into the true cross-lane number.
COVER_DIR: artifacts/coverage
COVER_DATA: artifacts/coverage/data
tasks:
default:
desc: Build and test the Go CLI
cmds:
- task: test:unit
- task: test:integration
- task: build
build:
desc: Build the static Go CLI
env:
CGO_ENABLED: "0"
cmds:
- go build -ldflags "{{.LDFLAGS}}" -o {{.BINARY}} ./cmd/devcontainer/
sources: [cmd/**/*.go, internal/**/*.go, go.mod, go.sum]
generates: ["{{.BINARY}}"]
build:coverage:
desc: Build an instrumented CLI for subprocess/integration coverage
env:
CGO_ENABLED: "0"
cmds:
- mkdir -p artifacts/coverage/bin artifacts/coverage/data
- go build -cover -ldflags "{{.LDFLAGS}}" -o artifacts/coverage/bin/{{.BINARY}} ./cmd/devcontainer/
build:cross:
desc: Cross-compile release binaries
env:
CGO_ENABLED: "0"
cmds:
- mkdir -p dist
# Linux-only scope: only linux/amd64 + linux/arm64 are supported targets.
- GOOS=linux GOARCH=amd64 go build -ldflags "{{.LDFLAGS}}" -o dist/{{.BINARY}}-linux-amd64 ./cmd/devcontainer/
- GOOS=linux GOARCH=arm64 go build -ldflags "{{.LDFLAGS}}" -o dist/{{.BINARY}}-linux-arm64 ./cmd/devcontainer/
test:unit:
desc: Run hermetic unit tests
cmds:
- go test ./cmd/... ./internal/... -skip '^TestClient$|^TestE2E|^TestParityMatrix$|^TestPublishParity$' -count=1 {{.CLI_ARGS}}
test:integration:
desc: Run local HTTP integration tests
cmds:
- go test ./internal/httpx -run TestClient -count=1 {{.CLI_ARGS}}
test:race:
desc: Run hermetic unit tests under the race detector (needs cgo)
env:
CGO_ENABLED: "1"
cmds:
- go test ./cmd/... ./internal/... -skip '^TestClient$|^TestE2E|^TestParityMatrix$|^TestPublishParity$' -race -count=1 {{.CLI_ARGS}}
vuln:
desc: Scan module and code paths for known vulnerabilities (govulncheck)
cmds:
- go run golang.org/x/vuln/cmd/govulncheck@latest ./...
coverage:
desc: Hermetic unit coverage — per-package table + enforce the total/package floors
cmds:
# rm -rf first: covdata files accumulate across runs and would inflate the
# merged number otherwise.
- rm -rf {{.COVER_DATA}}/unit && mkdir -p {{.COVER_DATA}}/unit
# -coverprofile keeps coverage.out byte-identical for the gate; the extra
# -test.gocoverdir also emits binary covdata so `coverage:merge` can union it
# with the e2e/parity lanes.
- go test ./cmd/... ./internal/... -skip '^TestClient$|^TestE2E|^TestParityMatrix$|^TestPublishParity$' -coverprofile=coverage.out -count=1 -args -test.gocoverdir={{.ROOT_DIR}}/{{.COVER_DATA}}/unit
- go run ./cmd/devtool coverage-report coverage.out "unit (hermetic)"
# Ratchet: total + per-package floors (parsers must not mask lost CLI/OCI coverage).
- go run ./cmd/devtool coverage-gate coverage.out TOTAL={{.COVERAGE_MIN}} cli={{.COVERAGE_CLI_MIN}} docker={{.COVERAGE_DOCKER_MIN}} lifecycle={{.COVERAGE_LIFECYCLE_MIN}} oci={{.COVERAGE_OCI_MIN}}
coverage:e2e:
desc: Coverage of the real in-process E2E paths (requires Docker)
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for E2E coverage
cmds:
- rm -rf {{.COVER_DATA}}/e2e && mkdir -p {{.COVER_DATA}}/e2e
# -coverpkg instruments the whole module (these tests invoke NewRootCommand
# directly); -test.gocoverdir emits binary covdata for the merge.
- go test ./internal/cli -run '^TestE2E' -count=1 -timeout 5m -coverpkg=./... -args -test.gocoverdir={{.ROOT_DIR}}/{{.COVER_DATA}}/e2e
- go run ./cmd/devtool coverage-report {{.COVER_DATA}}/e2e "e2e"
# coverage:_parity-lane runs the instrumented CLI (build:coverage) through a
# parity lane, collecting covdata into COVER_DATA/<LANE> and printing the table.
# LANE and RUN (the full `go test ...` invocation) are supplied by the wrappers.
coverage:_parity-lane:
internal: true
deps: [build:coverage, reference:compile]
cmds:
- rm -rf {{.COVER_DATA}}/{{.LANE}} && mkdir -p {{.COVER_DATA}}/{{.LANE}}
- GOCOVERDIR={{.ROOT_DIR}}/{{.COVER_DATA}}/{{.LANE}} CLI_GO={{.ROOT_DIR}}/{{.COVER_DIR}}/bin/{{.BINARY}} {{.RUN}}
- go run ./cmd/devtool coverage-report {{.COVER_DATA}}/{{.LANE}} "parity:{{.LANE}}"
coverage:parity-contract:
desc: Coverage of the instrumented binary through the hermetic contract parity lane
cmds:
- task: coverage:_parity-lane
vars:
LANE: contract
RUN: >-
PARITY_REPORT_FILE={{.ROOT_DIR}}/{{.COVER_DIR}}/parity-contract.json
PARITY_STRICT=true PARITY_LANE=contract PARITY_SKIP_DOCKER=true PARITY_SKIP_NETWORK=true
go test ./internal/cli -run '^TestParityMatrix$' -count=1 -timeout 10m -v
coverage:parity-runtime:
desc: Coverage of the instrumented binary through the full Docker parity lane
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for runtime parity coverage
- sh: docker info 2>/dev/null | grep -q io.containerd.snapshotter
msg: The parity runtime lane requires the containerd image store (docker build --platform on local images); enable it with `task ci:prepare-runner`.
- sh: go run ./cmd/devtool docker-cache-export
msg: The Docker builder must support cache export (see `devtool ci-prepare-runner`)
cmds:
- task: coverage:_parity-lane
vars:
LANE: runtime
RUN: >-
PARITY_REPORT_FILE={{.ROOT_DIR}}/{{.COVER_DIR}}/parity-runtime.json
PARITY_STRICT=true PARITY_LANE=all
go test ./internal/cli -run '^TestParityMatrix$' -count=1 -parallel {{.PARITY_PARALLEL | default "2"}} -timeout 60m -v
coverage:parity-publish:
desc: Coverage of real feature/template publication against an ephemeral OCI registry
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for publish parity coverage
cmds:
- task: coverage:_parity-lane
vars:
LANE: publish
RUN: go test ./internal/cli -run '^TestPublishParity$' -count=1 -timeout 10m -v
coverage:merge:
desc: Union every per-lane covdata dir into the true cross-lane coverage report
cmds:
- go run ./cmd/devtool coverage-merge {{.COVER_DATA}} {{.COVER_DATA}}/merged "merged (all lanes)"
test:e2e:
desc: Run Go E2E tests with Docker
deps: [build]
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for E2E tests
cmds:
- go test ./internal/cli -run TestE2E -count=1 -timeout 5m -v {{.CLI_ARGS}}
lint:
desc: Run Go static checks
cmds:
- go vet ./cmd/... ./internal/...
spec:compliance:
desc: Verify spec compliance — schema attribute coverage + normative behavior (merge/lifecycle/discovery), excludes Go-only additions
cmds:
# All spec contracts are TestSpec* across packages: config attribute
# coverage + variable/discovery/devcontainerId behavior, imagemeta merge
# rules, lifecycle execution order.
- go test ./internal/... -run '^TestSpec' -count=1
spec:schema-update:
desc: Refresh the vendored devcontainer.json schema the spec-compliance gate checks against
cmds:
- curl -fsSL https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.base.schema.json -o internal/config/testdata/devContainer.base.schema.json
- 'echo "Schema refreshed. Run ''task spec:compliance'' — any new spec property fails until modeled in internal/config/types.go."'
reference:
desc: Install and compile the TypeScript parity oracle
cmds:
- task: reference:install
- task: reference:compile
parity:contract:
desc: Run the hermetic CLI contract matrix
deps: [build, reference:compile]
cmds:
# Oracle flag-surface gate: every flag the TS oracle declares per command
# must be in the inventory (needs reference/ checked out, so it lives here).
- go test ./internal/cli -run TestOracleFlagCoverage -count=1
- PARITY_REPORT_FILE={{.ROOT_DIR}}/artifacts/parity-contract.json PARITY_STRICT=true PARITY_LANE=contract PARITY_SKIP_DOCKER=true PARITY_SKIP_NETWORK=true go test ./internal/cli -run TestParityMatrix -count=1 -timeout 10m -v {{.CLI_ARGS}}
parity:semantic:
desc: Run semantic cases without Docker or network
deps: [build, reference:compile]
cmds:
- PARITY_REPORT_FILE={{.ROOT_DIR}}/artifacts/parity-semantic.json PARITY_STRICT=true PARITY_LANE=semantic PARITY_SKIP_DOCKER=true PARITY_SKIP_NETWORK=true go test ./internal/cli -run TestParityMatrix -count=1 -timeout 10m -v {{.CLI_ARGS}}
parity:network:
desc: Run parity cases that require external network access
deps: [build, reference:compile]
cmds:
- PARITY_REPORT_FILE={{.ROOT_DIR}}/artifacts/parity-network.json PARITY_STRICT=true PARITY_LANE=all PARITY_NETWORK_ONLY=true PARITY_SKIP_DOCKER=true go test ./internal/cli -run TestParityMatrix -count=1 -timeout 15m -v {{.CLI_ARGS}}
parity:runtime:
desc: Run the runtime parity matrix with Docker (set PARITY_COMMAND=up,build to scope it)
deps: [build, reference:compile]
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for runtime parity
# Fail fast (~1s) if the containerd image store is off: the runtime lane's
# update-UID / cache-export builds run `docker build --platform` against
# locally-built images, which only the containerd store can resolve. Without
# it the TS oracle fails those builds while Go succeeds → false divergences.
# The test itself re-checks this (so a direct `go test` is covered too).
- sh: docker info 2>/dev/null | grep -q io.containerd.snapshotter
msg: |
The parity runtime lane requires the containerd image store (docker build
--platform against locally-built images). Enable it with `task ci:prepare-runner`,
or add {"features":{"containerd-snapshotter":true}} to /etc/docker/daemon.json.
# Fail fast (~1s) if the builder can't export build cache — otherwise the
# runtime cases (build.cache-to-*, build.output-oci-*) fail ~4 min deep with
# a cryptic buildkit error. The default `docker` driver lacks cache export.
- sh: go run ./cmd/devtool docker-cache-export
msg: |
The Docker builder cannot export build cache (needed by the runtime parity
matrix: build.cache-to-*, build.output-oci-*). Enable the containerd image
store (`devtool ci-prepare-runner`) or use a docker-container buildx builder.
cmds:
# -parallel default 2 for gate determinism: higher values overload the docker
# daemon on contended runners and transiently fail compose/heavy cases (they
# match in isolation). Raise via PARITY_PARALLEL on beefy/idle runners.
# PARITY_COMMAND (comma-separated, inherited from the environment) scopes the
# run to the cases affected by a change; unset/"all" runs the full matrix.
- PARITY_REPORT_FILE={{.ROOT_DIR}}/artifacts/parity-runtime.json PARITY_STRICT=true PARITY_LANE=all go test ./internal/cli -run TestParityMatrix -count=1 -parallel {{.PARITY_PARALLEL | default "2"}} -timeout 60m -v {{.CLI_ARGS}}
parity:publish:
desc: Run the OCI publish parity (features publish/package) — network + registry heavy
deps: [build, reference:compile]
preconditions:
- sh: docker info >/dev/null 2>&1
msg: Docker is required for publish parity
cmds:
- go test ./internal/cli -run TestPublishParity -count=1 -timeout 10m -v
ci:prepare-runner:
desc: "CI-ONLY: free disk + enable the containerd image store (uses sudo; not for local dev)"
cmds:
- go run ./cmd/devtool ci-prepare-runner
clean:
desc: Remove generated artifacts and test containers
cmds:
- task: docker:clean
- rm -f {{.BINARY}} coverage.out coverage.html
- rm -rf artifacts dist src/test/tmp
release:
desc: Run GoReleaser; pass --snapshot for a local release
preconditions:
- sh: command -v goreleaser >/dev/null
msg: goreleaser is required
cmds:
- goreleaser release --clean {{.CLI_ARGS}}
metrics:
desc: "Perf/distribution metrics (non-gating): Go startup vs Node oracle + artifact sizes -> artifacts/metrics.json"
deps: [build]
vars:
ORACLE: reference/dist/spec-node/devContainersSpecCLI.js
cmds:
# Non-gating: never fail the caller. All measurement is best-effort.
- cmd: |
set -u
out={{.ROOT_DIR}}/artifacts/metrics.json
mkdir -p {{.ROOT_DIR}}/artifacts
bin={{.ROOT_DIR}}/{{.BINARY}}
oracle={{.ROOT_DIR}}/{{.ORACLE}}
runs="${METRICS_RUNS:-30}"
# Portable byte size of a file (Linux stat -c; BSD/mac stat -f fallback).
fsize() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || echo null; }
# Mean wall-clock ms for a command over N runs.
# Uses hyperfine when present (JSON mean, seconds->ms); otherwise a
# portable `date +%s%N` loop averaged over the runs.
have_hyperfine=0
command -v hyperfine >/dev/null 2>&1 && have_hyperfine=1
time_cmd() {
# args: label, then the command
_label="$1"; shift
if [ "$have_hyperfine" = "1" ]; then
_hf=$(hyperfine --warmup 3 --runs "$runs" --shell=none --export-json /dev/stdout "$*" 2>/dev/null \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const j=JSON.parse(d);process.stdout.write(String(j.results[0].mean*1000))}catch(e){process.stdout.write("null")}})' 2>/dev/null)
[ -n "$_hf" ] && echo "$_hf" || echo null
else
# warmup
"$@" >/dev/null 2>&1 || true
_start=$(date +%s%N)
_i=0
while [ "$_i" -lt "$runs" ]; do "$@" >/dev/null 2>&1 || true; _i=$((_i+1)); done
_end=$(date +%s%N)
# (end-start) ns / runs / 1e6 -> ms mean
awk -v s="$_start" -v e="$_end" -v n="$runs" 'BEGIN{printf "%.3f",(e-s)/n/1000000}'
fi
}
echo "metrics: timing tool = $([ "$have_hyperfine" = 1 ] && echo hyperfine || echo date-loop), runs=$runs"
go_version_ms=$(time_cmd go_version "$bin" --version)
# read-configuration no-op against a minimal image-based workspace
# (variable substitution only; no Docker involved).
ws=$(mktemp -d)
mkdir -p "$ws/.devcontainer"
printf '{"image":"mcr.microsoft.com/devcontainers/base:ubuntu"}\n' > "$ws/.devcontainer/devcontainer.json"
go_readcfg_ms=$(time_cmd go_readcfg "$bin" read-configuration --workspace-folder "$ws")
node_version_ms=null
if [ -f "$oracle" ] && command -v node >/dev/null 2>&1; then
node_version_ms=$(time_cmd node_version node "$oracle" --version)
else
echo "metrics: node oracle not found at $oracle (run 'task reference') — skipping Node A/B"
fi
rm -rf "$ws"
# Sizes: local binary + cross-compiled dist binaries + gzip-compressed size.
bin_size=$(fsize "$bin")
la_bin=null; la_gz=null; lr_bin=null; lr_gz=null
if [ -f {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-amd64 ]; then
la_bin=$(fsize {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-amd64)
la_gz=$(gzip -c {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-amd64 | wc -c | tr -d ' ')
fi
if [ -f {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-arm64 ]; then
lr_bin=$(fsize {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-arm64)
lr_gz=$(gzip -c {{.ROOT_DIR}}/dist/{{.BINARY}}-linux-arm64 | wc -c | tr -d ' ')
fi
[ "$la_bin" = null ] && echo "metrics: dist binaries absent — run 'task build:cross' for release sizes"
cat > "$out" <<EOF
{
"version": "{{.VERSION}}",
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"timing_tool": "$([ "$have_hyperfine" = 1 ] && echo hyperfine || echo date-loop)",
"runs": $runs,
"startup_ms": {
"go_version": $go_version_ms,
"go_read_configuration": $go_readcfg_ms,
"node_version": $node_version_ms
},
"sizes_bytes": {
"local_binary": $bin_size,
"linux_amd64_binary": $la_bin,
"linux_amd64_gzip": $la_gz,
"linux_arm64_binary": $lr_bin,
"linux_arm64_gzip": $lr_gz
}
}
EOF
echo "metrics: wrote $out"
cat "$out"
ignore_error: true
reference:install:
internal: true
dir: reference
status: [test -d node_modules]
cmds:
- yarn install --frozen-lockfile
reference:compile:
internal: true
deps: [reference:install]
dir: reference
sources: [src/**/*.ts, package.json, yarn.lock]
generates: [dist/spec-node/devContainersSpecCLI.js]
cmds:
- yarn compile
docker:clean:
internal: true
cmds:
- docker ps -q --filter "label=devcontainer.local_folder" | xargs -r docker rm -f 2>/dev/null || true
- docker ps -aq --filter "label=com.docker.compose.project" | xargs -r docker rm -f 2>/dev/null || true
silent: true