Skip to main content

anodizer_cli/
lib.rs

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)]
28    pub command: Commands,
29}
30
31#[derive(Subcommand)]
32pub enum Commands {
33    /// Run the full release pipeline
34    Release {
35        #[arg(long = "crate", visible_alias = "id", action = clap::ArgAction::Append, help = "Release a specific crate (repeatable; --id is accepted as a GoReleaser-compat alias)")]
36        crate_names: Vec<String>,
37        #[arg(long, help = "Release all crates with unreleased changes")]
38        all: bool,
39        #[arg(long, help = "Force release even without unreleased changes")]
40        force: bool,
41        #[arg(long, help = "Build without publishing (snapshot mode)")]
42        snapshot: bool,
43        #[arg(long, help = "Create a nightly release with date-based version")]
44        nightly: bool,
45        #[arg(long, help = "Run full pipeline without side effects")]
46        dry_run: bool,
47        #[arg(long, help = "Remove dist directory before starting")]
48        clean: bool,
49        #[arg(
50            long,
51            value_delimiter = ',',
52            help = "Skip stages (comma-separated, e.g. docker,announce)"
53        )]
54        skip: Vec<String>,
55        #[arg(
56            long,
57            help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
58        )]
59        token: Option<String>,
60        #[arg(
61            long,
62            default_value = "60m",
63            help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
64        )]
65        timeout: String,
66        #[arg(
67            long,
68            short = 'p',
69            help = "Maximum number of parallel build jobs (default: number of CPUs)"
70        )]
71        parallelism: Option<usize>,
72        #[arg(long, help = "Automatically set --snapshot if the git repo is dirty")]
73        auto_snapshot: bool,
74        #[arg(long, help = "Build only for the host target triple")]
75        single_target: bool,
76        #[arg(
77            long,
78            help = "Path to a custom release notes file (overrides changelog)"
79        )]
80        release_notes: Option<PathBuf>,
81        #[arg(
82            long,
83            conflicts_with = "crate_names",
84            help = "Release a specific workspace in a monorepo config"
85        )]
86        workspace: Option<String>,
87        #[arg(long, help = "Set the release as a draft")]
88        draft: bool,
89        #[arg(long, help = "Path to a file containing custom release header text")]
90        release_header: Option<PathBuf>,
91        #[arg(
92            long,
93            help = "Path to a template file for release header (rendered with template variables)"
94        )]
95        release_header_tmpl: Option<PathBuf>,
96        #[arg(long, help = "Path to a file containing custom release footer text")]
97        release_footer: Option<PathBuf>,
98        #[arg(
99            long,
100            help = "Path to a template file for release footer (rendered with template variables)"
101        )]
102        release_footer_tmpl: Option<PathBuf>,
103        #[arg(
104            long,
105            help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
106        )]
107        release_notes_tmpl: Option<PathBuf>,
108        #[arg(long, help = "Abort immediately on first error during publishing")]
109        fail_fast: bool,
110        #[arg(
111            long,
112            conflicts_with = "merge",
113            help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
114        )]
115        split: bool,
116        #[arg(
117            long,
118            conflicts_with = "split",
119            help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
120        )]
121        merge: bool,
122        #[arg(
123            long,
124            help = "Run local build + archive + sign + checksum + sbom stages but skip release / publish / announce (GoReleaser Pro parity). Artifacts stay in dist/ for inspection."
125        )]
126        prepare: bool,
127    },
128    /// Build binaries only (always runs in snapshot mode)
129    Build {
130        #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
131        crate_names: Vec<String>,
132        #[arg(
133            long,
134            default_value = "60m",
135            help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
136        )]
137        timeout: String,
138        #[arg(
139            long,
140            short = 'p',
141            help = "Maximum number of parallel build jobs (default: number of CPUs)"
142        )]
143        parallelism: Option<usize>,
144        #[arg(long, help = "Build only for the host target triple")]
145        single_target: bool,
146        #[arg(
147            long,
148            conflicts_with = "crate_names",
149            help = "Build a specific workspace in a monorepo config"
150        )]
151        workspace: Option<String>,
152        #[arg(
153            long,
154            short = 'o',
155            help = "Copy the built binary to this path (requires --single-target and single crate)"
156        )]
157        output: Option<PathBuf>,
158        #[arg(
159            long,
160            value_delimiter = ',',
161            help = "Skip stages (comma-separated: pre-hooks, post-hooks, validate, before)"
162        )]
163        skip: Vec<String>,
164    },
165    /// Validate configuration
166    Check {
167        #[arg(long, help = "Validate a specific workspace in a monorepo config")]
168        workspace: Option<String>,
169    },
170    /// Generate starter config
171    Init,
172    /// Generate changelog only
173    Changelog {
174        #[arg(long = "crate", help = "Generate changelog for a specific crate")]
175        crate_name: Option<String>,
176    },
177    /// Generate shell completions
178    Completion {
179        #[arg(value_enum, help = "Shell to generate completions for")]
180        shell: Shell,
181    },
182    /// Check availability of required external tools
183    Healthcheck,
184    /// Generate man pages to stdout
185    Man,
186    /// Output JSON Schema for .anodizer.yaml
187    Jsonschema,
188    /// Resolve a git tag to its matching crate in the config
189    ResolveTag {
190        #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
191        tag: String,
192        #[arg(long, help = "Output as JSON")]
193        json: bool,
194    },
195    /// Emit the configured build targets as a GitHub Actions matrix.
196    ///
197    /// Derives `{os, target, artifact}` entries from `.anodizer.yaml`. Used by
198    /// `tj-smith47/anodizer-action`'s `split-matrix` output to feed a
199    /// `strategy.matrix` dynamically (via `fromJson`).
200    Targets {
201        #[arg(long, help = "Output as JSON (include-form matrix)")]
202        json: bool,
203        #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
204        crate_names: Vec<String>,
205    },
206    /// Auto-tag based on commit message directives
207    Tag {
208        #[arg(long, help = "Show what tag would be created without pushing")]
209        dry_run: bool,
210        #[arg(long, help = "Override bump logic with a specific tag value")]
211        custom_tag: Option<String>,
212        #[arg(long, help = "Override default bump type (patch/minor/major)")]
213        default_bump: Option<String>,
214        #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
215        crate_name: Option<String>,
216    },
217    /// Continue a split release by merging artifacts and running post-build stages
218    Continue {
219        #[arg(
220            long,
221            help = "Merge artifacts from split build jobs and run post-build stages"
222        )]
223        merge: bool,
224        #[arg(long, help = "Custom dist directory (overrides config)")]
225        dist: Option<PathBuf>,
226        #[arg(long, help = "Run full pipeline without side effects")]
227        dry_run: bool,
228        #[arg(
229            long,
230            value_delimiter = ',',
231            help = "Skip stages (comma-separated, e.g. docker,announce)"
232        )]
233        skip: Vec<String>,
234        #[arg(
235            long,
236            help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
237        )]
238        token: Option<String>,
239    },
240    /// Run only the publish stages (release, publish, blob) from a completed dist/
241    Publish {
242        #[arg(long, help = "Run full pipeline without side effects")]
243        dry_run: bool,
244        #[arg(
245            long,
246            help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
247        )]
248        token: Option<String>,
249        #[arg(long, help = "Custom dist directory (overrides config)")]
250        dist: Option<PathBuf>,
251    },
252    /// Bump crate versions (Conventional Commits → semver level)
253    ///
254    /// Infers the per-crate level from commits since each crate's last tag
255    /// when no positional argument is given. `patch|minor|major`, an explicit
256    /// version, or `release` (strip prerelease) are also accepted.
257    Bump {
258        #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
259        level_or_version: Option<String>,
260        #[arg(
261            long,
262            short = 'p',
263            visible_alias = "crate",
264            action = clap::ArgAction::Append,
265            help = "Bump a specific crate (repeatable)"
266        )]
267        package: Vec<String>,
268        #[arg(
269            long,
270            alias = "all",
271            conflicts_with = "package",
272            help = "Bump every workspace member (excluding publish=false)"
273        )]
274        workspace: bool,
275        #[arg(
276            long,
277            action = clap::ArgAction::Append,
278            help = "Exclude a crate from --workspace (repeatable)"
279        )]
280        exclude: Vec<String>,
281        #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
282        pre: Option<String>,
283        #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
284        exact: bool,
285        #[arg(
286            long,
287            help = "Proceed even if the working tree has uncommitted changes"
288        )]
289        allow_dirty: bool,
290        #[arg(long, short = 'y', help = "Skip confirmation prompt")]
291        yes: bool,
292        #[arg(long, help = "Print the plan without editing any files")]
293        dry_run: bool,
294        #[arg(long, help = "Stage edits and create a single commit")]
295        commit: bool,
296        #[arg(
297            long,
298            requires = "commit",
299            help = "GPG-sign the commit (requires --commit)"
300        )]
301        sign: bool,
302        #[arg(long, help = "Override the default commit message template")]
303        commit_message: Option<String>,
304        #[arg(
305            long,
306            default_value = "text",
307            help = "Output format: text | json (json requires --dry-run)"
308        )]
309        output: String,
310    },
311    /// Run only the announce stage from a completed dist/
312    Announce {
313        #[arg(long, help = "Run full pipeline without side effects")]
314        dry_run: bool,
315        #[arg(long, help = "Custom dist directory (overrides config)")]
316        dist: Option<PathBuf>,
317        #[arg(
318            long,
319            help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
320        )]
321        token: Option<String>,
322        #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
323        skip: Vec<String>,
324    },
325}
326
327/// Detect the host target triple by parsing `rustc -vV` output.
328/// Delegates to `anodizer_core::partial::detect_host_target()`.
329pub fn detect_host_target() -> anyhow::Result<String> {
330    anodizer_core::partial::detect_host_target()
331}
332
333/// Return a sensible default parallelism value (number of logical CPUs, minimum 1).
334pub fn num_cpus() -> usize {
335    std::thread::available_parallelism()
336        .map(|n| n.get())
337        .unwrap_or(4)
338}
339
340/// Build the clap `Command` tree for CLI introspection.
341pub fn build_cli() -> clap::Command {
342    <Cli as clap::CommandFactory>::command()
343}