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(
179 long,
180 help = "Run pre-flight publisher-state check and exit (don't start the pipeline)"
181 )]
182 preflight: bool,
183 #[arg(
184 long = "preflight-secrets",
185 help = "Validate that all required publish secrets / credentials are present (and key material is well-formed) without checking host-local tools — for a central pre-release gate across decoupled CI runners. Checks and exits; does not start the pipeline."
186 )]
187 preflight_secrets: bool,
188 #[arg(long, help = "Set the release as a draft")]
189 draft: bool,
190 #[arg(long, help = "Path to a file containing custom release header text")]
191 release_header: Option<PathBuf>,
192 #[arg(
193 long,
194 help = "Path to a template file for release header (rendered with template variables)"
195 )]
196 release_header_tmpl: Option<PathBuf>,
197 #[arg(long, help = "Path to a file containing custom release footer text")]
198 release_footer: Option<PathBuf>,
199 #[arg(
200 long,
201 help = "Path to a template file for release footer (rendered with template variables)"
202 )]
203 release_footer_tmpl: Option<PathBuf>,
204 #[arg(
205 long,
206 help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
207 )]
208 release_notes_tmpl: Option<PathBuf>,
209 #[arg(long, help = "Abort immediately on first error during publishing")]
210 fail_fast: bool,
211 #[arg(
212 long = "no-gate-submitter",
213 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"
214 )]
215 no_gate_submitter: bool,
216 #[arg(
217 long = "simulate-failure",
218 value_name = "publisher",
219 action = clap::ArgAction::Append,
220 hide = true,
221 help = "(TEST HARNESS) Force a named publisher to fail. Gated by ANODIZE_TEST_HARNESS=1."
222 )]
223 simulate_failure: Vec<String>,
224 #[arg(
225 long = "show-skipped",
226 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
227 (normally only visible with --debug). Use to diagnose why a publisher didn't \
228 run for a given crate."
229 )]
230 show_skipped: bool,
231 #[arg(
232 long = "allow-nondeterministic",
233 value_name = "name=reason",
234 action = clap::ArgAction::Append,
235 help = "Runtime non-determinism opt-out for a specific artifact (repeatable). Mutually exclusive with --strict."
236 )]
237 allow_nondeterministic: Vec<String>,
238 #[arg(
239 long = "summary-json",
240 value_name = "path",
241 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."
242 )]
243 summary_json: Option<PathBuf>,
244 #[arg(
245 long = "allow-ai-failure",
246 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."
247 )]
248 allow_ai_failure: bool,
249 #[arg(
250 long = "allow-snapshot-publish",
251 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."
252 )]
253 allow_snapshot_publish: bool,
254 #[arg(
255 long,
256 conflicts_with = "merge",
257 help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
258 )]
259 split: bool,
260 #[arg(
261 long,
262 conflicts_with = "split",
263 help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
264 )]
265 merge: bool,
266 #[arg(
267 long = "publish-only",
268 conflicts_with_all = ["split", "merge", "prepare", "announce_only", "snapshot", "clean"],
269 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/."
270 )]
271 publish_only: bool,
272 #[arg(
273 long,
274 alias = "prepare-only",
275 conflicts_with_all = ["publish_only", "announce_only"],
276 help = PREPARE_HELP.as_str()
277 )]
278 prepare: bool,
279 #[arg(
280 long = "announce-only",
281 conflicts_with_all = ["prepare", "publish_only", "snapshot", "split", "merge", "clean"],
282 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."
283 )]
284 announce_only: bool,
285 #[arg(
286 long,
287 help = "Resume into an existing release left over from a prior failed attempt; bypasses the safety check that bails on partial assets."
288 )]
289 resume_release: bool,
290 #[arg(
291 long,
292 help = "Force release.replace_existing_artifacts: true regardless of config (overwrite conflicting assets on retry)."
293 )]
294 replace_existing: bool,
295 #[arg(
296 long = "no-post-publish-poll",
297 help = "Skip post-publish polling for chocolatey moderation / winget PR validation; report NotPolled for affected publishers."
298 )]
299 no_post_publish_poll: bool,
300 #[arg(
301 long = "no-env-preflight",
302 hide = true,
303 help = "(HARNESS) Skip the environment preflight (tools / secrets / key material). Set by the determinism harness, whose hermetic replica runs in a deliberately credential-less env that the config-derived preflight would correctly reject."
304 )]
305 no_env_preflight: bool,
306 #[arg(
313 long = "rollback",
314 value_name = "policy",
315 hide = true,
316 value_parser = removed_rollback_flag,
317 help = "(REMOVED) Automatic rollback policy."
318 )]
319 removed_rollback: Option<String>,
320 #[arg(
321 long = "rollback-only",
322 value_name = "removed",
323 num_args = 0..=1,
324 default_missing_value = "rollback-only",
325 hide = true,
326 value_parser = removed_rollback_flag,
327 help = "(REMOVED) Run only the rollback of a prior run."
328 )]
329 removed_rollback_only: Option<String>,
330 #[arg(
331 long = "from-run",
332 value_name = "run-id",
333 hide = true,
334 value_parser = removed_rollback_flag,
335 help = "(REMOVED) Prior run id to roll back."
336 )]
337 removed_from_run: Option<String>,
338 #[arg(
339 long = "no-failure-policy",
340 value_name = "removed",
341 num_args = 0..=1,
342 default_missing_value = "no-failure-policy",
343 hide = true,
344 value_parser = removed_rollback_flag,
345 help = "(REMOVED) Ignore release.on_failure for this run."
346 )]
347 removed_no_failure_policy: Option<String>,
348 },
349 Build {
351 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
352 crate_names: Vec<String>,
353 #[arg(
354 long,
355 default_value = "3h",
356 help = "Pipeline timeout duration (e.g., 90m, 3h, 5s) — a generous \
357 safety backstop, not the primary bound; per-stage bounds \
358 (e.g. announce.deadline) catch a hung stage in seconds"
359 )]
360 timeout: String,
361 #[arg(
362 long,
363 short = 'p',
364 help = "Maximum number of parallel build jobs (default: number of CPUs)"
365 )]
366 parallelism: Option<usize>,
367 #[arg(long, help = "Build only for the host target triple")]
368 single_target: bool,
369 #[arg(
370 long,
371 conflicts_with = "crate_names",
372 help = "Build a specific workspace in a monorepo config"
373 )]
374 workspace: Option<String>,
375 #[arg(
376 long,
377 short = 'o',
378 help = "Copy the built binary to this path (requires --single-target and single crate)"
379 )]
380 output: Option<PathBuf>,
381 #[arg(
382 long,
383 value_delimiter = ',',
384 help = "Skip hook lanes or stages (comma-separated: before, after, always, on-error, validate, sign, notarize)"
385 )]
386 skip: Vec<String>,
387 },
388 Check {
390 #[command(subcommand)]
391 cmd: CheckCmd,
392 },
393 Init {
395 #[arg(
396 long,
397 help = "Discover repo files that embed the current version and enroll the selection into version_files in .anodizer.yaml"
398 )]
399 version_files: bool,
400 #[arg(
401 long,
402 value_delimiter = ',',
403 requires = "version_files",
404 help = "Glob(s) to drop from discovered candidates (repeatable or comma-separated); only with --version-files"
405 )]
406 exclude: Vec<String>,
407 #[arg(
408 long,
409 short = 'y',
410 requires = "version_files",
411 help = "Non-interactive: enroll all discovered candidates without prompting"
412 )]
413 yes: bool,
414 },
415 Changelog {
417 #[arg(
418 value_name = "tag|range",
419 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"
420 )]
421 range: Option<String>,
422 #[arg(
423 long,
424 value_enum,
425 default_value = "keep-a-changelog",
426 help = "Output format: keep-a-changelog (refresh the [Unreleased] section), release-notes (grouped-bullet GitHub body to stdout), or json"
427 )]
428 format: ChangelogFormat,
429 #[arg(
430 long,
431 help = "Apply the regenerated [Unreleased] section to the configured CHANGELOG.md file(s) in place (keep-a-changelog only)"
432 )]
433 write: bool,
434 #[arg(long = "crate", help = "Restrict to a specific crate in a workspace")]
435 crate_name: Option<String>,
436 #[arg(
437 long,
438 help = "Preview as a snapshot release (release-notes format only)"
439 )]
440 snapshot: bool,
441 },
442 Completion {
444 #[arg(value_enum, help = "Shell to generate completions for")]
445 shell: Shell,
446 },
447 Healthcheck,
449 Preflight {
459 #[arg(long, help = "Output the report as JSON")]
460 json: bool,
461 #[arg(
462 long,
463 help = "Check only the publish-time stages (what `release --publish-only` runs), not artifact-producing stages"
464 )]
465 publish_only: bool,
466 #[arg(
467 long,
468 value_delimiter = ',',
469 help = "Skip requirement collection for these stages (comma-separated, same names as release --skip)"
470 )]
471 skip: Vec<String>,
472 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
473 publishers: Vec<String>,
474 #[arg(
475 long,
476 help = "GitHub token override; when set, GitHub token env-var requirements are treated as satisfied"
477 )]
478 token: Option<String>,
479 },
480 Man,
482 Jsonschema,
484 ResolveTag {
486 #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
487 tag: String,
488 #[arg(long, help = "Output as JSON")]
489 json: bool,
490 },
491 Targets {
497 #[arg(long, help = "Output as JSON (include-form matrix)")]
498 json: bool,
499 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
500 crate_names: Vec<String>,
501 },
502 Vocabulary {
510 #[arg(long, help = "Output as JSON")]
511 json: bool,
512 },
513 Tools {
520 #[arg(long, help = "Output as JSON")]
521 json: bool,
522 #[arg(
523 long,
524 help = "Only the tools the publish-time stages need (what `release --publish-only` runs), not artifact-producing stages"
525 )]
526 publish_only: bool,
527 #[arg(
528 long,
529 value_delimiter = ',',
530 help = "Drop tools contributed by these skipped stages (comma-separated, same names as release --skip)"
531 )]
532 skip: Vec<String>,
533 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
534 publishers: Vec<String>,
535 },
536 Tag {
538 #[arg(long, help = "Show what tag would be created, without creating it")]
539 dry_run: bool,
540 #[arg(long, help = "Override bump logic with a specific tag value")]
541 custom_tag: Option<String>,
542 #[arg(long = "version", value_name = "VERSION")]
553 version_override: Option<String>,
554 #[arg(long, help = "Override default bump type (patch/minor/major)")]
555 default_bump: Option<String>,
556 #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
557 crate_name: Option<String>,
558 #[arg(
559 long,
560 help = "Push the version-sync bump commit to the release branch atomically with the tag"
561 )]
562 push: bool,
563 #[arg(
564 long,
565 conflicts_with = "push",
566 help = "Do not push anything; the tag(s) and version-sync bump commit stay local"
567 )]
568 no_push: bool,
569 #[arg(
570 long,
571 conflicts_with_all = ["push", "no_push"],
572 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)"
573 )]
574 push_tags_only: bool,
575 #[arg(
576 long,
577 help = "Create a signed annotated tag (git tag -s), using the signing key/method from git config (user.signingkey, gpg.format)"
578 )]
579 sign: bool,
580 #[arg(
581 long,
582 conflicts_with = "sign",
583 help = "Create an unsigned annotated tag (git tag -a), overriding tag.sign = true in config"
584 )]
585 no_sign: bool,
586 #[arg(
587 long,
588 value_name = "NAME",
589 help = "Remote to push to (default: origin)"
590 )]
591 push_remote: Option<String>,
592 #[arg(
593 long,
594 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"
595 )]
596 push_dry_run: bool,
597 #[arg(
598 long = "changelog",
599 help = "Refresh CHANGELOG.md as part of this tag (requires a `changelog:` config block)"
600 )]
601 changelog: bool,
602 #[command(subcommand)]
608 sub: Option<TagSub>,
609 },
610 Continue {
630 #[arg(
631 long,
632 help = "Merge artifacts from split build jobs and run post-build stages"
633 )]
634 merge: bool,
635 #[arg(long, help = "Custom dist directory (overrides config)")]
636 dist: Option<PathBuf>,
637 #[arg(long, help = "Run full pipeline without side effects")]
638 dry_run: bool,
639 #[arg(
640 long,
641 value_delimiter = ',',
642 help = "Skip stages or publishers (comma-separated, e.g. docker,announce,npm). \
643 Unified denylist: a stage name skips the stage, a publisher name \
644 (npm, homebrew, chocolatey, …) skips that publisher."
645 )]
646 skip: Vec<String>,
647 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
648 publishers: Vec<String>,
649 #[arg(
650 long,
651 help = TOKEN_HELP.as_str()
652 )]
653 token: Option<String>,
654 },
655 Publish {
668 #[arg(long, help = "Run full pipeline without side effects")]
669 dry_run: bool,
670 #[arg(
671 long,
672 help = TOKEN_HELP.as_str()
673 )]
674 token: Option<String>,
675 #[arg(long, help = "Custom dist directory (overrides config)")]
676 dist: Option<PathBuf>,
677 #[arg(
678 long,
679 help = "Merge artifacts from `release --split` workers (dist/<subdir>/context.json) before running the publish-only pipeline. Mirrors `goreleaser publish --merge`."
680 )]
681 merge: bool,
682 #[arg(
683 long = "show-skipped",
684 help = "Show per-crate 'no <publisher> config block' skip lines at default verbosity \
685 (normally only visible with --debug). Use to diagnose why a publisher didn't \
686 run for a given crate."
687 )]
688 show_skipped: bool,
689 #[arg(
690 long,
691 value_delimiter = ',',
692 help = "Skip stages or publishers (comma-separated, e.g. npm,blob). \
693 Unified denylist: a stage name skips the stage, a publisher name \
694 (npm, homebrew, chocolatey, …) skips that publisher."
695 )]
696 skip: Vec<String>,
697 #[arg(long = "publishers", value_delimiter = ',', help = PUBLISHERS_HELP_STEM)]
698 publishers: Vec<String>,
699 },
700 Promote {
709 #[arg(
710 long,
711 help = "Destination track: stable | prerelease | candidate | beta | edge, \
712 or a publisher-native track name (passed through verbatim)."
713 )]
714 to: String,
715 #[arg(
716 long,
717 help = "Source track (default: prerelease — each publisher's pre-stable track). \
718 Canonical or publisher-native."
719 )]
720 from: Option<String>,
721 #[arg(
722 long = "publishers",
723 value_delimiter = ',',
724 help = "Comma-separated promotion-capable publishers to run (default: all \
725 configured). Naming a configured-but-not-promotable publisher is an error."
726 )]
727 publishers: Vec<String>,
728 #[arg(
734 long = "version",
735 value_name = "VERSION",
736 conflicts_with = "from_run",
737 help = "Promote this explicit version/tag (default: the newest artifact in the \
738 --from track)."
739 )]
740 version_selector: Option<String>,
741 #[arg(
742 long = "from-run",
743 value_parser = parse_run_id,
744 help = "Promote what a prior release run recorded (reads dist/run-<id>/report.json)."
745 )]
746 from_run: Option<String>,
747 #[arg(
748 long,
749 help = "Resolve and print the plan without running any external command"
750 )]
751 dry_run: bool,
752 },
753 Bump {
759 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
760 level_or_version: Option<String>,
761 #[arg(
762 long,
763 short = 'p',
764 visible_alias = "crate",
765 action = clap::ArgAction::Append,
766 help = "Bump a specific crate (repeatable)"
767 )]
768 package: Vec<String>,
769 #[arg(
770 long,
771 alias = "all",
772 conflicts_with = "package",
773 help = "Bump every workspace member (excluding publish=false)"
774 )]
775 workspace: bool,
776 #[arg(
777 long,
778 action = clap::ArgAction::Append,
779 help = "Exclude a crate from --workspace (repeatable)"
780 )]
781 exclude: Vec<String>,
782 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
783 pre: Option<String>,
784 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
785 exact: bool,
786 #[arg(
787 long,
788 help = "Proceed even if the working tree has uncommitted changes"
789 )]
790 allow_dirty: bool,
791 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
792 yes: bool,
793 #[arg(long, help = "Print the plan without editing any files")]
794 dry_run: bool,
795 #[arg(long, help = "Stage edits and create a single commit")]
796 commit: bool,
797 #[arg(
798 long = "changelog",
799 requires = "commit",
800 help = "Refresh CHANGELOG.md in the bump commit (requires --commit and a `changelog:` config block)"
801 )]
802 changelog: bool,
803 #[arg(
804 long,
805 requires = "commit",
806 help = "GPG-sign the commit (requires --commit)"
807 )]
808 sign: bool,
809 #[arg(long, help = "Override the default commit message template")]
810 commit_message: Option<String>,
811 #[arg(
812 long,
813 default_value = "text",
814 help = "Output format: text | json (json requires --dry-run)"
815 )]
816 output: String,
817 },
818 Announce {
827 #[arg(long, help = "Run full pipeline without side effects")]
828 dry_run: bool,
829 #[arg(long, help = "Custom dist directory (overrides config)")]
830 dist: Option<PathBuf>,
831 #[arg(
832 long,
833 help = TOKEN_HELP.as_str()
834 )]
835 token: Option<String>,
836 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
837 skip: Vec<String>,
838 #[arg(
839 long,
840 help = "Merge artifact lists from `release --split` workers (dist/<subdir>/context.json) before announcing. Mirrors `goreleaser announce --merge`."
841 )]
842 merge: bool,
843 },
844 Notify {
851 message: String,
854 #[arg(long = "publishers", value_delimiter = ',')]
858 publishers: Vec<String>,
859 #[arg(long = "skip", value_delimiter = ',')]
861 skip: Vec<String>,
862 #[arg(long)]
866 raw: bool,
867 #[arg(long = "allow-secrets")]
871 allow_secrets: bool,
872 #[arg(long)]
874 dry_run: bool,
875 },
876}
877
878fn parse_run_id(s: &str) -> Result<String, String> {
891 anodizer_stage_publish::rollback::validate_run_id(s)
892 .map(|()| s.to_string())
893 .map_err(|err| format!("{:#}", err))
894}
895
896pub fn detect_host_target() -> anyhow::Result<String> {
899 anodizer_core::partial::detect_host_target()
900}
901
902pub fn num_cpus() -> usize {
904 std::thread::available_parallelism()
905 .map(|n| n.get())
906 .unwrap_or(4)
907}
908
909pub fn build_cli() -> clap::Command {
911 <Cli as clap::CommandFactory>::command()
912}