Repository navigation
Expand file tree
/
Copy pathTaskfile.yml
More file actions
663 lines (601 loc) · 30.7 KB
/
Copy pathTaskfile.yml
File metadata and controls
663 lines (601 loc) · 30.7 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
version: '3'
# Plain anodizer output under CI (GHA forces ANSI, splitting asserted headers).
env:
ANODIZER_COLOR: '{{if env "GITHUB_ACTIONS"}}never{{end}}'
tasks:
default:
desc: Show available tasks
cmds:
- task --list
_check:tmp-headroom:
internal: true
desc: "Precondition: when /tmp is tmpfs, require it under 50% full — tmpfs pressure OOMs the cargo linker."
preconditions:
# The pressure scenario only applies when /tmp is backed by tmpfs
# (agent sandbox host). macOS, Windows, and GitHub Actions runners
# all back /tmp with real disk; on those the linker can't OOM from
# /tmp pressure, so skip the check entirely.
- sh: '[ -n "${ANODIZER_STRUCTURAL_WALK:-}" ] || [ "$(uname)" != "Linux" ] || [ "$(stat -f -c %T /tmp 2>/dev/null)" != "tmpfs" ] || [ "$(df --output=pcent /tmp | tail -1 | tr -dc 0-9)" -lt 50 ]' # portable-ok: reached only on Linux, after the uname check
msg: "/tmp is over 50% full — cargo will OOM under tmpfs RAM pressure. Run `du -sh /tmp/*/ | sort -hr | head` to find offenders and clean them. Write all scratch / debug / download files to `/opt/repos/.det-debug/` or `~/.cache/`, NEVER `/tmp`."
_check:mem-headroom:
internal: true
desc: "Precondition: at least 8 GB of memory available on Linux — rustdoc over this workspace holds ~7 GB and the kernel OOM-kills the largest process on the host."
vars:
# MemAvailable is reported in kB; the minimum below is 8 GB = 8388608 kB.
# Only Linux publishes /proc/meminfo, so macOS and Windows skip both legs;
# GitHub's Linux runners carry ~16 GB, so CI's rustdoc job evaluates the
# minimum and clears it.
MEM_AVAILABLE_KB:
sh: awk '/^MemAvailable:/ { print $2 }' /proc/meminfo 2>/dev/null || true
preconditions:
# Without this leg an unreadable /proc/meminfo leaves the minimum test below
# as `[ "" -ge 8388608 ]` — a shell error go-task reports with the minimum's
# message, telling a host with a fine amount of memory that it is short.
- sh: '[ -n "${ANODIZER_STRUCTURAL_WALK:-}" ] || [ "$(uname)" != "Linux" ] || printf %s "{{.MEM_AVAILABLE_KB}}" | grep -qE "^[0-9]+$"'
msg: "Cannot read MemAvailable from /proc/meminfo, so the 8388608 kB (8 GB) minimum cannot be checked. `task doc` is the rustdoc gate for `task gate` / `task push` and for CI's rustdoc job; rustdoc over this workspace holds ~7 GB, so it is refused."
- sh: '[ -n "${ANODIZER_STRUCTURAL_WALK:-}" ] || [ "$(uname)" != "Linux" ] || [ "{{.MEM_AVAILABLE_KB}}" -ge 8388608 ]'
msg: "MemAvailable is {{.MEM_AVAILABLE_KB}} kB, under the 8388608 kB (8 GB) minimum: rustdoc over this workspace holds ~7 GB and the kernel OOM-kills the largest process on the host. `task doc` is the rustdoc gate for `task gate` / `task push` and for CI's rustdoc job; free memory and re-run."
build:
desc: Build all default workspace members in release mode
deps: [_check:tmp-headroom]
sources:
- "crates/**/src/**/*.rs"
- "Cargo.toml"
- "Cargo.lock"
generates:
- "target/release/anodizer"
cmds:
- cargo build --release
check:
# Two passes, because one hides the other's failure. `--workspace` alone
# builds lib targets under default features, which is the only way to catch
# a feature-gated item used without its gate — `--all-targets` pulls in the
# crate's own dev-dependencies and unifies the feature back on, so the break
# compiles clean there and fails only for a downstream consumer.
desc: "Fast type-check: default features, then all targets"
deps: [_check:tmp-headroom]
cmds:
- cargo check --workspace
- cargo check --all-targets
check:determinism:
desc: "Byte-verify this host's native release artifacts reproduce (CI-exact)."
deps: [_check:tmp-headroom]
env:
# Reproducibility must be proven by real compiles, never served from a
# compile cache; empty overrides any global rustc-wrapper config.
RUSTC_WRAPPER: ""
cmds:
# Mirror anodizer-action's per-shard determinism command
# (scripts/determinism/run-harness.sh): --require-tools so a missing
# host-native producer HARD-FAILS instead of warn-skipping (the false
# coverage that once hid installer formats), and the host's own target
# subset (a Linux box cannot cross-build the darwin/windows partitions).
# The CSV is derived from `anodizer targets` — the same source
# resolve-targets.sh filters — so it never drifts from the release matrix.
- |
set -eu
case "{{OS}}" in
linux) runner=ubuntu-latest ;;
darwin) runner=macos-latest ;;
windows) runner=windows-latest ;;
*) echo "check:determinism: unsupported host OS '{{OS}}'" >&2; exit 1 ;;
esac
csv=$(cargo run -q -p anodizer --release -- targets --json \
| jq -r --arg os "$runner" '(if type=="object" then .include else . end)|[.[]|select(.os==$os)|.target]|join(",")')
[ -n "$csv" ] || { echo "check:determinism: no targets for runner $runner" >&2; exit 1; }
echo "check:determinism (CI-exact): runner=$runner targets=$csv"
cargo run -p anodizer --release -- check determinism \
--require-tools --runs=2 --targets="$csv" --preserve-dist=./preserved-dist
check:native-object-determinism:
# aws-lc-sys / ring / zstd-sys / lzma-sys / blake3 must stay byte-identical
# under clang-cl across rebuilds, ignoring the benign COFF TimeDateStamp.
# Windows-only: clang-cl C-codegen determinism has no equivalent elsewhere,
# so this no-ops on other hosts rather than failing them.
desc: "Guard: native C objects rebuild byte-identically under clang-cl (Windows-only)."
cmds:
- |
set -eu
if [ "{{OS}}" != "windows" ]; then
echo "check:native-object-determinism: skip — windows-only guard (this host is {{OS}})."
exit 0
fi
bash scripts/native-object-determinism.sh
check:msrv:
# `rust-version` is only a claim until something builds on it. Stable CI
# never notices a dependency whose own minimum moved above the declared one;
# this check resolves the locked graph with exactly that toolchain.
desc: "Guard: the locked workspace builds on the declared rust-version (mirrors the CI msrv job)."
cmds:
- |
set -eu
msrv="$(sed -n 's/^rust-version = "\(.*\)"/\1/p' Cargo.toml)"
[ -n "$msrv" ] || { echo "check:msrv: no rust-version line in Cargo.toml" >&2; exit 1; }
rustup toolchain install "$msrv" --profile minimal
cargo "+$msrv" check --workspace --all-targets --locked
check:config:
desc: "Validate this repo's anodizer config (single-crate, post-restructure form)."
deps: [_check:tmp-headroom]
cmds:
# No --workspace: this repo is single-crate (no `workspaces:` block), so
# `--workspace=.` would error ("no workspaces defined") after validating.
- cargo run -p anodizer --release -- check config
release:snapshot:
# FULL pre-push gate (canonical). Unlike `task snapshot` (slim, single
# target, dry-run — the fast `task commit` gate), this runs a REAL build
# of every host-buildable target plus the whole snapshot pipeline
# (archive/sign/checksum/sbom) and the emission-validate stage — so a
# broken binstall/nix/version-sync emission is caught locally before push.
# `--host-targets` drops only the targets this host cannot cross-compile
# (apple targets off a non-macOS host) so the real build never aborts;
# the dropped triples are logged loudly. Snapshot mode auto-skips
# publish/snapcraft-publish/blob/announce, so this is "everything this
# host can build, minus publish/announce". `task prepush` aliases this so
# there's one source of truth for the flag set.
desc: "REAL build of every host-buildable target, through emission-validate. Slow."
deps: [_check:tmp-headroom]
cmds:
- cargo run -p anodizer --release -- release --snapshot --clean --host-targets --summary-json=dist/snapshot-summary.json
prepush:
# Cross-platform evidence run — everything `task gate` cannot prove from this
# box alone: a REAL build of every host-buildable target + emission-validate
# (release:snapshot) and the workspace suite on every reachable OS (test:os).
# Run it before a release push in addition to `task gate` (the CI-job mirror).
desc: "Cross-platform evidence: real build + the suite on every reachable OS. Complements gate."
cmds:
- task: release:snapshot
- task: test:os
summary:show:
desc: "Pretty-print the most recent run summary."
cmds:
- jq . dist/run-*/summary.json | less
fmt:
desc: Format all code
cmds:
- cargo fmt
fmt:check:
desc: Check formatting without modifying files
cmds:
- cargo fmt -- --check
clippy:
desc: Run clippy with warnings as errors (default members)
deps: [_check:tmp-headroom]
cmds:
- cargo clippy --all-targets -- -D warnings
test:
desc: Run all tests (default members, incl. doctests)
# The go-task-less fallback in .claude/scripts/test-os-suite.sh hand-mirrors
# these cargo passes for validation hosts that lack go-task; a pass added
# here must be added there too. audit-workflow-lockstep fails CI on drift.
deps: [_check:tmp-headroom]
cmds:
- cargo nextest run --workspace --no-fail-fast
# nextest never runs doctests; without this second pass a broken rustdoc
# example would ship through every gate that runs the workspace suite.
- cargo test --workspace --doc
test:lib:
desc: Run unit tests only (no integration/doc tests)
deps: [_check:tmp-headroom]
cmds:
- cargo nextest run --workspace --no-fail-fast --lib
test:brew-style:
# `brew style` needs a working Homebrew with its style gems, which only
# the macOS hosts have; the Linux box runs brew as root, which brew
# refuses. The test itself is opt-in through the variable, so `task test`
# never depends on a brew install. The host check sits in the command:
# `platforms:` would hide the target from `task -n gate`, and so from the
# gate-mirror audit.
desc: "Run brew style over the rendered Homebrew casks (macOS only)."
env:
ANODIZER_BREW_STYLE: "1"
cmds:
- |
set -eu
if [ "{{OS}}" != "darwin" ]; then
echo "test:brew-style: skip — macOS-only check (this host is {{OS}})."
exit 0
fi
cargo test -p anodizer-stage-publish --lib rendered_casks_pass_brew_style_opt_in -- --nocapture
test:os:
# Ships HEAD as a git bundle, so it proves the un-pushed commit rather than
# whatever origin holds. The local leg always runs; the Windows and macOS
# legs run only when TEST_OS_WINDOWS_HOST / TEST_OS_MACOS_HOST name an ssh
# target, so a contributor with neither still gets a useful local gate.
# Unconfigured and unreachable legs skip loudly; a reachable leg that fails
# fails the task.
desc: "Run the suite locally plus on any configured Windows/macOS ssh hosts."
# The local leg runs the workspace suite via test-os-suite.sh (nextest where
# present, else cargo test); guard tmpfs headroom like every other cargo
# task so a standalone `task test:os` can't OOM the linker.
deps: [_check:tmp-headroom]
cmds:
- bash .claude/scripts/test-os.sh
# gen-docs reads the clap tree and the schema in-process (`anodizer_cli::build_cli()`
# via xtask's path dependency on crates/cli), so it never executes the binary — a
# `deps: [build]` here bought nothing and actively overwrote target/release/anodizer,
# clobbering a binary a caller had put there deliberately (docs.yml reuses the
# released one rather than recompiling an artifact the release already produced).
docs:
desc: Generate reference docs (CLI + configuration)
deps: [_check:tmp-headroom]
cmds:
- cargo xtask gen-docs
docs:check:
desc: Verify generated docs are up-to-date
deps: [_check:tmp-headroom]
cmds:
- cargo xtask gen-docs --check
docs:validate-readme:
desc: Validate anodizer YAML config blocks in README.md against the current schema
cmds:
- cargo xtask validate-readme
docs:site:
desc: Build the docsite with zola, failing on any unresolved shortcode or broken link.
# zola expands `{{ name(k=v) }}` in prose as a SHORTCODE call before markdown
# is parsed, so a template example whose arguments are all literals aborts the
# build on an unknown name — code fences and backticks give no protection.
# Deploy Docs only fires after a Release that actually cut a version, so
# without this target such a break stays invisible until release day.
dir: docs/site
preconditions:
# One pinned Zola for CI and for a laptop: `.zola-version` is what
# .github/actions/setup-docs installs, so a local build that differs
# proves nothing about what the deploy will render. Zola 0.23 moved to
# Tera 2, which parses `{{ … }}` inside markdown prose — the syntax 100
# of these pages document — so the exact version matters.
- sh: test "$(zola --version | awk '{print $2}')" = "$(tr -d '[:space:]' < ../../.zola-version)"
msg: >-
zola version mismatch: this repo pins the version in .zola-version and
your `zola --version` differs. Install the pinned version before
building the docsite.
cmds:
- zola build
package:
desc: Dry-run package all publishable crates
deps: [_check:tmp-headroom]
cmds:
- cargo package --workspace --no-verify --allow-dirty --exclude xtask
snapshot:
# SLIM, fast gate wired into `task commit` (via `task lint`). Single target,
# dry-run (compiles nothing) — kept fast on purpose. For the thorough
# pre-push check (a REAL build of every host-buildable target +
# emission-validate), use `task prepush` / `task release:snapshot`.
desc: "Slim commit gate: dry-run pipeline, single target, no publish. Fast."
deps: [build]
cmds:
- ./target/release/anodizer release --snapshot --single-target --clean --dry-run
clean:
desc: Remove build artifacts and dist directory
cmds:
- cargo clean
- rm -rf dist/
coverage:check:
desc: Run workspace test coverage and write cobertura.xml. No branch switching, no push.
cmds:
- cargo llvm-cov nextest --cobertura --output-path cobertura.xml
- 'echo "Coverage (line, cobertura): $(bash .claude/scripts/coverage-percent.sh cobertura.xml)"'
coverage:publish:
# Switches branches via the badge script — CI only, never on a dev box.
# Composes coverage:gate rather than respelling the llvm-cov invocation: two
# copies of the same measurement command drift, and a badge measured with
# different flags than the gate is a badge that lies about the gate.
desc: "Measure coverage, enforce the minimum, publish the badge to origin/badges."
cmds:
- task: coverage:gate
- bash .claude/scripts/publish-coverage.sh cobertura.xml
coverage:gate:
# Full output ends up in target/coverage-gate.log. The minimum lives in
# coverage-gate.sh and nowhere else; override per-run with COVERAGE_MINIMUM.
desc: "Run the instrumented suite and fail below the coverage minimum."
deps: [_check:tmp-headroom]
cmds:
# The instrumented run spews thousands of compile/test lines; keep the
# terminal to percent + verdict and park the rest in a logfile.
- mkdir -p target
- |
if ! cargo llvm-cov nextest --cobertura --output-path cobertura.xml > target/coverage-gate.log 2>&1; then
echo "instrumented test run failed — tail of target/coverage-gate.log:" >&2
tail -n 40 target/coverage-gate.log >&2
exit 1
fi
- bash .claude/scripts/coverage-gate.sh cobertura.xml
tidy:
desc: Format code and regenerate docs
cmds:
- task: fmt
- task: docs
audit:deps:
desc: "Guard: dependency tree carries no unaddressed RUSTSEC advisory (mirrors the CI cargo-audit job; honors .cargo/audit.toml ignores)."
cmds:
- cargo audit
audit:workflows:
# Mirrors ci.yml's `actionlint` job, which runs actionlint plus four audit
# scripts. Keep this list equal to that job's steps: JOB_MIRROR maps whole
# jobs to whole tasks, so a step present in the job and absent here is drift
# the gate-mirror audit cannot see, and CI goes red after a passing gate.
desc: "Guard: workflows pass actionlint and the four workflow audits (mirrors the CI actionlint job)."
cmds:
- actionlint -color
- task: audit:job-timeouts
- task: audit:gate-mirror
- task: audit:workflow-lockstep
- task: audit:doc-source-links
audit:workflow-lockstep:
# Covers the shard roster, publish secret env, trigger gate, release/nightly
# mutex, bootstrap artifact, and post-release gate.
desc: "Guard: hand-synced copies across workflow files stay in lockstep."
cmds:
- .claude/scripts/audit-workflow-lockstep.sh
audit:log:
desc: "Guard: no subprocess command-echo at default log verbosity (status vs verbose)."
cmds:
- .claude/scripts/audit-log-status.sh
audit:tag-family:
desc: "Guard: a crate's tag family is read through CrateConfig::tag_family_template(), never the raw field."
cmds:
- .claude/scripts/audit-tag-family.sh
audit:binary-name:
desc: "Guard: an artifact's binary name is read only through Artifact::binary_name (.claude/scripts/audit-binary-name.sh)."
cmds:
- .claude/scripts/audit-binary-name.sh
audit:test-isolation:
desc: "Guard: no unjustified process-global env mutation in tests (serial/env-ok)."
cmds:
- .claude/scripts/audit-test-isolation.sh
audit:log-labels:
desc: "Guard: Warning/Error/Note status labels come only from the log module (no open-coding)."
cmds:
- .claude/scripts/audit-log-labels.sh
audit:test-spawn:
desc: "Guard: TEST-context git/node spawns route through output_with_spawn_retry (Windows nextest flake)."
cmds:
- .claude/scripts/audit-test-spawn-retry.sh
audit:test-exec-writer:
desc: "Guard: TEST-context executable writes route through write_executable_script (ETXTBSY flake)."
cmds:
- .claude/scripts/audit-test-exec-writer.sh
audit:test-live-host:
desc: "Guard: no test drives a publisher entry point without binding a loopback endpoint (live-registry reach)."
cmds:
- .claude/scripts/audit-test-live-host.sh
audit:prose:
desc: "Guard: no comment speaks in the first person, the tool is spelled anodizer, and no script count puts its plural in parentheses."
cmds:
- .claude/scripts/audit-prose.sh
audit:job-timeouts:
desc: "Guard: every .github/workflows job declares a positive-integer timeout-minutes (reusable-workflow callers exempt)."
cmds:
- .claude/scripts/audit-job-timeouts.sh
audit:repo-identity:
desc: "Guard: owner/repo slug and GitHub token resolve through one canonical path."
cmds:
- .claude/scripts/audit-repo-identity.sh
audit:gate-mirror:
desc: "Guard: task gate mirrors every blocking ci.yml job (fails when a CI job has no registered local equivalent)."
cmds:
- .claude/scripts/audit-gate-mirror.sh
audit:doc-source-links:
desc: "Guard: every crates/**.rs path cited in the docsite exists (god-file splits silently break these)."
cmds:
- .claude/scripts/audit-doc-source-links.sh
audit:god-files:
desc: "Guard: no source file carries more than 1000 lines of PRODUCTION code (test code never counts)."
cmds:
- .claude/scripts/audit-god-files.sh
audit:code:
# The convention guards, as one unit. They exist to hold a line no compiler
# checks, so every gate that can run them must run all of them — a guard
# wired into one entry point and not another is enforcement by luck.
desc: "Guard bundle: source and doc conventions."
cmds:
- task: audit:log
- task: audit:log-labels
- task: audit:tag-family
- task: audit:binary-name
- task: audit:test-isolation
- task: audit:test-spawn
- task: audit:test-exec-writer
- task: audit:test-live-host
- task: audit:prose
- task: audit:repo-identity
- task: audit:god-files
- task: audit:serial-groups
- task: audit:path-stub-serial
- task: audit:test-dead-port
- task: audit:script-portability
audit:script-portability:
desc: "Guard: no script the macOS CI job runs uses a GNU-only grep, sed, readlink, date or stat option."
cmds:
- .claude/scripts/audit-script-portability.sh
audit:test-dead-port:
desc: "Guard: no test binds a listener and releases it to get a 'nothing listens here' address (the freed port is reused by parallel tests)."
cmds:
- .claude/scripts/audit-test-dead-port.sh
audit:path-stub-serial:
desc: "Guard: every test that swaps the process PATH takes #[serial(path_env)] (one key, so readers serialise against every mutator)."
cmds:
- .claude/scripts/audit-path-stub-serial.sh
audit:path-stub-readers:
# Runs every test of every PATH-stubbing test binary alone, so it belongs
# beside the test run in `task gate` and not in the commit-time bundle.
desc: "Guard: a test that spawns a tool its own test binary stubs on PATH takes #[serial(path_env)]. Slow: builds and runs the tests."
cmds:
- .claude/scripts/audit-path-stub-readers.sh
audit:serial-groups:
desc: "Guard: every #[serial] names its group (an unkeyed #[serial] does not serialise against any keyed one)."
cmds:
- .claude/scripts/audit-serial-groups.sh
doc:
# rustdoc's intra-doc link resolution is the only checker for `[`Foo`]`
# references; nothing else in the toolchain reads them. The lint is
# warn-by-default and no commit-time chain runs `cargo doc`, so a broken
# link is invisible until a reader clicks dead text on the rendered docs.
# `--document-private-items` widens the check to private items' doc
# comments too — that is where the god-file splits do their damage, since
# a moved item keeps its name and loses its path.
# `private_intra_doc_links` is a separate lint for the inverse case: a
# public item linking to a private one. It resolves locally and passes the
# broken-link check, then renders as dead text on docs.rs, where the target
# is not published.
# The other three catch prose that only breaks once rendered: an unclosed
# `<email>` / `<label>` is parsed as an HTML tag and swallows the text
# after it, a bare URL renders as unclickable plain text, and a redundant
# explicit target hides the fact that the short form already resolved.
desc: "CI's rustdoc gate: every rustdoc link resolves and renders, all rustdoc lints denied. Runs in CI (`rustdoc` job), `task gate` and so `task push`; never `task commit`. Refuses to start on Linux under 8 GB available."
deps: [_check:tmp-headroom, _check:mem-headroom]
env:
RUSTDOCFLAGS: >-
-D rustdoc::broken_intra_doc_links
-D rustdoc::private_intra_doc_links
-D rustdoc::invalid_html_tags
-D rustdoc::bare_urls
-D rustdoc::redundant_explicit_links
cmds:
- cargo doc --workspace --no-deps --document-private-items
lint:
desc: Local pre-commit check (tidy, clippy, build, snapshot dry-run)
cmds:
- task: tidy
- task: clippy
- task: build
- task: snapshot
- task: audit:code
- task: audit:workflows
- task: check:windows-tests
check:windows-tests:
# A test helper gated to unix while a test that calls it is not compiles
# on Linux and macOS and breaks only the Windows CI job. The GNU target
# type-checks the same cfg(windows) code with mingw, which this host can
# install, where the MSVC target needs a C compiler for ring it cannot.
desc: "Type-check every test target for Windows (x86_64-pc-windows-gnu); skips when the target or mingw is missing."
cmds:
- |
set -eu
if ! rustup target list --installed 2>/dev/null | grep -qx x86_64-pc-windows-gnu \
|| ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
echo "check:windows-tests: skip — needs the x86_64-pc-windows-gnu target and x86_64-w64-mingw32-gcc (rustup target add x86_64-pc-windows-gnu; apt install gcc-mingw-w64-x86-64)."
exit 0
fi
cargo check --workspace --tests --target x86_64-pc-windows-gnu
ci:
desc: Run all CI checks (everything except cross-platform builds and the rustdoc gate, which CI's rustdoc job and task gate run)
cmds:
- task: fmt:check
- task: clippy
- task: test
- task: docs:check
- task: package
gate:
# Canonical pre-push / pre-release gate: this MUST run every job that blocks
# release.yml (i.e. every ci.yml job). release.yml triggers on the whole CI
# workflow succeeding, so any ci.yml job that is not mirrored here is a hole
# through which a red CI slips to a push. When a ci.yml job is added, add its
# local equivalent here (enforced by audit:gate-mirror). Slow by design
# (the instrumented coverage run dominates); the snapshot step is the same
# dry-run CI runs. `task ci` remains the fast inner-loop subset.
desc: "Pre-push gate: every blocking CI job, mirrored locally. Slow."
cmds:
- task: ci # fmt:check, clippy, test, docs:check, package
- task: test:brew-style # test (the macOS leg's brew style step; no-ops elsewhere)
- task: doc # rustdoc
- task: audit:deps # cargo-audit
- task: docs:validate-readme # validate-readme
- task: check:config # check-config
- task: check:msrv # msrv
- task: check:native-object-determinism # native-object-determinism (windows-only; no-ops elsewhere)
- task: snapshot # snapshot (dry-run)
- task: audit:workflows # actionlint
- task: audit:code # convention guards (no CI job)
- task: audit:path-stub-readers # measured PATH readers (no CI job)
- task: coverage:gate # coverage minimum
commit:
desc: "Lint, restage only your intended files, commit. Usage: task commit -- -m 'msg'"
preconditions:
- sh: git diff --cached --quiet || test -n "$(git diff --cached --name-only)"
msg: "Nothing staged. Stage files with git add before running task commit."
vars:
STAGED_PRE:
sh: git diff --cached --name-only
MODIFIED_PRE:
sh: git diff --name-only
cmds:
- task: lint
- task: _commit:restage
vars:
STAGED_LIST: "{{.STAGED_PRE}}"
MODIFIED_LIST: "{{.MODIFIED_PRE}}"
- git commit {{.CLI_ARGS}}
push:
desc: "Run task gate (rustdoc, coverage, every CI job), then push. Usage: task push [-- <git push args>] (default: origin master)."
preconditions:
# Uncommitted changes must not ride along on a push decision — gate the
# coverage run and the push on a clean tree.
- sh: git diff --quiet && git diff --cached --quiet
msg: "Working tree is dirty — commit or stash before task push."
cmds:
# Fetch + divergence check first: a CI-pushed release bump commit is
# rebased in automatically; any other divergence fails here, before
# the long gate spends its time.
- .claude/scripts/push-preflight.sh
- task: gate
- git push {{.CLI_ARGS | default "origin master"}}
_commit:restage:
internal: true
desc: "Internal: re-stage files that were staged at task start (capture lint touchups) plus any files lint NEWLY modified (auto-generated content). Files modified-but-unstaged at task start are left untouched so they don't contaminate the commit."
cmds:
- |
# (a) Re-add files that were staged pre-task. Use -A so deletes and
# renames staged with `git mv` aren't lost when lint touches them.
# Tolerate already-staged deletions: in git 2.53+, `git add -A` on a
# path that is only present as a staged deletion (no working-tree
# entry, no untracked entry, only an index removal) errors with
# "pathspec did not match any files". The deletion is already in
# the index from the user's pre-stage, so skipping it is harmless.
while IFS= read -r f; do
[ -z "$f" ] && continue
if [ -e "$f" ] || git ls-files --error-unmatch -- "$f" >/dev/null 2>&1; then
git add -A -- "$f"
fi
done <<'STAGED_EOF'
{{.STAGED_LIST}}
STAGED_EOF
# (b) Add files that lint NEWLY modified — i.e., not in pre-task
# staged OR modified lists. These are typically docs/snapshots
# regenerated by the lint step and belong with the commit.
#
# That inference only holds when the staged set is the ONLY source
# lint could have regenerated from. If the worktree also carries
# unstaged edits, the generator read those too, and adding its output
# commits derived content whose source is being left behind — a tree
# whose own `gen-docs --check` fails, discovered later by a bisect.
# Which edits belong is a judgement only the author can make, so this
# stops rather than guessing.
# Emitted by a function rather than accumulated into a variable: the
# list is needed twice (to test, then to add), and appending inside
# this indented block would carry the indentation into the filenames.
regenerated() {
git diff --name-only | while IFS= read -r f; do
[ -z "$f" ] && continue
printf '%s\n' '{{.STAGED_LIST}}' | grep -qxF -- "$f" && continue
printf '%s\n' '{{.MODIFIED_LIST}}' | grep -qxF -- "$f" && continue
printf '%s\n' "$f"
done
}
if [ -n "$(regenerated)" ] && [ -n '{{.MODIFIED_LIST}}' ]; then
echo "task commit: lint regenerated files while unstaged edits are present." >&2
echo >&2
echo "regenerated by lint (would be added to this commit):" >&2
regenerated | sed 's/^/ /' >&2
echo >&2
echo "unstaged when this task started (a possible source of that output):" >&2
printf '%s\n' '{{.MODIFIED_LIST}}' | sed 's/^/ /' >&2
echo >&2
echo "Stage the edits that belong with this commit, or move the rest" >&2
echo "aside, then re-run. Committing as-is would commit generated output" >&2
echo "ahead of the source that produces it, leaving a tree whose own" >&2
echo "'cargo xtask gen-docs --check' fails." >&2
exit 1
fi
regenerated | while IFS= read -r f; do
[ -z "$f" ] && continue
git add -- "$f"
done