Skip to main content

anodizer_core/config/publishers/
homebrew.rs

1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::super::{Amd64Variant, StringOrBool, deserialize_string_or_bool_opt};
7use super::{CommitAuthorConfig, RepositoryConfig};
8
9// ---------------------------------------------------------------------------
10// HomebrewConfig / ScoopConfig / TapConfig / BucketConfig
11// ---------------------------------------------------------------------------
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
14#[serde(default, deny_unknown_fields)]
15pub struct HomebrewConfig {
16    /// Unified repository config with branch, token, PR, git SSH support.
17    /// (Replaces the legacy `tap: TapConfig` owner/name-only form.)
18    pub repository: Option<RepositoryConfig>,
19    /// Commit author with optional signing.
20    pub commit_author: Option<CommitAuthorConfig>,
21    /// Formula directory in the tap (e.g. "Formula").
22    pub directory: Option<String>,
23    /// Override the formula name (default: crate name).
24    pub name: Option<String>,
25    /// Short description of the formula (shown in `brew info`).
26    pub description: Option<String>,
27    /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
28    pub license: Option<String>,
29    /// Ruby `install` block content for the formula.
30    pub install: Option<String>,
31    /// Additional install commands appended after the main install block.
32    pub extra_install: Option<String>,
33    /// Post-install commands (separate `def post_install` block in formula).
34    pub post_install: Option<String>,
35    /// Ruby `test` block content for the formula (run by `brew test`).
36    pub test: Option<String>,
37    /// Project homepage URL. Falls back to the GitHub release URL when unset.
38    pub homepage: Option<String>,
39    /// Package dependencies (e.g. `openssl`, `libgit2`).
40    pub dependencies: Option<Vec<HomebrewDependency>>,
41    /// Conflicting formula names with optional reason.
42    pub conflicts: Option<Vec<HomebrewConflict>>,
43    /// Post-install user-facing notes shown by `brew info`.
44    pub caveats: Option<String>,
45    /// Skip publishing the formula.  `"true"` always skips; `"auto"` skips
46    /// for prerelease versions. Accepts bool or template string.
47    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
48    pub skip_upload: Option<StringOrBool>,
49    /// Custom commit message template. Rendered via Tera with the standard
50    /// release template variables (`ProjectName`, `Tag`, `Version`, etc.).
51    /// Default: `"Brew formula update for {{ ProjectName }} version {{ Tag }}"`
52    /// (set in `crates/stage-publish/src/homebrew.rs::default_commit_msg_template`).
53    pub commit_msg_template: Option<String>,
54    // Legacy flat `commit_author_name` / `commit_author_email` fields are
55    // gone; use the structured `commit_author: { name, email, signing }`.
56    /// Build IDs filter: only include artifacts whose `id` is in this list.
57    pub ids: Option<Vec<String>>,
58    /// Custom URL template for download URLs (overrides release URL).
59    pub url_template: Option<String>,
60    /// HTTP headers to include in download requests (e.g. for private repos).
61    pub url_headers: Option<Vec<String>>,
62    /// Custom download strategy class name (e.g. `:using => GitHubPrivateRepositoryReleaseDownloadStrategy`).
63    pub download_strategy: Option<String>,
64    /// Ruby `require` statement for custom download strategies.
65    pub custom_require: Option<String>,
66    /// Custom Ruby code block inserted into the formula class body.
67    pub custom_block: Option<String>,
68    /// Launchd plist content for `brew services`.
69    pub plist: Option<String>,
70    /// Homebrew service block content (alternative to plist).
71    pub service: Option<String>,
72    /// Manpage file paths to install into the formula's `man1` (e.g.
73    /// `["mytool.1"]`). Each entry renders a `man1.install "<path>"` line in
74    /// the install block, mirroring real Rust-CLI formulae (ripgrep, fd, bat).
75    /// A path ending in `.N` (where N is 1–8) routes to the matching `manN`
76    /// section; anything else defaults to `man1`.
77    pub manpages: Option<Vec<String>>,
78    /// Prebuilt shell-completion file paths to install. When set, the formula
79    /// emits `bash_completion.install "<path>"` / `zsh_completion.install` /
80    /// `fish_completion.install` in its install block — the form used when the
81    /// archive ships ready-made completion files.
82    pub completions: Option<HomebrewCaskCompletions>,
83    /// Generate completions by running the installed binary at install time.
84    /// Renders the modern homebrew-core idiom
85    /// `generate_completions_from_executable(bin/"<exe>", ...)` in the install
86    /// block. Preferred over `completions` when the binary can emit its
87    /// own completions; the two are independent and may both be set.
88    pub generate_completions_from_executable: Option<HomebrewCaskGeneratedCompletions>,
89    /// `livecheck` stanza configuration for the formula. When unset, a binary
90    /// tap formula emits `livecheck { skip "Auto-generated on release." }` to
91    /// match the cask (the archive URL/sha are rewritten on every release, so
92    /// `brew livecheck` cannot meaningfully poll). Set `strategy:` /
93    /// `regex:`/`url:` to opt into active version detection instead.
94    pub livecheck: Option<HomebrewLivecheck>,
95    /// Homebrew Cask configuration (macOS .app bundles).
96    pub cask: Option<HomebrewCaskConfig>,
97    /// amd64 microarchitecture variant filter (`v1` / `v2` / `v3` / `v4`).
98    /// Only artifacts matching this variant are included. Default: `v1`.
99    /// Typed as [`Amd64Variant`], so any value outside `v1`..`v4` is
100    /// rejected when the config is parsed.
101    pub amd64_variant: Option<Amd64Variant>,
102    /// ARM version filter (e.g. "6", "7"). Only artifacts matching this
103    /// variant are included.
104    pub arm_variant: Option<String>,
105    /// Override whether this publisher failing should fail the overall release.
106    ///
107    /// Default: `false` — a failure here is logged but does not abort the release.
108    /// Set to `true` to fail the release on any error.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub required: Option<bool>,
111    /// Template-conditional gate: when the rendered result is falsy
112    /// (`"false"` / `"0"` / `"no"` / empty), the Homebrew publisher is
113    /// skipped. Render failure hard-errors. Config key: `brews[].if:`.
114    #[serde(rename = "if")]
115    pub if_condition: Option<String>,
116    /// When `true`, a triggered rollback leaves this publisher's work in
117    /// place rather than attempting to undo it. Default `false`.
118    pub retain_on_rollback: Option<bool>,
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
122#[serde(default, deny_unknown_fields)]
123pub struct HomebrewDependency {
124    /// Homebrew formula name of the dependency.
125    pub name: String,
126    /// Restrict to a specific OS: `"mac"` or `"linux"`.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub os: Option<String>,
129    /// Dependency type, e.g. `"optional"`.
130    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
131    pub dep_type: Option<String>,
132    /// Version constraint for the dependency (e.g. `">= 1.1"`).
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub version: Option<String>,
135}
136
137/// A Homebrew conflict entry, supporting both a bare name string and a
138/// structured object with an optional `because` reason.
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
140#[serde(untagged)]
141pub enum HomebrewConflict {
142    /// Just the formula name (e.g. `"other-tool"`).
143    Name(String),
144    /// Name with reason (e.g. `{name: "other-tool", because: "both install a bin/foo binary"}`).
145    WithReason {
146        name: String,
147        #[serde(skip_serializing_if = "Option::is_none")]
148        because: Option<String>,
149    },
150}
151
152impl HomebrewConflict {
153    pub fn name(&self) -> &str {
154        match self {
155            Self::Name(n) => n,
156            Self::WithReason { name, .. } => name,
157        }
158    }
159    pub fn because(&self) -> Option<&str> {
160        match self {
161            Self::Name(_) => None,
162            Self::WithReason { because, .. } => because.as_deref(),
163        }
164    }
165}
166
167/// `livecheck` stanza configuration for a Homebrew formula.
168///
169/// Default (the struct absent from config): the formula emits a
170/// `livecheck { skip "Auto-generated on release." }` block — correct for a
171/// binary tap whose archive URL/sha256 are rewritten every release. To opt
172/// into active version polling, set `skip: false` and a `strategy:` (and
173/// optionally `url:` / `regex:`):
174///
175/// ```yaml
176/// livecheck:
177///   strategy: github_latest
178/// ```
179///
180/// renders:
181///
182/// ```ruby
183/// livecheck do
184///   url :stable
185///   strategy :github_latest
186/// end
187/// ```
188#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
189#[serde(default, deny_unknown_fields)]
190pub struct HomebrewLivecheck {
191    /// Force the `skip "<reason>"` form even when a strategy is set, or set to
192    /// `false` to opt out of the default skip and emit an active livecheck.
193    /// When `None` (the default), the formula skips iff no `strategy`/`url`
194    /// is configured.
195    pub skip: Option<bool>,
196    /// Reason text for the `skip "<reason>"` form.
197    /// Default: `"Auto-generated on release."`.
198    pub skip_reason: Option<String>,
199    /// `livecheck` strategy symbol (e.g. `github_latest`, `git`, `page_match`).
200    /// Rendered as a Ruby symbol: `strategy :github_latest`.
201    pub strategy: Option<String>,
202    /// `url` for the livecheck. Accepts a Ruby symbol shorthand
203    /// (`stable` / `head` / `homepage` → `url :stable`) or a literal URL
204    /// string (`url "https://..."`). Defaults to `:stable` when a strategy
205    /// is set without an explicit url.
206    pub url: Option<String>,
207    /// `regex(...)` argument for `page_match`-style strategies. Emitted
208    /// verbatim inside `regex(...)`, so it is raw Ruby (e.g. `%r{v(\d+\.\d+)}i`).
209    pub regex: Option<String>,
210}
211
212/// Unified Homebrew Cask configuration.
213///
214/// Used at both call-sites:
215/// - `homebrew_casks:` — top-level array; carries `repository`,
216///   `commit_author`, `directory`, `ids`, `url`, structured `uninstall`/`zap`, etc.
217/// - `crates[].publish.homebrew_cask:` — per-crate override; same shape, with
218///   `url_template` as the simpler URL alternative.
219///
220/// Fields from both original types are present; any field may be `None` at either
221/// call-site. The union avoids a two-type bifurcation while keeping both axes.
222#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
223#[serde(default, deny_unknown_fields)]
224pub struct HomebrewCaskConfig {
225    // ----- Identity -----
226    /// Cask name (default: crate / project name).
227    pub name: Option<String>,
228    /// Alternative cask names (aliases).
229    pub alternative_names: Option<Vec<String>>,
230
231    // ----- Tap repository (top-level axis) -----
232    /// Unified repository config for the Homebrew tap.
233    pub repository: Option<RepositoryConfig>,
234    /// Commit author with optional signing.
235    pub commit_author: Option<CommitAuthorConfig>,
236    /// Custom commit message template.
237    /// Default: "Brew cask update for {{ ProjectName }} version {{ Tag }}"
238    pub commit_msg_template: Option<String>,
239    /// Subdirectory in the tap repo for cask placement (default: "Casks").
240    pub directory: Option<String>,
241
242    // ----- Artifact selection -----
243    /// Build IDs filter: only include artifacts from builds whose `id` is in this list.
244    pub ids: Option<Vec<String>>,
245
246    // ----- Download URL -----
247    /// Simple URL template for the .dmg/.zip download (per-crate shorthand).
248    ///
249    /// Cannot be combined with `url.template:` — set one or the other.
250    /// If both are present, config validation rejects the config at parse time.
251    /// Use `url:` for the structured form (verified domain, custom headers, etc.)
252    /// or `url_template:` for a bare string shorthand — never both simultaneously.
253    pub url_template: Option<String>,
254    /// Structured download URL configuration (top-level axis).
255    pub url: Option<HomebrewCaskURL>,
256
257    // ----- macOS bundle -----
258    /// macOS .app bundle name (e.g. "MyApp.app").
259    pub app: Option<String>,
260    /// Binary stubs to create in /usr/local/bin.
261    ///
262    /// Each entry is either a bare string (`"my-cli"` → emits
263    /// `binary "my-cli"`) or a structured `{ name, target }` object
264    /// (`{ name: "my-cli", target: "mycli" }` → emits
265    /// `binary "my-cli", target: "mycli"`). The `target:` form mirrors
266    /// the Homebrew Ruby cask DSL for binary renames — without it, a
267    /// wrapped binary installs at the wrong path.
268    /// Cask binary entry.
269    pub binaries: Option<Vec<HomebrewCaskBinary>>,
270    /// Deprecated singular spelling of [`Self::binaries`]. The upstream
271    /// replaced `binary: foo` with `binaries: [foo]`; this field captures the
272    /// legacy spelling so imported configs keep parsing.
273    /// [`apply_homebrew_cask_legacy_singulars`](super::super::apply_homebrew_cask_legacy_singulars)
274    /// folds the value into [`Self::binaries`] at config-load time and emits
275    /// a one-time deprecation warning per occurrence. The field is excluded
276    /// from serialization so a round-tripped config emits only the canonical
277    /// plural form.
278    #[serde(default, rename = "binary", skip_serializing)]
279    pub legacy_binary: Option<String>,
280
281    // ----- Metadata -----
282    /// Cask description.
283    pub description: Option<String>,
284    /// Project homepage URL.
285    pub homepage: Option<String>,
286    /// License identifier (SPDX).
287    pub license: Option<String>,
288    /// Custom caveats shown after install.
289    pub caveats: Option<String>,
290
291    // ----- Ruby block -----
292    /// Arbitrary Ruby code inserted into the cask block.
293    pub custom_block: Option<String>,
294    /// Homebrew service definition.
295    pub service: Option<String>,
296
297    // ----- Livecheck -----
298    /// `livecheck` stanza configuration for the cask. When unset, the cask
299    /// emits `livecheck do\n  skip "Auto-generated on release."\nend` (a
300    /// binary cask's download URL/sha256 are rewritten on every release, so
301    /// `brew livecheck` has nothing stable to poll). Set `strategy:` /
302    /// `url:` / `regex:` (with `skip: false`) to opt into active version
303    /// detection — the same shape a Homebrew cask `livecheck do … end`
304    /// block accepts. Reuses the formula `livecheck` config type.
305    pub livecheck: Option<HomebrewLivecheck>,
306
307    // ----- Completions / manpages -----
308    /// Manual page references to install.
309    pub manpages: Option<Vec<String>>,
310    /// Deprecated singular spelling of [`Self::manpages`]. The upstream replaced
311    /// `manpage: foo.1` with `manpages: [foo.1]`; this field captures the
312    /// legacy spelling so imported configs keep parsing.
313    /// [`apply_homebrew_cask_legacy_singulars`](super::super::apply_homebrew_cask_legacy_singulars)
314    /// folds the value into [`Self::manpages`] at config-load time and emits
315    /// a one-time deprecation warning per occurrence. The field is excluded
316    /// from serialization so a round-tripped config emits only the canonical
317    /// plural form.
318    #[serde(default, rename = "manpage", skip_serializing)]
319    pub legacy_manpage: Option<String>,
320    /// Shell completion definitions.
321    pub completions: Option<HomebrewCaskCompletions>,
322    /// Auto-generate shell completions from an executable.
323    pub generate_completions_from_executable: Option<HomebrewCaskGeneratedCompletions>,
324
325    // ----- Dependencies / conflicts -----
326    /// Cask dependencies (other casks or formulae).
327    pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
328    /// Conflicting casks or formulae.
329    pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
330
331    // ----- Lifecycle hooks -----
332    /// Pre/post install/uninstall hooks.
333    pub hooks: Option<HomebrewCaskHooks>,
334
335    // ----- Uninstall / zap -----
336    /// Structured uninstall stanza configuration.
337    pub uninstall: Option<HomebrewCaskUninstall>,
338    /// Deep uninstall (zap) stanza configuration.
339    pub zap: Option<HomebrewCaskUninstall>,
340
341    // ----- Publishing control -----
342    /// Skip publishing the cask. `"true"` always skips; `"auto"` skips
343    /// for prerelease versions. Accepts bool or template string.
344    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
345    pub skip_upload: Option<StringOrBool>,
346    /// When true, force-push the updated cask file to the existing PR branch
347    /// when a PR for the same head branch already exists. The PR content is
348    /// updated in place rather than creating a duplicate. When false (default),
349    /// the push is skipped and a warning is emitted so the operator sees that
350    /// the publisher did not update the PR.
351    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
352    pub update_existing_pr: Option<StringOrBool>,
353    /// Override whether this publisher failing should fail the overall release.
354    ///
355    /// Default: `false` — a failure here is logged but does not abort the release.
356    /// Set to `true` to fail the release on any error.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub required: Option<bool>,
359    /// Template-conditional gate: when the rendered result is falsy
360    /// (`"false"` / `"0"` / `"no"` / empty), the Homebrew Cask config is
361    /// skipped. Render failure hard-errors. Config key: `homebrew_casks[].if:`.
362    #[serde(rename = "if")]
363    pub if_condition: Option<String>,
364    /// When `true`, a triggered rollback leaves this publisher's work in
365    /// place rather than attempting to undo it. Default `false`.
366    pub retain_on_rollback: Option<bool>,
367}
368
369/// Structured URL configuration for Homebrew Cask downloads.
370#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
371#[serde(default, deny_unknown_fields)]
372pub struct HomebrewCaskURL {
373    /// URL template for the download.
374    pub template: Option<String>,
375    /// Verification string (domain shown to user).
376    pub verified: Option<String>,
377    /// Custom downloader (e.g. `:homebrew_curl`, `:post`).
378    pub using: Option<String>,
379    /// HTTP cookies for the download.
380    pub cookies: Option<HashMap<String, String>>,
381    /// Referer header for the download.
382    pub referer: Option<String>,
383    /// Custom HTTP headers.
384    pub headers: Option<Vec<String>>,
385    /// Custom user agent string.
386    pub user_agent: Option<String>,
387    /// POST data for form submissions.
388    pub data: Option<HashMap<String, String>>,
389}
390
391/// Structured uninstall/zap configuration for Homebrew Cask.
392/// Used for both `uninstall` and `zap` stanzas.
393#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
394#[serde(default, deny_unknown_fields)]
395pub struct HomebrewCaskUninstall {
396    /// Launch daemon/agent identifiers to stop.
397    pub launchctl: Option<Vec<String>>,
398    /// Application bundle IDs to quit.
399    pub quit: Option<Vec<String>>,
400    /// Login item names to remove.
401    pub login_item: Option<Vec<String>>,
402    /// File paths to delete.
403    pub delete: Option<Vec<String>>,
404    /// File paths to trash (preserves app state).
405    pub trash: Option<Vec<String>>,
406}
407
408/// Pre/post install/uninstall hooks for Homebrew Cask.
409#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
410#[serde(default, deny_unknown_fields)]
411pub struct HomebrewCaskHooks {
412    /// Pre-install/uninstall hooks.
413    pub pre: Option<HomebrewCaskHook>,
414    /// Post-install/uninstall hooks.
415    pub post: Option<HomebrewCaskHook>,
416}
417
418/// Individual hook for install/uninstall phases.
419#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
420#[serde(default, deny_unknown_fields)]
421pub struct HomebrewCaskHook {
422    /// Ruby code for preflight/postflight during install.
423    pub install: Option<String>,
424    /// Ruby code for uninstall_preflight/uninstall_postflight.
425    pub uninstall: Option<String>,
426}
427
428/// Shell completion file paths for Homebrew Cask.
429#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
430#[serde(default, deny_unknown_fields)]
431pub struct HomebrewCaskCompletions {
432    /// Path to bash completion file.
433    pub bash: Option<String>,
434    /// Path to zsh completion file.
435    pub zsh: Option<String>,
436    /// Path to fish completion file.
437    pub fish: Option<String>,
438}
439
440/// Cask `binary` stanza entry.
441///
442/// Two shapes accepted in YAML:
443/// - bare string — `"my-cli"` → renders `binary "my-cli"`.
444/// - `{ name, target }` object — `{ name: "my-cli", target: "mycli" }`
445///   → renders `binary "my-cli", target: "mycli"`. The `target:` form is
446///   the Homebrew Ruby cask DSL rename: install the symlink at
447///   `/usr/local/bin/<target>` instead of `/usr/local/bin/<name>`.
448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
449#[serde(untagged)]
450pub enum HomebrewCaskBinary {
451    /// Bare binary name. Equivalent to `{ name: "<n>", target: None }`.
452    Name(String),
453    /// Structured `{ name, target }` rename form.
454    WithTarget {
455        /// Path inside the .app bundle (e.g. `"my-cli"`).
456        name: String,
457        /// Optional rename target — the symlink name in `/usr/local/bin`.
458        /// When `None`, the symlink uses `name`.
459        #[serde(skip_serializing_if = "Option::is_none")]
460        target: Option<String>,
461    },
462}
463
464impl HomebrewCaskBinary {
465    /// The binary name (the path inside the .app bundle).
466    pub fn name(&self) -> &str {
467        match self {
468            Self::Name(n) => n,
469            Self::WithTarget { name, .. } => name,
470        }
471    }
472    /// The optional rename target. `None` for bare-string entries and for
473    /// `{ name, target }` objects without `target` set.
474    pub fn target(&self) -> Option<&str> {
475        match self {
476            Self::Name(_) => None,
477            Self::WithTarget { target, .. } => target.as_deref(),
478        }
479    }
480}
481
482/// Cask dependency (on another cask or formula).
483#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
484#[serde(default, deny_unknown_fields)]
485pub struct HomebrewCaskDependencyEntry {
486    /// Dependent cask name.
487    pub cask: Option<String>,
488    /// Dependent formula name.
489    pub formula: Option<String>,
490}
491
492/// Cask conflict (with another cask or formula).
493#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
494#[serde(default, deny_unknown_fields)]
495pub struct HomebrewCaskConflictEntry {
496    /// Conflicting cask name.
497    pub cask: Option<String>,
498    /// Conflicting formula name (deprecated by Homebrew).
499    pub formula: Option<String>,
500}
501
502/// Auto-generate shell completions from an executable.
503#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
504#[serde(default, deny_unknown_fields)]
505pub struct HomebrewCaskGeneratedCompletions {
506    /// Binary to generate completions from.
507    pub executable: Option<String>,
508    /// Arguments to pass to the executable.
509    pub args: Option<Vec<String>>,
510    /// Base name for completion files.
511    pub base_name: Option<String>,
512    /// Shell completion framework type (arg, clap, click, cobra, flag, none, typer).
513    pub shell_parameter_format: Option<String>,
514    /// Target shells (bash, zsh, fish, pwsh).
515    pub shells: Option<Vec<String>>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
519#[serde(default, deny_unknown_fields)]
520pub struct ScoopConfig {
521    /// Unified repository config with branch, token, PR, git SSH support.
522    /// (Replaces the legacy `bucket: BucketConfig` owner/name-only form.)
523    pub repository: Option<RepositoryConfig>,
524    /// Commit author with optional signing.
525    pub commit_author: Option<CommitAuthorConfig>,
526    /// Override the manifest name (default: crate name).
527    pub name: Option<String>,
528    /// Subdirectory in the bucket repo for manifest placement. Defaults to
529    /// `bucket` — scoop resolves manifests only from `bucket/` when that
530    /// directory exists (root otherwise), so `bucket/` is correct for both
531    /// layouts. Set to `""` to target the repo root. A stale same-named
532    /// root-level manifest is removed when publishing into a subdirectory.
533    pub directory: Option<String>,
534    /// Short description of the package (shown in `scoop info`).
535    pub description: Option<String>,
536    /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
537    pub license: Option<String>,
538    /// Project homepage URL. Falls back to the GitHub-derived URL when unset.
539    pub homepage: Option<String>,
540    /// Data paths persisted between Scoop updates.
541    pub persist: Option<Vec<String>>,
542    /// Application dependencies (other Scoop packages).
543    pub depends: Option<Vec<String>>,
544    /// Commands to run before installation.
545    pub pre_install: Option<Vec<String>>,
546    /// Commands to run after installation.
547    pub post_install: Option<Vec<String>>,
548    /// Start menu shortcuts as `[executable, label]` pairs.
549    pub shortcuts: Option<Vec<Vec<String>>>,
550    /// Scoop `checkver` strategy used by bucket maintainers to detect new
551    /// releases. Defaults to `"github"` (derived from the configured GitHub
552    /// repo) — `ScoopInstaller/Main` requires checkver for automated-update
553    /// PRs. Override with a homepage regex when GitHub release detection is
554    /// not appropriate.
555    ///
556    /// Example: `checkver: "github"` or `checkver: "v([\\d.]+)"`.
557    pub checkver: Option<String>,
558    /// Skip publishing the manifest.  `"true"` always skips; `"auto"` skips
559    /// for prerelease versions. Accepts bool or template string.
560    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
561    pub skip_upload: Option<StringOrBool>,
562    /// Custom commit message template.
563    pub commit_msg_template: Option<String>,
564    // Use the structured `commit_author: { name, email, signing }` form for
565    // commit author identity (legacy flat `commit_author_name` /
566    // `commit_author_email` fields are not accepted).
567    /// Build IDs filter: only include artifacts whose `id` is in this list.
568    pub ids: Option<Vec<String>>,
569    /// Custom URL template for download URLs (overrides release URL).
570    pub url_template: Option<String>,
571    /// Artifact selection: "archive" (default), "msi", or "nsis".
572    #[serde(rename = "use")]
573    pub use_artifact: Option<String>,
574    /// amd64 microarchitecture variant filter (`v1` / `v2` / `v3` / `v4`).
575    /// Only artifacts matching this variant are included. Default: `v1`.
576    /// Typed as [`Amd64Variant`], so any value outside `v1`..`v4` is
577    /// rejected when the config is parsed.
578    pub amd64_variant: Option<Amd64Variant>,
579    /// Override whether this publisher failing should fail the overall release.
580    ///
581    /// Default: `false` — a failure here is logged but does not abort the release.
582    /// Set to `true` to fail the release on any error.
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub required: Option<bool>,
585    /// Template-conditional gate: when the rendered result is falsy
586    /// (`"false"` / `"0"` / `"no"` / empty), the Scoop publisher is
587    /// skipped. Render failure hard-errors. Config key: `scoop[].if:`.
588    #[serde(rename = "if")]
589    pub if_condition: Option<String>,
590    /// When `true`, a triggered rollback leaves this publisher's work in
591    /// place rather than attempting to undo it. Default `false`.
592    pub retain_on_rollback: Option<bool>,
593}
594
595// `TapConfig` / `BucketConfig` (legacy {owner, name}-only repo types) live
596// nowhere — every publisher now carries `repository: RepositoryConfig`
597// with the broader feature set (token / branch / git SSH / pull_request).