Skip to content

feat: Introduce cargo-ox-check for unified Rust build/CI scaffolding#33

Draft
martin-kolinek wants to merge 119 commits into
unified-buildsfrom
unified-builds-impl
Draft

feat: Introduce cargo-ox-check for unified Rust build/CI scaffolding#33
martin-kolinek wants to merge 119 commits into
unified-buildsfrom
unified-builds-impl

Conversation

@martin-kolinek
Copy link
Copy Markdown
Collaborator

@martin-kolinek martin-kolinek commented May 27, 2026

Introduces cargo-ox-check: a cargo subcommand that ships and maintains an opinionated, unified build/CI scaffolding for Rust workspaces. One cargo ox-check update invocation emits a complete, reviewable tree of GitHub Actions workflows, Azure DevOps pipelines, justfiles/ox-check/ recipes, and managed regions in Cargo.toml / deny.toml / rustfmt.toml / .delta.toml. Subsequent runs keep the tree in sync as the tool's catalog evolves, while preserving any local customizations the adopter has made.

Why this exists

Today every Rust repo in the oxidizer / oxidizer-github / ox-tools family hand-rolls its own CI on the same opinionated checks (fmt, clippy, deny, audit, miri, careful, mutants, llvm-cov, cargo-hack, semver-check, doc, ...). The cost of keeping those copies in sync — toolchain bumps, lint updates, new advisory feeds, cross-OS matrix changes — is paid per-repo, by hand, every time. cargo-ox-check centralizes the catalog in one place and renders it into the per-repo CI surface.

What an adopter gets

After one cargo ox-check update, the repo contains:

  • .github/workflows/ox-check-pr.yml / ox-check-pr-impl.yml + composite actions for the PR tier (pr-fast, pr-test, pr-mutants).
  • .github/workflows/ox-check-nightly.yml / ox-check-nightly-impl.yml + composite actions for the nightly tier (nightly-test, nightly-advisories, nightly-runtime, nightly-exhaustive).
  • .pipelines/ox-check/ mirror of the same wiring for Azure DevOps.
  • justfiles/ox-check/ recipe tree (checks.just, groups.just, tiers.just, tools.just, mod.just, tool-minimums.txt) — local just ox-check-pr reproduces the PR CI without any ox-check binary present.
  • Managed regions spliced into Cargo.toml (opinionated workspace + per-crate lints), rustfmt.toml, deny.toml, .delta.toml.
  • A sidecar .ox-check.lock manifest that tracks per-file/per-region checksums for the three-checksum update algorithm.

