1use clap::{Parser, Subcommand};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[command(name = "anodizer", version, about = "Release Rust projects with ease")]
7pub struct Cli {
8 #[arg(
9 long,
10 short = 'f',
11 global = true,
12 help = "Path to config file (overrides auto-detection)"
13 )]
14 pub config: Option<PathBuf>,
15 #[arg(long, global = true, help = "Enable verbose output")]
16 pub verbose: bool,
17 #[arg(long, global = true, help = "Enable debug output")]
18 pub debug: bool,
19 #[arg(long, short = 'q', global = true, help = "Suppress non-error output")]
20 pub quiet: bool,
21 #[arg(
22 long,
23 global = true,
24 help = "Strict mode: configured features that silently skip become hard errors"
25 )]
26 pub strict: bool,
27 #[command(subcommand)]
33 pub command: Option<Commands>,
34}
35
36#[derive(Subcommand)]
37#[allow(clippy::large_enum_variant)]
42pub enum Commands {
43 Release {
45 #[arg(long = "crate", visible_alias = "id", action = clap::ArgAction::Append, help = "Release a specific crate (repeatable; --id is accepted as a GoReleaser-compat alias)")]
46 crate_names: Vec<String>,
47 #[arg(long, help = "Release all crates with unreleased changes")]
48 all: bool,
49 #[arg(long, help = "Force release even without unreleased changes")]
50 force: bool,
51 #[arg(long, help = "Build without publishing (snapshot mode)")]
52 snapshot: bool,
53 #[arg(long, help = "Create a nightly release with date-based version")]
54 nightly: bool,
55 #[arg(long, help = "Run full pipeline without side effects")]
56 dry_run: bool,
57 #[arg(long, help = "Remove dist directory before starting")]
58 clean: bool,
59 #[arg(
60 long,
61 value_delimiter = ',',
62 help = "Skip stages (comma-separated, e.g. docker,announce)"
63 )]
64 skip: Vec<String>,
65 #[arg(
66 long,
67 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
68 )]
69 token: Option<String>,
70 #[arg(
71 long,
72 default_value = "60m",
73 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
74 )]
75 timeout: String,
76 #[arg(
77 long,
78 short = 'p',
79 help = "Maximum number of parallel build jobs (default: number of CPUs)"
80 )]
81 parallelism: Option<usize>,
82 #[arg(long, help = "Automatically set --snapshot if the git repo is dirty")]
83 auto_snapshot: bool,
84 #[arg(long, help = "Build only for the host target triple")]
85 single_target: bool,
86 #[arg(
87 long,
88 value_name = "csv",
89 conflicts_with = "single_target",
90 help = "Restrict the build to a comma-separated subset of configured target triples (e.g. x86_64-apple-darwin,aarch64-apple-darwin). Used by the Determinism Harness's sharded job matrix; conflicts with --single-target."
91 )]
92 targets: Option<String>,
93 #[arg(
94 long = "host-targets",
95 conflicts_with = "single_target",
96 conflicts_with = "targets",
97 help = "Build every configured target this host can build, skipping cross-compile-only targets (apple targets on a non-macOS host). Only valid with --snapshot or --dry-run. Used by `task prepush` to do a real host-scoped build without aborting on un-buildable targets."
98 )]
99 host_targets: bool,
100 #[arg(
101 long,
102 help = "Path to a custom release notes file (overrides changelog)"
103 )]
104 release_notes: Option<PathBuf>,
105 #[arg(
106 long,
107 conflicts_with = "crate_names",
108 help = "Release a specific workspace in a monorepo config"
109 )]
110 workspace: Option<String>,
111 #[arg(
112 long,
113 conflicts_with = "no_preflight",
114 help = "Run pre-flight publisher-state check and exit (don't start the pipeline)"
115 )]
116 preflight: bool,
117 #[arg(
118 long,
119 conflicts_with = "preflight",
120 help = "Skip the automatic pre-flight publisher-state check"
121 )]
122 no_preflight: bool,
123 #[arg(
124 long,
125 help = "Alias for --strict (also treats Unknown publisher state as a blocker during pre-flight)"
126 )]
127 strict_preflight: bool,
128 #[arg(long, help = "Set the release as a draft")]
129 draft: bool,
130 #[arg(long, help = "Path to a file containing custom release header text")]
131 release_header: Option<PathBuf>,
132 #[arg(
133 long,
134 help = "Path to a template file for release header (rendered with template variables)"
135 )]
136 release_header_tmpl: Option<PathBuf>,
137 #[arg(long, help = "Path to a file containing custom release footer text")]
138 release_footer: Option<PathBuf>,
139 #[arg(
140 long,
141 help = "Path to a template file for release footer (rendered with template variables)"
142 )]
143 release_footer_tmpl: Option<PathBuf>,
144 #[arg(
145 long,
146 help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
147 )]
148 release_notes_tmpl: Option<PathBuf>,
149 #[arg(long, help = "Abort immediately on first error during publishing")]
150 fail_fast: bool,
151 #[arg(
152 long = "no-gate-submitter",
153 help = "Disable the Submitter gate: dispatch Submitter publishers even when required Assets/Manager publishers failed"
154 )]
155 no_gate_submitter: bool,
156 #[arg(
157 long = "rollback",
158 value_name = "none|best-effort",
159 help = "Rollback policy after publish stage. Defaults to best-effort when preflight is clean, none otherwise."
160 )]
161 rollback: Option<String>,
162 #[arg(
163 long = "simulate-failure",
164 value_name = "publisher",
165 action = clap::ArgAction::Append,
166 hide = true,
167 help = "(TEST HARNESS) Force a named publisher to fail. Gated by ANODIZE_TEST_HARNESS=1."
168 )]
169 simulate_failure: Vec<String>,
170 #[arg(
171 long = "rollback-only",
172 requires = "from_run",
173 conflicts_with = "clean",
174 help = "Skip publish; re-attempt rollback from a prior run report. Requires --from-run=<id>."
175 )]
176 rollback_only: bool,
177 #[arg(
178 long = "from-run",
179 value_name = "id",
180 requires = "rollback_only",
181 value_parser = parse_run_id,
182 help = "Prior run id whose state to load when running --rollback-only. \
183 Loads <dist>/run-<id>/rollback.json if present (a prior replay's state), \
184 otherwise <dist>/run-<id>/report.json. Delete rollback.json to force a \
185 full re-roll. Must match the run_id format written by the release pipeline \
186 (alphanumeric, dot, dash, underscore; no path separators)."
187 )]
188 from_run: Option<String>,
189 #[arg(
190 long = "allow-rerun",
191 conflicts_with = "rollback_only",
192 help = "DANGEROUS: force publish to proceed even when a prior \
193 dist/run-<id>/report.json exists for this tag. PR-based publishers \
194 (homebrew, scoop, nix, krew, MCP) will open DUPLICATE pull requests. \
195 Recover from partial failures with --rollback-only --from-run=<id> first. \
196 Cannot be combined with --rollback-only (which has its own idempotency)."
197 )]
198 allow_rerun: bool,
199 #[arg(
200 long = "show-skipped",
201 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
202 (normally only visible with --debug). Use to diagnose why a publisher didn't \
203 run for a given crate."
204 )]
205 show_skipped: bool,
206 #[arg(
207 long = "allow-nondeterministic",
208 value_name = "name=reason",
209 action = clap::ArgAction::Append,
210 help = "Runtime non-determinism opt-out for a specific artifact (repeatable). Mutually exclusive with --strict."
211 )]
212 allow_nondeterministic: Vec<String>,
213 #[arg(
214 long = "summary-json",
215 value_name = "path",
216 help = "Write the per-publisher run summary JSON to this path. Without it, real (non-snapshot, non-dry-run) releases write <dist>/run-<id>/summary.json — even when a stage fails — so recovery tooling always has machine-readable publish state."
217 )]
218 summary_json: Option<PathBuf>,
219 #[arg(
220 long = "allow-ai-failure",
221 help = "If `changelog.ai` is configured and the AI provider fails, log a warning and keep the pre-AI release notes instead of aborting the release."
222 )]
223 allow_ai_failure: bool,
224 #[arg(
225 long,
226 conflicts_with = "merge",
227 help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
228 )]
229 split: bool,
230 #[arg(
231 long,
232 conflicts_with = "split",
233 help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
234 )]
235 merge: bool,
236 #[arg(
237 long = "publish-only",
238 conflicts_with_all = ["split", "merge", "prepare", "announce_only", "snapshot", "rollback_only", "clean"],
239 help = "Load artifacts from dist/ (preserved by `anodize check determinism --preserve-dist`) and run only the sign + publish pipeline. Skips build/archive/nfpm/sbom/checksum — those stages' outputs must already be present in dist/."
240 )]
241 publish_only: bool,
242 #[arg(
243 long,
244 alias = "prepare-only",
245 conflicts_with_all = ["publish_only", "announce_only", "rollback_only"],
246 help = "Run local build + archive + sign + checksum + sbom stages but skip release / publish / announce (GoReleaser Pro parity). Artifacts stay in dist/ for inspection. `--prepare-only` is accepted as an alias for GR-imported scripts."
247 )]
248 prepare: bool,
249 #[arg(
250 long = "announce-only",
251 conflicts_with_all = ["prepare", "publish_only", "snapshot", "rollback_only", "split", "merge", "clean"],
252 help = "Re-fire announcers only. Loads `<dist>/run-<id>/report.json` written by a prior run, skips every pipeline stage except announce (which itself short-circuits on nightly), then runs after-hooks. Use this to retry a transient announcer failure (Slack 502, Discord 5xx) without re-creating the GitHub release or re-publishing to package managers. Fails fast when no `<dist>/run-<id>/report.json` is present."
253 )]
254 announce_only: bool,
255 #[arg(
256 long,
257 help = "Resume into an existing release left over from a prior failed attempt; bypasses the safety check that bails on partial assets."
258 )]
259 resume_release: bool,
260 #[arg(
261 long,
262 help = "Force release.replace_existing_artifacts: true regardless of config (overwrite conflicting assets on retry)."
263 )]
264 replace_existing: bool,
265 #[arg(
266 long = "no-post-publish-poll",
267 help = "Skip post-publish polling for chocolatey moderation / winget PR validation; report NotPolled for affected publishers."
268 )]
269 no_post_publish_poll: bool,
270 },
271 Build {
273 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
274 crate_names: Vec<String>,
275 #[arg(
276 long,
277 default_value = "60m",
278 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
279 )]
280 timeout: String,
281 #[arg(
282 long,
283 short = 'p',
284 help = "Maximum number of parallel build jobs (default: number of CPUs)"
285 )]
286 parallelism: Option<usize>,
287 #[arg(long, help = "Build only for the host target triple")]
288 single_target: bool,
289 #[arg(
290 long,
291 conflicts_with = "crate_names",
292 help = "Build a specific workspace in a monorepo config"
293 )]
294 workspace: Option<String>,
295 #[arg(
296 long,
297 short = 'o',
298 help = "Copy the built binary to this path (requires --single-target and single crate)"
299 )]
300 output: Option<PathBuf>,
301 #[arg(
302 long,
303 value_delimiter = ',',
304 help = "Skip stages (comma-separated: pre-hooks, post-hooks, validate, before)"
305 )]
306 skip: Vec<String>,
307 },
308 Check {
310 #[command(subcommand)]
311 cmd: CheckCmd,
312 },
313 Init {
315 #[arg(
316 long,
317 help = "Discover repo files that embed the current version and enroll the selection into version_files in .anodizer.yaml"
318 )]
319 version_files: bool,
320 #[arg(
321 long,
322 value_delimiter = ',',
323 requires = "version_files",
324 help = "Glob(s) to drop from discovered candidates (repeatable or comma-separated); only with --version-files"
325 )]
326 exclude: Vec<String>,
327 #[arg(
328 long,
329 short = 'y',
330 requires = "version_files",
331 help = "Non-interactive: enroll all discovered candidates without prompting"
332 )]
333 yes: bool,
334 },
335 Changelog {
337 #[arg(
338 value_name = "tag|range",
339 help = "Commit range to render: a single tag (predecessor-resolved against its crate), an explicit `from..to` range, or omitted to refresh each crate's pending section against its last tag"
340 )]
341 range: Option<String>,
342 #[arg(
343 long,
344 value_enum,
345 default_value = "keep-a-changelog",
346 help = "Output format: keep-a-changelog (refresh the [Unreleased] section), release-notes (grouped-bullet GitHub body to stdout), or json"
347 )]
348 format: ChangelogFormat,
349 #[arg(
350 long,
351 help = "Apply the regenerated [Unreleased] section to the configured CHANGELOG.md file(s) in place (keep-a-changelog only)"
352 )]
353 write: bool,
354 #[arg(long = "crate", help = "Restrict to a specific crate in a workspace")]
355 crate_name: Option<String>,
356 #[arg(
357 long,
358 help = "Preview as a snapshot release (release-notes format only)"
359 )]
360 snapshot: bool,
361 },
362 Completion {
364 #[arg(value_enum, help = "Shell to generate completions for")]
365 shell: Shell,
366 },
367 Healthcheck,
369 Preflight {
376 #[arg(long, help = "Output the report as JSON")]
377 json: bool,
378 #[arg(
379 long,
380 help = "Check only the publish-time surface (the stages `release --publish-only` runs), not artifact-producing stages"
381 )]
382 publish_only: bool,
383 #[arg(
384 long,
385 value_delimiter = ',',
386 help = "Skip requirement collection for these stages (comma-separated, same names as release --skip)"
387 )]
388 skip: Vec<String>,
389 #[arg(
390 long,
391 hide_env_values = true,
392 help = "GitHub token override; when set, GitHub token env-var requirements are treated as satisfied"
393 )]
394 token: Option<String>,
395 },
396 Man,
398 Jsonschema,
400 ResolveTag {
402 #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
403 tag: String,
404 #[arg(long, help = "Output as JSON")]
405 json: bool,
406 },
407 Targets {
413 #[arg(long, help = "Output as JSON (include-form matrix)")]
414 json: bool,
415 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
416 crate_names: Vec<String>,
417 },
418 Tag {
420 #[arg(long, help = "Show what tag would be created without pushing")]
421 dry_run: bool,
422 #[arg(long, help = "Override bump logic with a specific tag value")]
423 custom_tag: Option<String>,
424 #[arg(long = "version", value_name = "VERSION")]
435 version_override: Option<String>,
436 #[arg(long, help = "Override default bump type (patch/minor/major)")]
437 default_bump: Option<String>,
438 #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
439 crate_name: Option<String>,
440 #[arg(
441 long,
442 help = "Push the version-sync bump commit to the release branch atomically with the tag"
443 )]
444 push: bool,
445 #[arg(
446 long,
447 conflicts_with = "push",
448 help = "Push the tag only, leaving the version-sync bump commit local"
449 )]
450 no_push: bool,
451 #[arg(
452 long,
453 value_name = "NAME",
454 help = "Remote to push to (default: origin)"
455 )]
456 push_remote: Option<String>,
457 #[arg(
458 long,
459 help = "Create the tag + bump commit locally but only print (not run) the git push commands --push would use; pass --dry-run to also preview tagging"
460 )]
461 push_dry_run: bool,
462 #[arg(
463 long = "changelog",
464 help = "Refresh CHANGELOG.md as part of this tag (requires a `changelog:` config block)"
465 )]
466 changelog: bool,
467 #[command(subcommand)]
473 sub: Option<TagSub>,
474 },
475 Continue {
495 #[arg(
496 long,
497 help = "Merge artifacts from split build jobs and run post-build stages"
498 )]
499 merge: bool,
500 #[arg(long, help = "Custom dist directory (overrides config)")]
501 dist: Option<PathBuf>,
502 #[arg(long, help = "Run full pipeline without side effects")]
503 dry_run: bool,
504 #[arg(
505 long,
506 value_delimiter = ',',
507 help = "Skip stages (comma-separated, e.g. docker,announce)"
508 )]
509 skip: Vec<String>,
510 #[arg(
511 long,
512 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
513 )]
514 token: Option<String>,
515 },
516 Publish {
529 #[arg(long, help = "Run full pipeline without side effects")]
530 dry_run: bool,
531 #[arg(
532 long,
533 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
534 )]
535 token: Option<String>,
536 #[arg(long, help = "Custom dist directory (overrides config)")]
537 dist: Option<PathBuf>,
538 #[arg(
539 long,
540 help = "Merge artifacts from `release --split` workers (dist/<subdir>/context.json) before running the publish-only pipeline. Mirrors `goreleaser publish --merge`."
541 )]
542 merge: bool,
543 #[arg(
544 long,
545 help = "Force re-publish even when a prior report.json exists. \
546 WARNING: PR-based publishers will open duplicate pull requests."
547 )]
548 allow_rerun: bool,
549 #[arg(
550 long = "show-skipped",
551 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
552 (normally only visible with --debug). Use to diagnose why a publisher didn't \
553 run for a given crate."
554 )]
555 show_skipped: bool,
556 },
557 Bump {
563 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
564 level_or_version: Option<String>,
565 #[arg(
566 long,
567 short = 'p',
568 visible_alias = "crate",
569 action = clap::ArgAction::Append,
570 help = "Bump a specific crate (repeatable)"
571 )]
572 package: Vec<String>,
573 #[arg(
574 long,
575 alias = "all",
576 conflicts_with = "package",
577 help = "Bump every workspace member (excluding publish=false)"
578 )]
579 workspace: bool,
580 #[arg(
581 long,
582 action = clap::ArgAction::Append,
583 help = "Exclude a crate from --workspace (repeatable)"
584 )]
585 exclude: Vec<String>,
586 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
587 pre: Option<String>,
588 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
589 exact: bool,
590 #[arg(
591 long,
592 help = "Proceed even if the working tree has uncommitted changes"
593 )]
594 allow_dirty: bool,
595 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
596 yes: bool,
597 #[arg(long, help = "Print the plan without editing any files")]
598 dry_run: bool,
599 #[arg(long, help = "Stage edits and create a single commit")]
600 commit: bool,
601 #[arg(
602 long = "changelog",
603 requires = "commit",
604 help = "Refresh CHANGELOG.md in the bump commit (requires --commit and a `changelog:` config block)"
605 )]
606 changelog: bool,
607 #[arg(
608 long,
609 requires = "commit",
610 help = "GPG-sign the commit (requires --commit)"
611 )]
612 sign: bool,
613 #[arg(long, help = "Override the default commit message template")]
614 commit_message: Option<String>,
615 #[arg(
616 long,
617 default_value = "text",
618 help = "Output format: text | json (json requires --dry-run)"
619 )]
620 output: String,
621 },
622 Announce {
631 #[arg(long, help = "Run full pipeline without side effects")]
632 dry_run: bool,
633 #[arg(long, help = "Custom dist directory (overrides config)")]
634 dist: Option<PathBuf>,
635 #[arg(
636 long,
637 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
638 )]
639 token: Option<String>,
640 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
641 skip: Vec<String>,
642 #[arg(
643 long,
644 help = "Merge artifact lists from `release --split` workers (dist/<subdir>/context.json) before announcing. Mirrors `goreleaser announce --merge`."
645 )]
646 merge: bool,
647 },
648 Notify {
655 message: String,
658 #[arg(long = "publishers", value_delimiter = ',')]
662 publishers: Vec<String>,
663 #[arg(long = "skip", value_delimiter = ',')]
665 skip: Vec<String>,
666 #[arg(long)]
668 dry_run: bool,
669 },
670}
671
672#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
674pub enum ChangelogFormat {
675 #[default]
679 #[value(name = "keep-a-changelog", alias = "kac")]
680 KeepAChangelog,
681 ReleaseNotes,
684 Json,
687}
688
689#[derive(Subcommand)]
696pub enum TagSub {
697 Rollback {
700 #[arg(
701 value_name = "sha",
702 help = "Commit SHA to roll back from. Defaults to HEAD."
703 )]
704 sha: Option<String>,
705 #[arg(long, help = "Print what would happen without mutating anything")]
706 dry_run: bool,
707 #[arg(
708 long = "no-push",
709 help = "Skip remote tag delete and branch push (local-only)"
710 )]
711 no_push: bool,
712 #[arg(
713 long,
714 help = "Override the published-state guard: roll back even when the tag's run summary shows a one-way-door publisher (crates.io, chocolatey, winget, snapcraft, ...) accepted the version, or — when no summary exists — when a published (non-draft) GitHub release exists for the tag. Without it, rollback refuses because those registries never accept the same version twice: the version is burned and the only clean recovery is fixing forward"
715 )]
716 force: bool,
717 #[arg(
718 long,
719 default_value = "all",
720 help = "Tag-shape filter: all | lockstep | per-crate"
721 )]
722 scope: String,
723 #[arg(
724 long,
725 default_value = "revert",
726 help = "Rollback strategy: revert (default; history-preserving) | reset (opt-in; rewrites history, requires --force-with-lease to push)"
727 )]
728 mode: String,
729 #[arg(
730 long,
731 value_name = "name",
732 help = "Branch name to push the revert commit to. Required when HEAD is detached and no local branch points at it (typical CI tag-push context, where GITHUB_REF_NAME is the tag — not the bump-commit branch). Pass --branch master (or whichever branch the bump commit was created on)."
733 )]
734 branch: Option<String>,
735 },
736}
737
738#[derive(Subcommand)]
744pub enum CheckCmd {
745 Config {
747 #[arg(long, help = "Validate a specific workspace in a monorepo config")]
748 workspace: Option<String>,
749 },
750 Determinism(CheckDeterminismArgs),
752 VersionFiles,
754}
755
756#[derive(clap::Args)]
757pub struct CheckDeterminismArgs {
758 #[arg(
759 long,
760 default_value = "2",
761 help = "Number of from-clean rebuilds to diff"
762 )]
763 pub runs: u32,
764 #[arg(
765 long,
766 value_name = "stages",
767 help = "Optional stage subset (build,archive,sbom,sign,checksum,cargo-package,docker). `cargo-package` is harness-only — drives `cargo package --no-verify --allow-dirty` per workspace member to probe `.crate` byte-stability without hitting a registry. `docker` is harness-only — drives `docker buildx build --output=type=oci,rewrite-timestamp=true,dest=…` against `<repo>/Dockerfile` to probe OCI image byte-stability without pushing to a registry; skipped when `docker buildx` is unavailable or no Dockerfile exists."
768 )]
769 pub stages: Option<String>,
770 #[arg(
771 long,
772 value_name = "csv",
773 help = "Restrict the harness to a comma-separated subset of configured target triples. Used by the sharded release workflow so each runner only validates targets it can natively build (Linux runner skips macOS targets, etc.). Forwarded to the child `anodize release --snapshot` subprocess."
774 )]
775 pub targets: Option<String>,
776 #[arg(
777 long,
778 value_name = "path",
779 help = "JSON report path; default dist/run-<id>/determinism.json"
780 )]
781 pub report: Option<PathBuf>,
782 #[arg(
783 long,
784 conflicts_with = "no_snapshot",
785 help = "Force snapshot mode on the child release subprocess (artifacts get a `-SNAPSHOT-<sha>` suffix). Default: auto — snapshot off when HEAD is at a tag, on otherwise."
786 )]
787 pub snapshot: bool,
788 #[arg(
789 long = "no-snapshot",
790 conflicts_with = "snapshot",
791 help = "Force snapshot mode OFF on the child release subprocess (artifacts emit the actual release version). Default: auto — see --snapshot."
792 )]
793 pub no_snapshot: bool,
794 #[arg(
795 long = "inject-drift",
796 value_name = "stage",
797 hide = true,
798 help = "(TEST HARNESS) Append 1 random byte to the first artifact emitted by <stage>. Gated by ANODIZE_TEST_HARNESS=1."
799 )]
800 pub inject_drift: Option<String>,
801 #[arg(
802 long = "preserve-dist",
803 value_name = "path",
804 help = "When the harness greens, copy run-0's `<worktree>/dist/**` to <path> and emit `<path>/context.json` describing the artifact set. The release workflow's publish-only path consumes this to ship the determinism step's output directly (eliminates the redundant `build:` recompilation). Local operators can pass this too — useful for inspecting a hermetic dist tree without re-running the release pipeline."
805 )]
806 pub preserve_dist: Option<PathBuf>,
807 #[arg(
808 long = "crate",
809 value_name = "name",
810 help = "When --preserve-dist is set, write the preserved dist tree to \
811 <dest>/<name>/ instead of directly into <dest>/. Used by the \
812 sharded matrix to produce per-crate subdirectories so a \
813 `release --publish-only` job can merge all crates into a single \
814 dist/ without context.json collision."
815 )]
816 pub crate_name: Option<String>,
817}
818
819fn parse_run_id(s: &str) -> Result<String, String> {
832 anodizer_stage_publish::rollback_only::validate_run_id(s)
833 .map(|()| s.to_string())
834 .map_err(|err| format!("{:#}", err))
835}
836
837pub fn detect_host_target() -> anyhow::Result<String> {
840 anodizer_core::partial::detect_host_target()
841}
842
843pub fn num_cpus() -> usize {
845 std::thread::available_parallelism()
846 .map(|n| n.get())
847 .unwrap_or(4)
848}
849
850pub fn build_cli() -> clap::Command {
852 <Cli as clap::CommandFactory>::command()
853}