feat(cargo-each): run a command per workspace member with cargo-style selection - #61
feat(cargo-each): run a command per workspace member with cargo-style selection#61martin-kolinek wants to merge 21 commits into
Conversation
…to README.md Add a workspace-wide 'Design Docs Are Part of the Source' section to the root AGENTS.md: design docs live under crates/<crate>/docs/design/README.md, kept in lockstep with the code; implementation plans are optional; code must not cite design-doc sections; docs must not be deleted. Remove the now-redundant design-docs section from cargo-coverage-gate/AGENTS.md (its code citations are intentionally left as-is). Rename the top-level design docs to README.md so they render as the docs/design/ landing page on GitHub and ADO, and update all cross-references in companion docs, implementation plans, and rustdoc comments. Regenerate cargo-anvil's crate README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b9a4bc0-14af-4dcd-96e7-0b9414a10387
460cf6f to
418d096
Compare
|
a51815b to
e81ee81
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #61 +/- ##
========================================
Coverage 100.0% 100.0%
========================================
Files 50 57 +7
Lines 2652 3054 +402
========================================
+ Hits 2652 3054 +402
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
0a1fc24 to
a75f588
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new cargo-each crate (library + cargo each subcommand) to run a command across a cargo-style workspace member selection with optional metadata-based filtering, and includes initial unit + CLI-style tests and design documentation. It also standardizes several crates’ design-doc layout by moving to docs/design/README.md and updates references accordingly.
Changes:
- Add
crates/cargo-eachimplementing workspace discovery (cargo metadata), selection (-p/--package,--workspace/--all,--exclude,--none), filtering predicates, placeholder substitution, and execution planning/modes. - Add unit tests plus end-to-end-ish CLI tests using a temporary fixture workspace.
- Normalize “design doc as
docs/design/README.md” documentation across crates and update repo-level contribution guidance and spelling dictionary.
Reviewed changes
Copilot reviewed 33 out of 36 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/cargo-each/tests/cli.rs | New CLI-driven tests using a temporary fixture workspace. |
| crates/cargo-each/src/workspace.rs | Workspace/member discovery via cargo_metadata. |
| crates/cargo-each/src/substitute.rs | Placeholder substitution ({name}, {spec}, {manifest}, {packages}, etc.). |
| crates/cargo-each/src/select.rs | Cargo-style selector parsing/resolution and glob matching. |
| crates/cargo-each/src/plan.rs | Build per-invocation execution plan (per-package vs --once). |
| crates/cargo-each/src/lib.rs | Public crate docs and re-exports (library surface). |
| crates/cargo-each/src/filter.rs | Predicate parsing/evaluation for --filter / --exclude-filter. |
| crates/cargo-each/src/error.rs | Error taxonomy and umbrella EachError. |
| crates/cargo-each/src/bin/cargo-each/run.rs | cargo each implementation: resolve, filter, plan, execute. |
| crates/cargo-each/src/bin/cargo-each/main.rs | Binary entrypoint and exit-code mapping. |
| crates/cargo-each/src/bin/cargo-each/cli.rs | Clap CLI definitions for cargo each. |
| crates/cargo-each/README.md | Generated crate README mirroring rustdoc content. |
| crates/cargo-each/docs/design/README.md | New design doc for cargo-each. |
| crates/cargo-each/Cargo.toml | New crate manifest with deps, bin target, and dev-deps. |
| crates/cargo-coverage-gate/docs/implementation-plans/0000.md | Update design-doc path reference to docs/design/README.md. |
| crates/cargo-coverage-gate/docs/design/README.md | Add (or move to) top-level design doc as README.md. |
| crates/cargo-coverage-gate/AGENTS.md | Remove duplicated design-doc guidance now covered at repo root. |
| crates/cargo-anvil/src/workspace.rs | Update design-doc references (now docs/design/README.md). |
| crates/cargo-anvil/src/lib.rs | Update design-doc filename references in rustdoc list. |
| crates/cargo-anvil/src/backend.rs | Update design-doc references (now docs/design/README.md). |
| crates/cargo-anvil/README.md | Refresh generated README content/links. |
| crates/cargo-anvil/docs/verification.md | Update design-doc link references. |
| crates/cargo-anvil/docs/implementation-plans/0002.md | Update design-doc link references. |
| crates/cargo-anvil/docs/implementation-plans/0001.md | Update design-doc link references. |
| crates/cargo-anvil/docs/implementation-plans/0000.md | Update design-doc link references. |
| crates/cargo-anvil/docs/design/updates.md | Update cross-links to the design doc filename. |
| crates/cargo-anvil/docs/design/README.md | Add (or move to) top-level design doc as README.md. |
| crates/cargo-anvil/docs/design/local.md | Update cross-links to the design doc filename. |
| crates/cargo-anvil/docs/design/github.md | Update cross-links to the design doc filename. |
| crates/cargo-anvil/docs/design/extensibility.md | Update cross-links to the design doc filename. |
| crates/cargo-anvil/docs/design/checks.md | Update cross-links to the design doc filename. |
| crates/cargo-anvil/docs/design/ado.md | Update cross-links to the design doc filename. |
| crates/automation/README.md | Refresh generated README formatting/header block. |
| Cargo.lock | Add cargo-each package entry. |
| AGENTS.md | Add repo-wide guidance about design docs and section-linking policy. |
| .spelling | Add new allowed words relevant to new CLI/docs. |
Comments suppressed due to low confidence (2)
crates/cargo-each/src/bin/cargo-each/cli.rs:46
--excludeis documented as requiring--workspace, but clap currently allows--excludewithout it (andSelection::resolvewill apply the excludes anyway). Enforcing the cargo-style constraint at the CLI layer prevents surprising combinations like-p foo --exclude foosilently producing an empty set.
/// Exclude a member from the selection (requires --workspace). Repeatable.
#[arg(long, value_name = "SPEC")]
pub(crate) exclude: Vec<String>,
crates/cargo-anvil/src/workspace.rs:33
- Repository guidance says not to cite design doc sections from code comments (AGENTS.md:67-74), because section numbers/anchors drift as docs are reorganized. These doc comments reference
README.md §6; please reword to avoid embedding a section reference (you can still link to the design doc generally).
/// Used by the emitter for [`README.md §6`]: multi-crate workspaces get
/// `[workspace.lints]` with the catalog plus `[lints] workspace = true`
/// in each member; single-crate repos get the catalog directly in
/// `[lints]`.
///
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… member
Introduce `cargo-each`: a cargo-native tool that resolves a cargo-style
package selection (-p/--package with globs, --workspace, --exclude, plus
--none), optionally narrows it with a small metadata predicate language
(--filter/--exclude-filter: lib, bin, dep:<name>, metadata:<key>[=<value>]),
and runs a command over the result — once per member with
{name}/{spec}/{version}/{manifest} substitution, or once for the whole set
(--once, with {packages}). --chdir runs each per-package command from that
member's crate root; --keep-going and --dry-run round out execution.
An empty resolved selection is a successful no-op (exit 0), which lets adopters
drop the per-recipe skip guards. Purpose: collapse the hand-rolled impact
skip/splat/@version-strip/metadata-filter/per-package-iteration PowerShell in
the cargo-anvil check recipes to a single line.
Includes the crate, design doc, and unit + assert_cmd integration tests (100%
line coverage per cargo-coverage-gate; cargo-mutants clean). Adoption in the
cargo-anvil recipes follows once this crate is published to crates.io.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
The design-docs-policy rebase (renaming docs/design/design.md to README.md and touching cargo-anvil's lib.rs) combined with main's cargo-anvil 0.3.0 bump left these auto-generated crate READMEs stale. Regenerated via cargo-doc2readme 0.6.4 (the anvil-pinned version). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
a75f588 to
106a266
Compare
- run.rs: honor the documented --keep-going contract by exiting with a
flat 1 when any invocation fails, instead of the first failing child's
exit code (which contradicted lib.rs and design doc). Tighten the
keep-going integration test to assert exit code 1.
- plan.rs: drop the doc reference to a {manifest_dir} placeholder that
cargo-each does not implement; describe the --chdir target as the
crate's Cargo.toml directory instead.
- cargo-anvil workspace.rs/backend.rs: remove design-doc section anchors
(README.md §N) from rustdoc per the hoisted AGENTS.md policy; link the
design doc without a section number.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
- cli.rs: make --exclude require --workspace via clap (matches the documented contract and cargo's own semantics); add integration tests. - run.rs: on fail-fast, reduce a wide/Windows exit code to its low byte (a closer approximation of the child's code than collapsing to 1), mapping the ambiguous zero-low-byte and signal cases to 1. Extracted as a unit-tested exit_byte helper. - Add the shared project logo.png to cargo-each, cargo-anvil, and automation so their generated READMEs no longer show a broken image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
resolve() ignores -p/--package selectors when --workspace/--all is set
(--workspace wins, matching cargo build), so the resolved set is the
whole workspace. is_whole_workspace() now agrees instead of returning
false on that combination, keeping the {packages} expansion (--workspace
vs explicit --package list) consistent with what actually runs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
- filter.rs: reject metadata keys with empty path segments (a..b, .role,
role.), which parse but can never match (Value::get("") is always None)
and would silently yield an empty set instead of a loud usage error.
- select.rs: replace the recursive glob backtracker (exponential worst
case on inputs like *a*a*a...) with the standard iterative two-pointer
matcher (O(pattern*name)); add coverage for the new cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 39 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
crates/cargo-anvil/docs/design/extensibility.md:333
- The link text says “README.md §5.2”, but the link target is just
./README.md(no section anchor), so it won’t actually navigate to the referenced section. Point it at the correct header anchor to avoid a misleading/broken link.
Selection is unchanged from [README.md §5.2](./README.md): the engine resolves a backend set from
Reject empty prerelease/build suffixes (1.2.3-, 1.2.3+) and leading-zero release components (01.2.3) in version_matches, matching cargo's grammar. Add end-to-end tests for the cargo metadata to Member projection: a path dev-dependency selected via --filter dep:<name>, and default-members resolution on a bare invocation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 39 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
crates/cargo-each/src/workspace.rs:97
has_libis computed by checking for a target kind of exactly "lib". Incargo metadata, proc-macro crates report kind "proc-macro" (not "lib"), so--filter libwill incorrectly exclude proc-macro crates even though they are library targets in practice.
crates/cargo-each/src/select.rs:36- The field doc says
excludeis “only meaningful withall”, butSelection::resolveappliesexcludeeven whenallis false (e.g. it will exclude from a-pselection). Since the CLI already enforces--excluderequires--workspace, the doc here should reflect that rather than implying the library ignores it.
crates/cargo-each/src/plan.rs:75 Plan::buildvalidates placeholders up front, butsubstitute()also callsvalidate_placeholders()for every invocation. For large workspaces this doubles the placeholder-scan work (and repeats it per member in per-package mode). Consider adding an internalsubstitute_unchecked(or passing a pre-validated flag) soPlan::buildcan validate once and then substitute without re-validating.
There was a problem hiding this comment.
[Copilot speaking]
Multi-agent code review — overview
Automated review of the cargo-each crate introduction (plus the cross-crate doc renames, root AGENTS.md, .spelling, and logos) performed by a swarm of subagents across a file-group × focus-area matrix (maintainability, API docs, module structure, testing, present-tense, design docs, Rust errors/panics, naming/API, correctness, tooling/packaging).
Scope & prior state: all 30 pre-existing review threads on this PR are resolved; this pass adds only new findings.
Outcome: all substantive feedback is delivered as individual line comments (no blockers). Findings are low-to-medium severity. Four cross-cutting synthesis findings are anchored on their most relevant files:
- Public-library contract (is this a consumable API?) — on
crates/cargo-each/src/lib.rs. - Recurring primitive-obsession / stringly-typed pattern — on
crates/cargo-each/src/plan.rs. - Documentation ↔ code contract drift — on
crates/cargo-each/docs/design/README.md. - Orphaned code→design section reference exposed by a deletion — on the deleted line in
crates/cargo-coverage-gate/AGENTS.md.
No approve / request-changes status is set — comments only.
- run.rs: under --keep-going, treat a spawn failure as a failed invocation
(log, continue, exit 1) instead of aborting with exit 2.
- filter.rs: name MetadataEquals fields; fix NaN string-equality; match target
kinds via the typed enum.
- plan.rs: replace the whole_workspace bool with a PackagesExpansion enum.
- substitute.rs: make validate_placeholders pub(crate); document the
unknown-placeholder passthrough contract.
- error.rs: document the intentional app-style opaque umbrella.
- workspace.rs: enum-based lib/bin target detection; contract-first load docs.
- select.rs: document index panic-safety and defensive guard.
- docs: {packages} filter caveat; lib = plain lib kind only.
- tests: spawn-failure coverage; specific-code / stderr assertions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 39 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
crates/cargo-each/src/plan.rs:90
Plan::buildaccepts an emptycommandslice and will then emitInvocation { argv: vec![] }for non-emptymembers(becausesubstitute()returns an empty vec). The binary later assumesargvis non-empty (split_first().expect("Plan::build never emits an empty argv")inrun.rs), so library callers (or a future CLI regression) could trigger a panic instead of a usage error.
Consider validating command is non-empty in Plan::build and returning an EachError (likely via a new pub(crate) error variant included in the EachError umbrella) rather than allowing an empty argv to escape.
crates/cargo-each/src/bin/cargo-each/run.rs:143
shell_join()does not quote empty-string arguments. In--dry-runoutput, an empty arg currently renders as nothing, and the joined output loses the fact that an empty argument was present (it will just appear as extra whitespace). Quoting empty args as""would make the dry-run output unambiguous.
- lib.rs/substitute.rs: settle the library as binary-facing tooling — narrow substitute/Placeholders to pub(crate) and state the intent in the spine docs. - substitute.rs/plan.rs: validate_placeholders now takes Mode, not a bare bool. - cargo-coverage-gate: drop the orphaned code->design section reference in render/markdown.rs left behind by the design-doc rename. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
Add a Prior art section to the design doc answering why cargo-workspaces' exec subcommand doesn't cover cargo-each's cargo-native selection, metadata filtering, selection-aware placeholder injection, or empty-set-as-success. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
Broaden the Prior art section to cover cargo-hack: it shares package
selectors but is built for feature/version combinatorics over cargo
subcommands, and cannot run arbitrary per-manifest commands, inject
{manifest}/{name}, or filter by package.metadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d4a5d3ad-e273-4730-8c0f-d1cb806148e0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 40 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
crates/cargo-each/src/bin/cargo-each/run.rs:99
execute()panics if an invocation ever contains an emptyargvbecause of the unconditionalsplit_first().expect(...). Even if the CLI normally prevents this, treating it as a runtime error (exit 2) is more robust than crashing the process.
| @@ -0,0 +1,337 @@ | |||
| # cargo-each — Design | |||
There was a problem hiding this comment.
the design doc misses explanation why do we need to support the library surface. do we have any plans to use it as a library?
Summary
Introduces
cargo-each, a cargo-native subcommand that runs a command over a cargo-style selection of workspace members. It exists to collapse the hand-rolled impact-scoping PowerShell duplicated across ~26cargo-anvilcheck recipes into a single line.What it does
cargo build:-p/--package(globs +name@version),--workspace/--all,--exclude, plus--none(explicit empty set).--filter/--exclude-filteroverlib,bin,dep:<name>,metadata:<key>[=<value>].{name}/{spec}/{version}/{manifest}substitution) or--once(a single invocation, with{packages}expanding to--workspaceor an explicit--package name@versionlist).--chdirruns each per-package command from that member's crate root (per-package only; a usage error with--once).--keep-goingand--dry-runround out execution.--skipguards.Why
The impact scoping in cargo-anvil re-spells the same four chores per recipe: the skip/default preamble,
@versionstripping,cargo metadatafiltering (lib-only, coverage opt-outs, loom-dependents), and per-package iteration.cargo-eachabsorbs all four. Two recipes already carry aTODO(anvil-runner)anticipating exactly this helper.The design doc (
crates/cargo-each/docs/design/README.md§6) shows the before/after for clippy, external-types, loom, and llvm-cov, and lists the smallanvil-impactadjustments the recipe refactor will make (emit--noneinstead of--skip; print one selector token per line; stop version-qualifying).Design decisions worth a look
--from-file/--from-envsource.cargo-eachstays flag-only; a computed selection (an impact tier) is fed in via ordinary shell expansion of anvil's existing_anvil-impact-include <tier>helper, keeping theANVIL_IMPACT=offwidening + tier-default policy in the one place it already lives. (§7 rejected alternatives.)--chdiras an opt-in for directory-oriented tools.Tests
Unit tests (selection resolution incl. globs/exclude/none, filter predicates, placeholder substitution + mode misuse, plan build incl.
--chdir) plusassert_cmdintegration tests driving the binary against a fixture workspace. 100% line coverage percargo-coverage-gate;cargo-mutantsclean;fmt/clippy/spellcheck/external-types/readme-checkall green.Adoption
cargo-eachcan only be wired into the cargo-anvil recipes once it is merged and published to crates.io, so that work is intentionally not part of this PR. Post-publish, the anvilimpact.justhelpers and the ~26 check recipes collapse to singlecargo eachlines (see design doc §6), plus ananvil-tool-cargo-each-installrecipe.