Design highlights

  • Three impact tiers (modified, affected, required) backed by cargo-delta. Each check is tagged with the tier it scopes to; the CI wiring emits one include-list env var per tier with a --skip sentinel for empty tiers. The required tier is included for tools whose correctness resolves through the dep graph (cargo doc, cargo hack, cargo udeps); the unscoped bucket is reserved for Cargo.lock- and PR-context-only checks (deny, audit, aprz, pr-title).
  • Three update decisions (Write, LeaveAlone, Propose) keyed on a three-way comparison of (original-template, current-template, live-file) checksums, all tracked in the lock manifest. A user edit inside a managed region is preserved when the template hasn't moved; a template bump becomes a .ox-check-proposed sidecar with a one-line summary, so customizations are never silently overwritten.
  • Backend autodetection from origin's URL; explicit --backend github / --backend ado override.
  • Cross-OS matrix defaults that match the surveyed-repo evidence. GitHub default: Linux/Windows × x86_64/aarch64 (4 legs, using GH's hosted ARM runners). ADO default: Linux + Windows x86_64 (ADO has no hosted ARM agents; adopters with self-hosted ARM pools extend in their root pipeline). Compile-sensitive checks (clippy, doc-build, udeps, semver-check, external-types, mutants, cargo-hack, bench, miri, careful) all matrix; text/metadata checks ride along on the same matrix to keep group definitions stable.
  • Caching (GitHub actions/cache, ADO Cache@2) keyed on OS + arch + rustc version + lockfile-family hashes + tool-minimums.txt hash, so toolchain bumps and catalog updates invalidate cleanly.
  • Self-validation gate via .github/workflows/regenerate-check.yml — the one hand-written workflow that builds cargo-ox-check from the PR's branch, runs cargo ox-check update --dry-run, and fails the PR if the in-tree state drifts from what the templates would render.

Verification

  • 157 unit tests in crates/cargo_ox_check/src/ (run/plan/decision/region/emit/workspace/manifest/checksum).
  • 4 schema tests in tests/schemas.rs (every emitted TOML parseable; manifest schema valid).
  • 3 snapshot tests in tests/snapshots.rs covering the three representative backend combinations (local-only, GitHub-backend, ADO-backend) — full byte-exact emitted tree.
  • 4 fixture tests in tests/update.rs covering single-crate (no [workspace]), opt-outs (emptied managed region preserved), customized (user edit inside a managed region preserved), migration (pre-existing hand-written Justfile / deny.toml / [profile.release] survive splicing).
  • cargo clippy -p cargo-ox-check --all-targets -- -D warnings clean.
  • cargo-delta CLI and JSON shape verified by running the actual binary against this workspace; the impact-step shell scripts encode the real two-snapshot --baseline / --current flow.

Total: 168 tests green locally.

Documentation

Lives under crates/cargo_ox_check/docs/:

  • design/design.md — top-level design and CLI shape.
  • design/local.md — the just recipe tree, impact env vars, daily-driver flow.
  • design/github.md — owned reusable workflows, per-group composite actions, impact scoping.
  • design/ado.md — owned stages templates, per-group step templates, 1ESPT composition guidance.
  • design/checks.md — the opinionated catalog and per-group OS scope matrix.
  • design/updates.md — the three-checksum state machine validated by fixture tests.
  • verification.md — continuous-validation strategy (dogfooding + snapshot + fixtures + schema).
  • implementation-plans/0000.md — initial implementation plan (history).
  • implementation-plans/0001.md — design-vs-implementation reconciliation plan executed in this PR.

Follow-ups (not in this PR)

  • Actually run cargo ox-check update against ox-tools to land the emitted CI surface (the regenerate-check workflow will start passing the moment the maintainer commits that bootstrap output).
  • pr-title Conventional Commits regex hasn't been exhaustively validated against edge cases (scoped, breaking-change, mixed-case).
  • 1ESPT compliance composition for ADO pipelines is intentionally out of scope — ox-check emits composable stages, not a 1ESPT extender. Adopters compose the stages template into their compliance pipeline.

Martin Kolinek (from Dev Box) and others added 30 commits May 14, 2026 12:51
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires clap (derive) with the single 'update' subcommand and flags --backend (repeatable), --no-backends (mutually exclusive with --backend), and --dry-run. Adds anyhow, thiserror, tracing, tracing-subscriber deps. main.rs strips the 'ox-check' token cargo injects for subcommand binaries. run_update is a no-op that logs its inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the backend module with Backend enum (GitHub, Ado), URL parsing (https/scp/ssh forms), git-config invocation for origin, and a resolve() function that implements the CLI resolution order: --no-backends > --backend > autodetect. Unit tests cover URL parsing edge cases, name parsing, and resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the workspace module: find_workspace_root() walks up from a start path to the nearest [workspace] ancestor (falling back to the nearest [package] for single-crate repos), and load_workspace() parses the manifest with toml_edit and resolves members. Supports literal member entries and 'crates/*'-style trailing globs (the only form observed in surveyed repos). Adds toml_edit to workspace deps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the manifest module implementing the sidecar file documented in updates.md. Manifest::load() returns Manifest::default() for missing files. Manifest::save() writes atomically via temp+rename. to_toml() serializes deterministically with sorted entries (BTreeMap-backed), always ending in a trailing newline so diffs are minimal. Rejects schema versions newer than 1; missing/duplicate entries are hard errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds checksum and decision modules. checksum_bytes/checksum_str return the canonical 'sha256:<hex>' string used throughout the codebase. decide() implements the table from updates.md section 5 with five outcomes: InSync, Skipped (opted out), Write, Propose, LeaveAlone. Includes should_emit_proposed_for_opt_out() to handle the empty-stub case where we still proactively surface upstream churn. Adds sha2 to workspace deps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the region module: find_region(), upsert_region(), and render_region() implement the sentinel-delimited managed-region machinery for any host file. Supports two comment syntaxes (# for Justfile/TOML/YAML and // for future hosts). Recognizes indented sentinels (needed for YAML). Detects malformed regions (duplicate opener, missing closer, closer-before-opener) and reports clean errors. Treats whitespace-only bodies as empty (= opt-out signal).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the plan module: PlanItem encapsulates one accumulated decision plus the rendered content (or spliced host body for regions). Plan::apply() writes owned files, splices region updates, writes .ox-check-proposed siblings, and returns an updated manifest. Plan::summary() renders a stable line-oriented digest; Plan::dry_run_exit_code() returns 1 iff any item would write. For region proposals the proposed file lives next to the *host* (host.ox-check-proposed), and per updates.md section 7 it contains the full spliced host file rather than just the region body.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the emit module (mod, owned_file, local). plan_owned_file() drives one file through the decision algorithm: reads the disk content, detects opt-out (empty/whitespace-only), and produces a PlanItem. plan_tools_just() is the first concrete emitter, embedding templates/justfiles/ox-check/tools.just via include_str! and dispatching through the owned-file driver. The template is wrapped in a sentinel-delimited block per the design convention but is itself an owned file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the full catalog from checks.md as one recipe per check (ox-check-<name>): all pr-fast, pr-test, pr-mutants, nightly-runtime, and nightly-exhaustive members. pr-title is the lone [script('pwsh')] block; everything else is a one-line cargo invocation gated by an ox-check-tools-check or _ox-check-require dependency. plan_checks_just() drives it through the same owned-file path as tools.just.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the two remaining files of the justfiles/ox-check/ tree. groups.just defines the seven check-groups (3 pr + 4 nightly) as just-recipe dependency lists pointing at the per-check recipes from commit 9. tiers.just aggregates them into ox-check-pr, ox-check-nightly, and ox-check-full. plan_local_just_tree() bundles all four file emitters into a single helper for the run driver.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the managed_region driver — the region equivalent of plan_owned_file — and the first managed-region emitter for the user's Justfile. plan_justfile_imports() inserts the four 'import' lines plus the 'alias ox-check := ox-check-pr' line into the ox-check-imports region. Creates the Justfile if absent; appends the region if the file has user content; replaces just the region body otherwise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the cargo_toml emitter producing managed regions for the lint catalog. For multi-crate workspaces: one ox-check-workspace-lints region in the root Cargo.toml ([workspace.lints] in dotted-key form) plus an ox-check-lints region with 'workspace = true' in each member. For single-crate repos: one ox-check-lints region with the full catalog at [lints] scope. The dotted-key form lets users extend the same scope outside the sentinels (TOML forbids re-declaring [workspace.lints.clippy]). Includes a round-trip test that splices the body into a real Cargo.toml and re-parses with toml_edit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds shared_configs emitter producing one managed region per file. Bodies stay intentionally small so most adopters never need to override: deny.toml carries a permissive SPDX allowlist plus advisory/yanked rules, rustfmt.toml sets edition/width/newline-style only, and .delta.toml configures the root files that invalidate impact-scoping. Each can be opted out by emptying its region — no special flag needed. Round-trip test parses every spliced body through toml_edit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the run module to actually drive the update algorithm: discovers the workspace root, loads the manifest, resolves backends, builds the full plan from all local emitters (just tree + Justfile imports + Cargo.toml lints + shared configs), applies (or dry-runs) it, and rewrites .ox-check.lock. Exposes run_update() taking an explicit start directory so integration tests can drive the algorithm without std::process::exit. Adds e2e tests covering first-run write, idempotent second run, dry-run abstinence, opt-out via empty region, and user-edit-with-template-unchanged leave-alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the first layer of the GitHub Actions backend. Two shared composite actions live as static template files: ox-check-setup installs just and the catalog tools; ox-check-impact runs cargo-delta and emits excludes/skip outputs for the impact job. The seven per-group composite actions are rendered programmatically (render_group_action) since they differ only by group name; each takes excludes/skip inputs from the impact job and invokes just ox-check-<group>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the wiring layer: ox-check-pr-impl.yml and ox-check-nightly-impl.yml. Both are workflow_call entry points. PR runs an impact job (cargo-delta) then fans out into pr-fast, pr-test (matrix across test_os input), and pr-mutants jobs that each consume the excludes/skip outputs. Nightly skips the impact job (slow checks always run on main) and fans out into the four nightly groups, uploading the coverage lcov artifact from the Linux test leg.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the two root workflows (ox-check-pr.yml, ox-check-nightly.yml) that own triggers, permissions, and concurrency, then call the reusable workflows. plan_github_backend() bundles all 13 files (2 shared actions + 7 group actions + 2 reusable workflows + 2 root workflows). The run driver dispatches to it when Backend::GitHub is in the resolved set. After this commit --backend github is fully functional. Integration test creates a workspace and asserts the full .github tree is present after one update; a second test confirms idempotency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the first layer of the ADO backend. setup.yml installs just + catalog tools; impact.yml runs cargo-delta and publishes excludes/skip variables via ##vso[task.setvariable] with isOutput=true so the stages template can consume them as stage outputs. The seven per-group step templates are rendered programmatically (render_group_step); each takes excludes/skip parameters, includes setup.yml, and invokes the matching just recipe with OX_CHECK_EXCLUDES set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds pr.yml and nightly.yml stages templates. PR stages: impact stage publishes excludes/skip outputs that downstream stages (pr_fast, pr_test, pr_mutants) consume via stageDependencies. pr_test fans out across linux/windows jobs. Every group stage condition is succeededOrFailed() so a failing pr-fast doesn't gate pr-test. Nightly stages: four parallel stages with empty dependsOn arrays, plus an lcov artifact publish from the linux nightly-test leg. Both templates expose linuxPool/windowsPool object parameters so users can override pools when wrapping in a 1ESPT pipeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds ox-check-pr.yml and ox-check-nightly.yml root pipelines. PR pipeline is PR-only (trigger: none, pr triggers on main); nightly carries a daily cron schedule. Both pass default vmImage-based pools to the stages templates; users wrapping in 1ESPT override these by replacing the root pipeline. plan_ado_backend() bundles all 13 files. The run driver dispatches to it when Backend::Ado is in the resolved set. After this commit --backend ado is fully functional. Integration tests cover ado-only and ado+github combined runs for idempotency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds tests/schemas.rs running external validators against the full emitted tree from a fresh workspace. Validators (taplo for TOML, actionlint for GH workflows, just --list for the Justfile) are invoked via Command; any validator not installed produces a 'skipping' message rather than a test failure, so the suite works on every developer machine while enforcing schema correctness in CI. ADO YAML has no public standalone validator we depend on, so the ado test confirms only structural properties (no tabs, even-space indentation). Also adds 'set unstable' to checks.just so just accepts the [script('pwsh')] attribute on pr-title.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds .github/workflows/regenerate-check.yml: builds cargo-ox-check from the PR branch and runs 'cargo ox-check update --dry-run' against the repo, failing the PR if the in-tree state diverges from what the templates would render. This is the primary dogfooding mechanism from verification.md section 3.

Note: the workflow will report a divergence until a maintainer runs the actual bootstrap step (regenerate the workspace lints region in dotted-key form, write the justfiles/ox-check tree, etc.) — that is a separate human-driven activity, intentionally not folded into this implementation PR. The verification.md bootstrap section covers the procedure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expands lib.rs doc-comments into a publishable-quality crate description covering the what/install/usage/daily-driver/customization story, all linking back to the docs/design/ files. Adds a placeholder README.md following the cargo_heather pattern (auto-generated from doc-comments by 'just readme' from the repo root). 'cargo publish --dry-run -p cargo-ox-check --allow-dirty' completes successfully: 49 files, 341 KiB packaged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Swaps the error type to match the convention used by the other crates in this repo (cargo_heather, cargo_ensure_no_cyclic_deps). Function signatures now return Result<T, ohno::AppError>; error construction uses ohno::app_err! and ohno::bail!; context attachment uses IntoAppError::into_app_err / into_app_err_with. The allowed_external_types whitelist is updated accordingly. All 155 tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Empty content has a stable checksum that no template ever produces, so emptying a file or region body always lands in the standard D != L branches. The steady-state opt-out case (after at least one prior render) reaches LeaveAlone naturally — silent — and a template change reaches Propose — surfaces upstream churn. Both outcomes preserve the user's empty stub, matching the design contract. The only behavior change is the edge case where the user creates an empty file *before* the first ox-check update (no manifest entry yet): instead of a silent skip, ox-check now writes a one-time .ox-check-proposed sibling showing what the template would render. That's arguably better UX since it documents what the user is opting out of.

Removes:

  - DecisionInputs::emptied field

  - Decision::Skipped variant

  - should_emit_proposed_for_opt_out helper

  - the trim-empty detection in owned_file and managed_region drivers

All 155 tests still pass. The behavior the tests covered survives: opt-out tests now assert LeaveAlone (when template unchanged) or Propose (when changed) instead of Skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tools.just, checks.just, groups.just, tiers.just are owned files — the manifest tracks them by full-file checksum. Sentinels are reserved for managed regions inside user-composed hosts (Justfile, Cargo.toml, deny.toml, rustfmt.toml, .delta.toml) where ox-check carves out a section within other content. The 'Owned by cargo-ox-check' advisory comment stays as a one-line notice for human readers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…files

Three coupled cleanups:

1. Add justfiles/ox-check/mod.just as the single entry point. It imports the four sibling .just files and defines 'alias ox-check := ox-check-pr'. The user's Justfile region is now a single 'import justfiles/ox-check/mod.just' line — everything else is owned files the user never edits directly.

2. Move the alias out of the user's Justfile region. The user no longer has any recipe content in their Justfile; mod.just owns it. Easier to evolve (renaming or retargeting the alias is a template update, not a managed-region change).

3. Move every region body out of Rust constants into templates/regions/*.toml and templates/regions/*.just. cargo-lints-body.toml carries the dotted-key catalog with no host-specific header; cargo_toml.rs prepends [workspace.lints] or [lints] based on the manifest shape. Eliminates ~50 lines of inline string and array-of-tuples plumbing.

All 152 unit + 4 integration tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The two remaining YAML-in-Rust blobs (render_group_action,
render_group_step) used format! to splice the group name into a
multi-line string. Replaced with templates/github/group-action.yml and
templates/ado/steps/group.yml carrying a __GROUP__ placeholder; Rust now
just include_str!s the file and runs a single .replace(). __GROUP__
chosen over a curly-brace placeholder so it can't collide with GitHub
Actions' expression syntax inside the same file.

All templates now live as files on disk; no YAML or TOML appears inline
in .rs sources.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drops the hand-written placeholder in favor of the standard ox-tools README generated by 'just readme' (uses ../README.j2 with the crate-level doc-comments from src/lib.rs). Adds the standard crates.io/docs.rs/MSRV/CI/Coverage/License badge row and the project footer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 95.42715% with 129 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.7%. Comparing base (2ed27b1) to head (bb04839).

Files with missing lines Patch % Lines
crates/cargo_ox_check/src/run.rs 91.4% 37 Missing ⚠️
crates/cargo_ox_check/src/backend.rs 78.6% 32 Missing ⚠️
crates/cargo_ox_check/src/plan.rs 96.1% 18 Missing ⚠️
crates/cargo_ox_check/src/main.rs 0.0% 16 Missing ⚠️
crates/cargo_ox_check/src/region.rs 98.1% 5 Missing ⚠️
crates/cargo_ox_check/src/workspace.rs 97.7% 5 Missing ⚠️
crates/cargo_ox_check/src/emit/ado.rs 97.2% 3 Missing ⚠️
crates/cargo_ox_check/src/emit/cargo_toml.rs 97.6% 3 Missing ⚠️
crates/cargo_ox_check/src/emit/github.rs 97.5% 3 Missing ⚠️
crates/cargo_ox_check/src/emit/shared_configs.rs 97.0% 2 Missing ⚠️
... and 4 more

❌ Your project status has failed because the head coverage (96.7%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@                Coverage Diff                @@
##           unified-builds     #33      +/-   ##
=================================================
+ Coverage            86.4%   96.7%   +10.3%     
=================================================
  Files                  16      30      +14     
  Lines                 665    4706    +4041     
=================================================
+ Hits                  575    4554    +3979     
- Misses                 90     152      +62     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Martin Kolinek (from Dev Box) and others added 5 commits June 1, 2026 20:22
cargo-spellcheck flags 100+ words in our own docstrings (autodetect,
subcommand, prepended, splice, sentinels, LeaveAlone, OrphanedKept,
rustfmt, clippy, nextest, binstall, etc.) that aren't in the
hunspell en_US base dictionary. Add the obvious technical jargon
batch — 43 new entries. There will likely be a long tail of
remaining flagged words; iterating on CI to find them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extracted from CI's caret positions on the previous run's failures:
md (13x — markdown file refs in docstrings), Checksum (15x),
CRLF/LF (4-5x — line ending names), aggregator, argv, autocrlf,
B6, checksums, footgun, FS, globbing, invariants, L, schemas,
toml, whitespace, plus a few stems of already-added words.

Down from 109 -> 78 errors on the prior pass; this batch should
clear the remaining 17 unique offenders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous batch added lowercase 'toml', 'checksum', etc. that
were missing — but PowerShell's Sort-Object -Unique is
case-insensitive by default, so each lowercase add was silently
dropped because the uppercase variant existed. The dict ended up
with only TOML, Checksum, MD, ADO, GitHub, CLI capitalizations —
which Hunspell (case-sensitive) doesn't accept as matches for the
lowercase tokens in our docstrings.

Re-merged with Sort-Object -CaseSensitive -Unique so both
casings survive. Verified both 'toml' and 'TOML', 'checksum' and
'Checksum', 'md' and 'MD', etc., now appear in .spelling and
will be carried through preprocessing to target/spelling.dic.

Expected to clear the remaining 34 spellcheck errors driven by
3 unique words (checksum 11x, toml 5x, L 1x).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-spellcheck Hunspell is flagging 'md' as a bare word in
markdown link text like [design.md](../path/design.md). Despite
md being in .spelling, the matching isn't kicking in for these
2-letter tokens — likely a Hunspell short-word issue or
extra_dictionaries merge behavior we don't fully understand.

Pragmatic fix: wrap the link text in backticks so cargo-spellcheck
treats it as inline code (which it skips by default). The link
itself still resolves; the displayed text just shows as monospace
'design.md' in rendered docs.

16 occurrences across 11 source files in cargo_ox_check. Touched
files: backend, cli, decision, manifest, plan, region, workspace,
emit/ado, emit/cargo_toml, emit/github, emit/local.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…it's

Five remaining occurrences after the .md backtick batch:
- plan.rs:129  '(D ≠ L, L = T)' — single-letter Hunspell flags;
  wrap the whole math expression in backticks: '(\D ≠ L, L = T\)'.
- emit/github.rs:44  '§1.' after the markdown link close paren —
  outside the link's backticks; wrap as '\§1\.'.
- checksum.rs:13  'autocrlf=true' has '=' getting flagged;
  wrap as '\�utocrlf=true\'.
- checksum.rs:12  'Git's' apostrophe form not in dict; added to
  .spelling (case-sensitive). Used in two doc lines kept as prose.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@martin-kolinek martin-kolinek changed the title Introduce cargo-ox-check: unified Rust build/CI scaffolding feat: Introduce cargo-ox-check for unified Rust build/CI scaffolding Jun 1, 2026
Martin Kolinek (from Dev Box) and others added 16 commits June 1, 2026 21:50
Empty commit to re-trigger CI now that the PR title has been
renamed to match the conventional-commits pattern enforced by
ox-check-pr-title.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two real deny.toml issues:

1. The repo had a hand-authored [licenses] block from before
   ox-check existed. Our managed region also defined [licenses].
   TOML forbids re-declaring a table, so 'cargo deny check' errored
   out with 'redefinition of table 'licenses''. Removed the
   pre-existing block; our managed region is now the canonical
   source. (The removed entries — BSL-1.0, confidence-threshold=0.8,
   unused-allowed-license — were a strict subset; the catalog's
   defaults are more comprehensive.)

2. unmaintained = 'warn' is stale cargo-deny syntax — newer
   versions take a scope value: 'all' | 'workspace' | 'transitive'
   | 'none'. Bumped catalog default to 'all' (surfaces transitive
   unmaintained crates too); commented the alternative in the
   template body.

Verified locally with 'cargo deny check': advisories ok, bans ok,
licenses ok, sources ok.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-udeps flagged 4 genuinely-unused dependency entries (verified
no source references exist for any of them):

- cargo_ox_check: drop thiserror (dep), assert_cmd + predicates
  (dev-deps). The crate uses ohno's AppError throughout, not
  thiserror; tests use insta + walkdir, not assert_cmd/predicates.
- cargo_ensure_no_cyclic_deps: drop tempfile (dev-dep). The
  integration tests don't actually create temp dirs.

Cargo.lock updated accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kage

cargo-semver-checks errored with 'no library targets found in package
cargo-ensure-no-cyclic-deps' — the explicit --package args from the
impact stage included binary-only crates that have no public API
surface to verify. With --workspace these would be silently skipped,
but per-package mode requires the caller to filter.

Both ox-check-semver-check and ox-check-external-types use rustdoc
and hit the same constraint. Added pre-filter: for each --package
arg, look up cargo metadata for a 'lib' target and drop those that
don't have one. If no library packages remain, skip the recipe
entirely (same exit-0 path as the --skip sentinel).

The filter only fires when explicit --package args are present
(impact-scoped runs); --workspace mode passes through unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-semver-checks reports as errors several conditions that
aren't actual SemVer violations:
1. 'no library targets found in package X' — bin-only crate
2. 'not found in registry' — unpublished or never-released
3. bin->lib transition: published baseline lacks the lib target
   the current source has

All 4 of this repo's crates hit one of these in some combination:
- automation: publish=false; not in registry
- cargo-ensure-no-cyclic-deps: was bin-only at v0.2.0, now bin+lib
- cargo_heather: not in crates.io despite version=0.1.0
- cargo-ox-check: brand new, never published

Rewrote the recipe to:
1. With --workspace, defer to cargo-semver-checks (it handles
   these cases naturally).
2. With explicit --package args, pre-filter to library-bearing
   crates via cargo metadata, then run cargo-semver-checks
   per-package and tolerate output matching
   'not found in registry|no library targets found' as a warning.
   Anything else is treated as a real failure.

Verified locally with all 4 in-PR packages: each warns and skips
cleanly; recipe exits 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-check-external-types CLI is per-manifest:
  cargo check-external-types --manifest-path <path/to/Cargo.toml>

It does not accept --package or --workspace. Our previous recipe
passed --package args through, which produced
'error: unexpected argument --package found'.

Rewrote the recipe to:
1. Build a pkg-name -> manifest-path map from cargo metadata,
   filtered to library-bearing crates.
2. Map --workspace -> all library crates' manifests.
3. Map --package <name> args -> the matching manifest paths (drop
   names that have no library target).
4. Iterate the resulting set, run cargo check-external-types
   --manifest-path <p> for each. Continue on per-package failure;
   exit 1 only if any failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-check-external-types requires a nightly Rust toolchain (uses
unstable rustdoc -Z flags). On the stable channel CI runs were
hitting 'error: the option Z is only accepted on the nightly
compiler'.

Switch the invocation to 'cargo +nightly check-external-types'.
rustup will auto-install nightly on first use; adopters using
msrustup with no nightly channel can override the recipe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-check-external-types pins a specific rustdoc-types schema
that tracks a narrow window of nightly rustdoc JSON output. Newer
nightlies routinely drift past it — v0.4.0 of the tool requires
JSON format version 56, current nightly produces 57. This is a
tooling-cadence mismatch, not a real API violation.

Swallow the specific 'JSON format version X, but this tool
requires format version Y' error as a warning ('nightly drift';
skip this package), continue with the rest. Other errors still
fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-aprz has no 'check' subcommand — its CLI is:
  cargo aprz <crates|deps|init|validate|help>

oxidizer's recipe uses 'cargo aprz deps --error-if-high-risk
--console appraisal'. Adopted the same invocation (without the
HTML report path / cache dir options; adopters who want those
extend the recipe).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo +nightly auto-install via rustup can hang silently on some
runners — observed in the previous PR run where all 4 pr-fast legs
sat in 'Run ./.github/actions/ox-check-pr-fast' for 70+ minutes
with no visible progress, after we switched ox-check-external-types
to use cargo +nightly.

Pre-install nightly explicitly in the setup composite via
'rustup toolchain install nightly --profile minimal --no-self-update'.
+nightly invocations in recipes are then no-ops on the toolchain
side and proceed immediately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-check-external-types requires nightly rustdoc + compiles its
own dependencies from source against nightly (rustdoc-types is heavy)
+ runs rustdoc JSON generation per package. On the dogfooded
ox-tools-gh repo this combination consistently pushed pr-fast past
70 minutes — twice in a row, the previous attempts were cancelled.

This is fundamentally a heavyweight check that doesn't fit the
pr-fast 'all the lightweight static analysis' budget. Move it to
nightly-advisories where it sits alongside other heavy checks
(deny, audit, clippy on workspace, udeps) that can take longer
without blocking PR throughput.

Updates groups.just (recipe wiring) and docs/design/checks.md
(documents the move + rationale).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These nightly-toolchain / rustdoc-based checks were causing pr-fast
to run 100+ minutes by compiling their tools and deps from source.
Aligns with external-types (already moved). pr-fast now stays on
stable-only static analysis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo-aprz queries the GitHub API; unauthenticated requests are
rate-limited to 60/hr, which makes the recipe hang ~50 minutes waiting
on the next bucket. Forward the built-in GITHUB_TOKEN so aprz uses the
authenticated 1000/hr quota.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These checks' outcomes can only change with a code change in this repo
(both analyze the workspace's own deps/API surface), so the
nightly-only justification doesn't hold. The pr-fast hang they were
moved to avoid was actually caused by cargo-aprz GitHub API rate
limiting, now fixed via GITHUB_TOKEN.

external-types stays in nightly-advisories because it pins a specific
nightly rustdoc JSON schema and breaks on toolchain drift independent
of repo changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Strategy:
- Refactor 3 copies of the fs::read_to_string + NotFound passthrough
  helper into a single crate::io::read_file_if_present, marked
  #[mutants::skip] (trivial fs wrapper; mutations on the Ok arms and
  NotFound match guard are not behavior-meaningful and are already
  exercised through every plan/emit path).
- Mark main() #[mutants::skip] (tracing/clap glue).
- Mark backend::read_origin_url #[mutants::skip] (shells out to git;
  integration-tested via backend autodetect).
- Mark run::run #[mutants::skip] (thin process-boundary glue with
  std::process::exit; behavior covered by run_update tests).
- Add targeted unit tests for the 3 remaining genuine logic mutants:
  * manifest::to_toml always ends with newline
  * region::upsert_region adds exactly one blank separator and does
    NOT add an extra one when text already ends with double newline
    (catches the && -> || mutation)
  * run::plan_removals queues orphaned-clean files for Remove, and
    keeps customized orphans as OrphanedKept (covers disabling a
    backend after first writing it)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Martin Kolinek (from Dev Box) and others added 6 commits June 2, 2026 10:14
…tolerance

Adds an owned justfiles/ox-check/versions.just holding the pinned nightly
toolchains used by udeps, miri, careful, and check-external-types:

  rust_nightly                := "nightly-2026-01-21"  general
  rust_nightly_external_types := "nightly-2025-10-18"  narrow

One source of truth, two consumers:

- Recipes read pins via {{ var }} interpolation
  (cargo +{{ rust_nightly }} udeps ...).
- Setup composites (GH setup-action.yml, ADO steps/setup.yml) read pins
  via 'just --evaluate <var>' and call 'rustup toolchain install' so the
  +nightly invocations later are no-ops. Just is installed before nightly
  so the extract works.

Drops the regex-based "JSON format version X but requires Y" tolerance
arm in the check-external-types recipe. With the pin matched to the
tool's required rustdoc schema there is no drift to absorb; if a future
bump breaks the check it means the pin or the tool needs to move,
deliberately.

Why pin instead of float: with floating nightly we needed tolerance
shims to absorb rustdoc / lint / intrinsic drift. Pinning handles all
present and future cases with one mechanism; tolerance shims are
bespoke and silently degrade what the check validates. General nightly
bumps on a regular cadence; external-types pin bumps alongside
cargo-check-external-types upgrades. Both live in versions.just (an
owned file), so adopters can override either.

Other changes:

- mod.just imports versions.just.
- Cache keys (GH, ADO) include versions.just hash so nightly bumps
  invalidate cleanly.
- local.md gets new section 3.6 "Nightly pinning" documenting the
  policy and bump cadence.
- Regression test: checks.just has no bare +nightly invocations.
- Regression test: versions.just defines both pin variables (setup
  composites depend on these names).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds spellcheck.toml as the fourth managed-region shared-config file,
alongside deny.toml, rustfmt.toml, and .delta.toml. Closes the
oversight where ox-check ships the cargo-spellcheck recipe and the
.spelling -> target/spelling.dic generation, but no spellcheck.toml
config tying them together — adopters had to discover and author the
config themselves.

Default body mirrors oxidizer-github's spellcheck.toml:

  [Hunspell]
  lang = "en_US"
  search_dirs = ["."]
  extra_dictionaries = ["target/spelling.dic"]
  skip_os_lookups = true       cross-platform determinism
  use_builtin = true            no system hunspell dep
  [Hunspell.quirks]
  allow_concatenation = true    CamelCase identifier handling

The extra_dictionaries path matches the target/spelling.dic that the
ox-check-spellcheck recipe generates from .spelling — recipe and
config now agree out of the box.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nions

Moves ox-check-fmt from stable rustfmt to the pinned nightly defined
in versions.just (rust_nightly) and turns on the unstable options
oxidizer uses in its own unstable-rustfmt.toml:

  unstable_features = true
  format_code_in_doc_comments = true
  imports_granularity = "Module"
  group_imports = "StdExternalCrate"

These are the high-value rustfmt opinions the surveyed Microsoft Rust
repos reach for; stable rustfmt's option set doesn't include them.
Pinning nightly is the prerequisite that makes this sustainable:
formatting churn now happens on a deliberate pin bump rather than on
every 
ustup update.

Other changes:

- ox-check-fmt now invokes cargo +{{ rust_nightly }} fmt --all --check.
- setup-action.yml and ADO setup.yml install the rust_nightly toolchain
  with --component rustfmt so the nightly rustfmt binary is available.
  The external-types nightly still installs with minimal profile only
  (it doesn't need rustfmt).
- rustfmt_body_sets_edition_and_width test grows to cover the four
  new unstable opinions; if any are dropped, the regression fires.
- local.md \xc2\xa73.6 documents fmt as a nightly-pinned check.
- checks.md mt recipe row reflects the +<pinned-nightly> invocation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applies cargo +nightly-2026-01-21 fmt --all to align with the new
opinion set in the catalog rustfmt.toml (imports_granularity = Module,
group_imports = StdExternalCrate, format_code_in_doc_comments).

Changes are all import-grouping / sorting; no code-logic edits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t removals)

Two adjustments to the workspace-lints catalog body, with the dogfood
state in ox-tools updated to match:

1. Fold 16 new lints from the oxidizer + oxidizer-github two-repo
   consensus into the catalog (Bucket A). 14 restriction-group warns:
   as_pointer_underscore, assertions_on_result_states, deref_by_slicing,
   empty_drop, empty_enum_variants_with_brackets, fn_to_numeric_cast_any,
   if_then_some_else_none, multiple_unsafe_ops_per_block,
   redundant_type_annotations, renamed_function_params,
   semicolon_outside_block, unnecessary_safety_doc,
   unneeded_field_pattern, unused_result_ok. Plus 2 suppressions:
   redundant_pub_crate, should_panic_without_expect.

2. Drop three contested lints from the catalog:
   * rust.missing_docs   noisy on large workspaces (oxidizer omits)
   * clippy.expect_used  over-strict for tools with legitimate panic paths
   * clippy.panic        same rationale; unwrap_used stays

Rationale: a workspace-wide lint we ship can only be selectively
disabled by an adopter via per-crate overrides or by taking ownership
of the entire managed region. TOML's no-duplicate-key rule means
adopters can't simply add 
ust.missing_docs = "allow" outside the
region to override it. So the catalog should be conservative: enable
something only when the consensus is strong enough to justify that
adopter cost. The 16 Bucket A lints meet that bar (two independent
repos enable them); the three dropped lints don't (oxidizer omits all
three).

ox-tools dogfood state aligned: the managed region was previously a
user-customized subset (rust lints only) with the rest cohabited
outside the sentinels. With the new catalog the region is fully
regenerated; the cohabit now keeps only the two genuinely
repo-specific entries (rust.unexpected_cfgs check-cfg list,
clippy.empty_structs_with_brackets - oxidizer is inconsistent on this
so it stayed per-repo).

Regression tests added in cargo_toml.rs:
- catalog_intentionally_omits_contested_lints: locks in the three
  removals; accidental re-additions fire here.
- catalog_includes_consensus_restriction_lints: locks in the 16
  Bucket A entries; accidental removals fire here.

Two trivial clippy fixes in src/run.rs that the new
clippy.manual_contains lint surfaces on the orphan-removal tests
added in an earlier commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Not in the ox-check catalog consensus (oxidizer has it inconsistent in
its own config). No code in this workspace currently relies on it, so
dropping shrinks the cohabit section to its only genuinely repo-specific
entry (rust.unexpected_cfgs check-cfg list).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants