1use clap::{Parser, Subcommand};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5pub mod mcp;
6pub mod subcommands;
7
8pub use subcommands::{ChangelogFormat, CheckCmd, CheckDeterminismArgs, TagSub};
9
10const PUBLISHERS_HELP_STEM: &str = "Comma-separated publishers to run (default: all configured). \
14 --skip always wins over --publishers.";
15
16static TOKEN_HELP: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
20 format!(
21 "GitHub token (overrides {} env vars)",
22 anodizer_core::git::GITHUB_TOKEN_ENV_LADDER.join(" / ")
23 )
24});
25
26static PREPARE_HELP: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
29 format!(
30 "Run local build + archive + sign + checksum + sbom stages but skip every \
31 upstream-reaching stage ({}) — GoReleaser Pro parity. Artifacts stay in dist/ \
32 for inspection. `--prepare-only` is accepted as an alias for GR-imported scripts.",
33 anodizer_core::stages::UPSTREAM_STAGES.join(", ")
34 )
35});
36
37const REMOVED_ROLLBACK_FLAG_MIGRATION: &str = "removed in favor of convergent re-run: re-running `anodizer release` with the identical \
43 arguments reconciles against what already published and skips it, so a failed release is \
44 recovered by re-running it. To withdraw a release deliberately, use `anodizer tag rollback`. \
45 Drop this flag from the invocation.";
46
47fn removed_rollback_flag(_: &str) -> Result<String, String> {
51 Err(REMOVED_ROLLBACK_FLAG_MIGRATION.to_string())
52}
53
54#[derive(Parser)]
57#[command(name = "anodizer", version, about = "Release Rust projects with ease")]
58pub struct Cli {
59 #[arg(
60 long,
61 short = 'f',
62 global = true,
63 help = "Path to config file (overrides auto-detection)"
64 )]
65 pub config: Option<PathBuf>,
66 #[arg(long, global = true, help = "Enable verbose output")]
67 pub verbose: bool,
68 #[arg(long, global = true, help = "Enable debug output")]
69 pub debug: bool,
70 #[arg(long, short = 'q', global = true, help = "Suppress non-error output")]
71 pub quiet: bool,
72 #[arg(
73 long,
74 global = true,
75 help = "Strict mode: configured features that silently skip become hard errors"
76 )]
77 pub strict: bool,
78 #[command(subcommand)]
84 pub command: Option<Commands>,
85}
86
87#[derive(Subcommand)]
90#[allow(clippy::large_enum_variant)]
95pub enum Commands {
96 Release {
106 #[arg(long = "crate", visible_alias = "id", action = clap::ArgAction::Append, help = "Release a specific crate (repeatable; --id is accepted as a GoReleaser-compat alias)")]
107 crate_names: Vec<String>,
108 #[arg(long, help = "Release all crates with unreleased changes")]
109 all: bool,
110 #[arg(long, help = "Force release even without unreleased changes")]
111 force: bool,
112 #[arg(long, help = "Build without publishing (snapshot mode)")]
113 snapshot: bool,
114 #[arg(long, help = "Create a nightly release with date-based version")]
115 nightly: bool,
116 #[arg(long, help = "Run full pipeline without side effects")]
117 dry_run: bool,
118 #[arg(long, help = "Remove dist directory before starting")]
119 clean: bool,
120 #[arg(
121 long,
122 value_delimiter = ',',
123 help = "Skip stages or publishers (comma-separated, e.g. docker,announce,npm). \
124 Unified denylist: a stage name skips the stage, a publisher name \
125 (npm, homebrew, chocolatey, …) skips that publisher."
126 )]
127 skip: Vec<String>,
128 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
129 publishers: Vec<String>,
130 #[arg(
131 long,
132 help = TOKEN_HELP.as_str()
133 )]
134 token: Option<String>,
135 #[arg(
136 long,
137 default_value = "3h",
138 help = "Pipeline timeout duration (e.g., 90m, 3h, 5s) — a generous \
139 safety backstop, not the primary bound; per-stage bounds \
140 (e.g. announce.deadline) catch a hung stage in seconds"
141 )]
142 timeout: String,
143 #[arg(
144 long,
145 short = 'p',
146 help = "Maximum number of parallel build jobs (default: number of CPUs)"
147 )]
148 parallelism: Option<usize>,
149 #[arg(long, help = "Automatically set --snapshot if the git repo is dirty")]
150 auto_snapshot: bool,
151 #[arg(long, help = "Build only for the host target triple")]
152 single_target: bool,
153 #[arg(
154 long,
155 value_name = "csv",
156 conflicts_with = "single_target",
157 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."
158 )]
159 targets: Option<String>,
160 #[arg(
161 long = "host-targets",
162 conflicts_with = "single_target",
163 conflicts_with = "targets",
164 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."
165 )]
166 host_targets: bool,
167 #[arg(
168 long,
169 help = "Path to a custom release notes file (overrides changelog)"
170 )]
171 release_notes: Option<PathBuf>,
172 #[arg(
173 long,
174 conflicts_with = "crate_names",
175 help = "Release a specific workspace in a monorepo config"
176 )]
177 workspace: Option<String>,
178 #[arg(long, help = "Set the release as a draft")]
179 draft: bool,
180 #[arg(long, help = "Path to a file containing custom release header text")]
181 release_header: Option<PathBuf>,
182 #[arg(
183 long,
184 help = "Path to a template file for release header (rendered with template variables)"
185 )]
186 release_header_tmpl: Option<PathBuf>,
187 #[arg(long, help = "Path to a file containing custom release footer text")]
188 release_footer: Option<PathBuf>,
189 #[arg(
190 long,
191 help = "Path to a template file for release footer (rendered with template variables)"
192 )]
193 release_footer_tmpl: Option<PathBuf>,
194 #[arg(
195 long,
196 help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
197 )]
198 release_notes_tmpl: Option<PathBuf>,
199 #[arg(long, help = "Abort immediately on first error during publishing")]
200 fail_fast: bool,
201 #[arg(
202 long = "no-gate-submitter",
203 help = "Disable the Submitter gate: dispatch Submitter publishers even when required Assets/Manager publishers failed, or when the pre-submitter verify-release check did not pass"
204 )]
205 no_gate_submitter: bool,
206 #[arg(
207 long = "simulate-failure",
208 value_name = "publisher",
209 action = clap::ArgAction::Append,
210 hide = true,
211 help = "(TEST HARNESS) Force a named publisher to fail. Gated by ANODIZE_TEST_HARNESS=1."
212 )]
213 simulate_failure: Vec<String>,
214 #[arg(
215 long = "show-skipped",
216 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
217 (normally only visible with --debug). Use to diagnose why a publisher didn't \
218 run for a given crate."
219 )]
220 show_skipped: bool,
221 #[arg(
222 long = "allow-nondeterministic",
223 value_name = "name=reason",
224 action = clap::ArgAction::Append,
225 help = "Runtime non-determinism opt-out for a specific artifact (repeatable). Mutually exclusive with --strict."
226 )]
227 allow_nondeterministic: Vec<String>,
228 #[arg(
229 long = "summary-json",
230 value_name = "path",
231 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."
232 )]
233 summary_json: Option<PathBuf>,
234 #[arg(
235 long = "allow-ai-failure",
236 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."
237 )]
238 allow_ai_failure: bool,
239 #[arg(
240 long = "allow-snapshot-publish",
241 help = "DANGEROUS: allow publishing a non-release version (snapshot / dirty / 0.0.0-sentinel, e.g. 0.0.0~SNAPSHOT-<sha>) to external publishers. By default the publish, blob, and announce stages refuse such versions — several indexes (crates.io, Cloudsmith, Chocolatey, winget, AUR) are one-way doors. Use ONLY for a private/test channel."
242 )]
243 allow_snapshot_publish: bool,
244 #[arg(
245 long,
246 conflicts_with = "merge",
247 help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
248 )]
249 split: bool,
250 #[arg(
251 long,
252 conflicts_with = "split",
253 help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
254 )]
255 merge: bool,
256 #[arg(
257 long = "publish-only",
258 conflicts_with_all = ["split", "merge", "prepare", "announce_only", "snapshot", "clean"],
259 help = "Load artifacts from dist/ (preserved by `anodizer 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/."
260 )]
261 publish_only: bool,
262 #[arg(
263 long,
264 alias = "prepare-only",
265 conflicts_with_all = ["publish_only", "announce_only"],
266 help = PREPARE_HELP.as_str()
267 )]
268 prepare: bool,
269 #[arg(
270 long = "announce-only",
271 conflicts_with_all = ["prepare", "publish_only", "snapshot", "split", "merge", "clean"],
272 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."
273 )]
274 announce_only: bool,
275 #[arg(
276 long,
277 help = "Resume into an existing release left over from a prior failed attempt; bypasses the safety check that bails on partial assets."
278 )]
279 resume_release: bool,
280 #[arg(
281 long,
282 help = "Force release.replace_existing_artifacts: true regardless of config (overwrite conflicting assets on retry)."
283 )]
284 replace_existing: bool,
285 #[arg(
286 long = "no-post-publish-poll",
287 help = "Skip post-publish polling for chocolatey moderation / winget PR validation; report NotPolled for affected publishers."
288 )]
289 no_post_publish_poll: bool,
290 #[arg(
297 long = "rollback",
298 value_name = "policy",
299 hide = true,
300 value_parser = removed_rollback_flag,
301 help = "(REMOVED) Automatic rollback policy."
302 )]
303 removed_rollback: Option<String>,
304 #[arg(
305 long = "rollback-only",
306 value_name = "removed",
307 num_args = 0..=1,
308 default_missing_value = "rollback-only",
309 hide = true,
310 value_parser = removed_rollback_flag,
311 help = "(REMOVED) Run only the rollback of a prior run."
312 )]
313 removed_rollback_only: Option<String>,
314 #[arg(
315 long = "from-run",
316 value_name = "run-id",
317 hide = true,
318 value_parser = removed_rollback_flag,
319 help = "(REMOVED) Prior run id to roll back."
320 )]
321 removed_from_run: Option<String>,
322 #[arg(
323 long = "no-failure-policy",
324 value_name = "removed",
325 num_args = 0..=1,
326 default_missing_value = "no-failure-policy",
327 hide = true,
328 value_parser = removed_rollback_flag,
329 help = "(REMOVED) Ignore release.on_failure for this run."
330 )]
331 removed_no_failure_policy: Option<String>,
332 },
333 Build {
335 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
336 crate_names: Vec<String>,
337 #[arg(
338 long,
339 default_value = "3h",
340 help = "Pipeline timeout duration (e.g., 90m, 3h, 5s) — a generous \
341 safety backstop, not the primary bound; per-stage bounds \
342 (e.g. announce.deadline) catch a hung stage in seconds"
343 )]
344 timeout: String,
345 #[arg(
346 long,
347 short = 'p',
348 help = "Maximum number of parallel build jobs (default: number of CPUs)"
349 )]
350 parallelism: Option<usize>,
351 #[arg(long, help = "Build only for the host target triple")]
352 single_target: bool,
353 #[arg(
354 long,
355 conflicts_with = "crate_names",
356 help = "Build a specific workspace in a monorepo config"
357 )]
358 workspace: Option<String>,
359 #[arg(
360 long,
361 short = 'o',
362 help = "Copy the built binary to this path (requires --single-target and single crate)"
363 )]
364 output: Option<PathBuf>,
365 #[arg(
366 long,
367 value_delimiter = ',',
368 help = "Skip hook lanes or stages (comma-separated: before, after, always, on-error, validate, sign, notarize)"
369 )]
370 skip: Vec<String>,
371 },
372 Check {
374 #[command(subcommand)]
375 cmd: CheckCmd,
376 },
377 Init {
379 #[arg(
380 long,
381 help = "Discover repo files that embed the current version and enroll the selection into version_files in .anodizer.yaml"
382 )]
383 version_files: bool,
384 #[arg(
385 long,
386 value_delimiter = ',',
387 requires = "version_files",
388 help = "Glob(s) to drop from discovered candidates (repeatable or comma-separated); only with --version-files"
389 )]
390 exclude: Vec<String>,
391 #[arg(
392 long,
393 short = 'y',
394 requires = "version_files",
395 help = "Non-interactive: enroll all discovered candidates without prompting"
396 )]
397 yes: bool,
398 },
399 Changelog {
401 #[arg(
402 value_name = "tag|range",
403 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"
404 )]
405 range: Option<String>,
406 #[arg(
407 long,
408 value_enum,
409 default_value = "keep-a-changelog",
410 help = "Output format: keep-a-changelog (refresh the [Unreleased] section), release-notes (grouped-bullet GitHub body to stdout), or json"
411 )]
412 format: ChangelogFormat,
413 #[arg(
414 long,
415 help = "Apply the regenerated [Unreleased] section to the configured CHANGELOG.md file(s) in place (keep-a-changelog only)"
416 )]
417 write: bool,
418 #[arg(long = "crate", help = "Restrict to a specific crate in a workspace")]
419 crate_name: Option<String>,
420 #[arg(
421 long,
422 help = "Preview as a snapshot release (release-notes format only)"
423 )]
424 snapshot: bool,
425 },
426 Completion {
428 #[arg(value_enum, help = "Shell to generate completions for")]
429 shell: Shell,
430 },
431 Healthcheck,
433 Preflight {
446 #[arg(long, help = "Output the report as JSON")]
447 json: bool,
448 #[arg(
449 long,
450 help = "Check only the publish-time stages (what `release --publish-only` runs), not artifact-producing stages"
451 )]
452 publish_only: bool,
453 #[arg(
454 long,
455 value_delimiter = ',',
456 help = "Skip requirement collection for these stages (comma-separated, same names as release --skip)"
457 )]
458 skip: Vec<String>,
459 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
460 publishers: Vec<String>,
461 #[arg(
462 long,
463 help = "GitHub token override; when set, GitHub token env-var requirements are treated as satisfied"
464 )]
465 token: Option<String>,
466 },
467 Man,
469 Jsonschema,
471 ResolveTag {
473 #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
474 tag: String,
475 #[arg(long, help = "Output as JSON")]
476 json: bool,
477 },
478 Targets {
484 #[arg(long, help = "Output as JSON (include-form matrix)")]
485 json: bool,
486 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
487 crate_names: Vec<String>,
488 },
489 Vocabulary {
497 #[arg(long, help = "Output as JSON")]
498 json: bool,
499 },
500 Tools {
507 #[arg(long, help = "Output as JSON")]
508 json: bool,
509 #[arg(
510 long,
511 help = "Only the tools the publish-time stages need (what `release --publish-only` runs), not artifact-producing stages"
512 )]
513 publish_only: bool,
514 #[arg(
515 long,
516 value_delimiter = ',',
517 help = "Drop tools contributed by these skipped stages (comma-separated, same names as release --skip)"
518 )]
519 skip: Vec<String>,
520 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
521 publishers: Vec<String>,
522 },
523 Tag {
525 #[arg(long, help = "Show what tag would be created, without creating it")]
526 dry_run: bool,
527 #[arg(long, help = "Override bump logic with a specific tag value")]
528 custom_tag: Option<String>,
529 #[arg(long = "version", value_name = "VERSION")]
540 version_override: Option<String>,
541 #[arg(long, help = "Override default bump type (patch/minor/major)")]
542 default_bump: Option<String>,
543 #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
544 crate_name: Option<String>,
545 #[arg(
546 long,
547 help = "Push the version-sync bump commit to the release branch atomically with the tag"
548 )]
549 push: bool,
550 #[arg(
551 long,
552 conflicts_with = "push",
553 help = "Do not push anything; the tag(s) and version-sync bump commit stay local"
554 )]
555 no_push: bool,
556 #[arg(
557 long,
558 conflicts_with_all = ["push", "no_push"],
559 help = "Push the tag(s) but not the version-sync bump commit (deferred-branch CI pattern; the branch must be advanced to the bump commit separately)"
560 )]
561 push_tags_only: bool,
562 #[arg(
563 long,
564 help = "Create a signed annotated tag (git tag -s), using the signing key/method from git config (user.signingkey, gpg.format)"
565 )]
566 sign: bool,
567 #[arg(
568 long,
569 conflicts_with = "sign",
570 help = "Create an unsigned annotated tag (git tag -a), overriding tag.sign = true in config"
571 )]
572 no_sign: bool,
573 #[arg(
574 long,
575 value_name = "NAME",
576 help = "Remote to push to (default: origin)"
577 )]
578 push_remote: Option<String>,
579 #[arg(
580 long,
581 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"
582 )]
583 push_dry_run: bool,
584 #[arg(
585 long = "changelog",
586 help = "Refresh CHANGELOG.md as part of this tag (requires a `changelog:` config block)"
587 )]
588 changelog: bool,
589 #[command(subcommand)]
595 sub: Option<TagSub>,
596 },
597 Continue {
617 #[arg(
618 long,
619 help = "Merge artifacts from split build jobs and run post-build stages"
620 )]
621 merge: bool,
622 #[arg(long, help = "Custom dist directory (overrides config)")]
623 dist: Option<PathBuf>,
624 #[arg(long, help = "Run full pipeline without side effects")]
625 dry_run: bool,
626 #[arg(
627 long,
628 value_delimiter = ',',
629 help = "Skip stages or publishers (comma-separated, e.g. docker,announce,npm). \
630 Unified denylist: a stage name skips the stage, a publisher name \
631 (npm, homebrew, chocolatey, …) skips that publisher."
632 )]
633 skip: Vec<String>,
634 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
635 publishers: Vec<String>,
636 #[arg(
637 long,
638 help = TOKEN_HELP.as_str()
639 )]
640 token: Option<String>,
641 },
642 Publish {
655 #[arg(long, help = "Run full pipeline without side effects")]
656 dry_run: bool,
657 #[arg(
658 long,
659 help = TOKEN_HELP.as_str()
660 )]
661 token: Option<String>,
662 #[arg(long, help = "Custom dist directory (overrides config)")]
663 dist: Option<PathBuf>,
664 #[arg(
665 long,
666 help = "Merge artifacts from `release --split` workers (dist/<subdir>/context.json) before running the publish-only pipeline. Mirrors `goreleaser publish --merge`."
667 )]
668 merge: bool,
669 #[arg(
670 long = "show-skipped",
671 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
672 (normally only visible with --debug). Use to diagnose why a publisher didn't \
673 run for a given crate."
674 )]
675 show_skipped: bool,
676 #[arg(
677 long,
678 value_delimiter = ',',
679 help = "Skip stages or publishers (comma-separated, e.g. npm,blob). \
680 Unified denylist: a stage name skips the stage, a publisher name \
681 (npm, homebrew, chocolatey, …) skips that publisher."
682 )]
683 skip: Vec<String>,
684 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
685 publishers: Vec<String>,
686 },
687 Promote {
696 #[arg(
697 long,
698 help = "Destination track: stable | prerelease | candidate | beta | edge, \
699 or a publisher-native track name (passed through verbatim)."
700 )]
701 to: String,
702 #[arg(
703 long,
704 help = "Source track (default: prerelease — each publisher's pre-stable track). \
705 Canonical or publisher-native."
706 )]
707 from: Option<String>,
708 #[arg(
709 long = "publishers",
710 value_delimiter = ',',
711 help = "Comma-separated promotion-capable publishers to run (default: all \
712 configured). Naming a configured-but-not-promotable publisher is an error."
713 )]
714 publishers: Vec<String>,
715 #[arg(
721 long = "version",
722 value_name = "VERSION",
723 conflicts_with = "from_run",
724 help = "Promote this explicit version/tag (default: the newest artifact in the \
725 --from track)."
726 )]
727 version_selector: Option<String>,
728 #[arg(
729 long = "from-run",
730 value_parser = parse_run_id,
731 help = "Promote what a prior release run recorded (reads dist/run-<id>/report.json)."
732 )]
733 from_run: Option<String>,
734 #[arg(
735 long,
736 help = "Resolve and print the plan without running any external command"
737 )]
738 dry_run: bool,
739 },
740 Bump {
746 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
747 level_or_version: Option<String>,
748 #[arg(
749 long,
750 short = 'p',
751 visible_alias = "crate",
752 action = clap::ArgAction::Append,
753 help = "Bump a specific crate (repeatable)"
754 )]
755 package: Vec<String>,
756 #[arg(
757 long,
758 alias = "all",
759 conflicts_with = "package",
760 help = "Bump every workspace member (excluding publish=false)"
761 )]
762 workspace: bool,
763 #[arg(
764 long,
765 action = clap::ArgAction::Append,
766 help = "Exclude a crate from --workspace (repeatable)"
767 )]
768 exclude: Vec<String>,
769 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
770 pre: Option<String>,
771 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
772 exact: bool,
773 #[arg(
774 long,
775 help = "Proceed even if the working tree has uncommitted changes"
776 )]
777 allow_dirty: bool,
778 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
779 yes: bool,
780 #[arg(long, help = "Print the plan without editing any files")]
781 dry_run: bool,
782 #[arg(long, help = "Stage edits and create a single commit")]
783 commit: bool,
784 #[arg(
785 long = "changelog",
786 requires = "commit",
787 help = "Refresh CHANGELOG.md in the bump commit (requires --commit and a `changelog:` config block)"
788 )]
789 changelog: bool,
790 #[arg(
791 long,
792 requires = "commit",
793 help = "GPG-sign the commit (requires --commit)"
794 )]
795 sign: bool,
796 #[arg(long, help = "Override the default commit message template")]
797 commit_message: Option<String>,
798 #[arg(
799 long,
800 default_value = "text",
801 help = "Output format: text | json (json requires --dry-run)"
802 )]
803 output: String,
804 },
805 Announce {
814 #[arg(long, help = "Run full pipeline without side effects")]
815 dry_run: bool,
816 #[arg(long, help = "Custom dist directory (overrides config)")]
817 dist: Option<PathBuf>,
818 #[arg(
819 long,
820 help = TOKEN_HELP.as_str()
821 )]
822 token: Option<String>,
823 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
824 skip: Vec<String>,
825 #[arg(
826 long,
827 help = "Merge artifact lists from `release --split` workers (dist/<subdir>/context.json) before announcing. Mirrors `goreleaser announce --merge`."
828 )]
829 merge: bool,
830 },
831 Notify {
838 message: String,
841 #[arg(long = "publishers", value_delimiter = ',')]
845 publishers: Vec<String>,
846 #[arg(long = "skip", value_delimiter = ',')]
848 skip: Vec<String>,
849 #[arg(long)]
853 raw: bool,
854 #[arg(long = "allow-secrets")]
858 allow_secrets: bool,
859 #[arg(long)]
861 dry_run: bool,
862 },
863}
864
865fn parse_run_id(s: &str) -> Result<String, String> {
878 anodizer_stage_publish::rollback::validate_run_id(s)
879 .map(|()| s.to_string())
880 .map_err(|err| format!("{:#}", err))
881}
882
883pub fn detect_host_target() -> anyhow::Result<String> {
886 anodizer_core::partial::detect_host_target()
887}
888
889pub fn num_cpus() -> usize {
891 std::thread::available_parallelism()
892 .map(|n| n.get())
893 .unwrap_or(4)
894}
895
896pub fn build_cli() -> clap::Command {
898 <Cli as clap::CommandFactory>::command()
899}