Skip to main content

anodizer_core/config/publishers/
mod.rs

1use std::path::PathBuf;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::aur_source::AurSourceConfig;
7use super::string_or_bool::HumanDuration;
8use super::{StringOrBool, deserialize_string_or_bool_opt};
9
10mod homebrew;
11pub use homebrew::*;
12
13mod chocolatey;
14pub use chocolatey::*;
15
16mod winget;
17pub use winget::*;
18
19mod aur;
20pub use aur::*;
21
22mod krew;
23pub use krew::*;
24
25mod nix;
26pub use nix::*;
27
28mod schemastore;
29pub use schemastore::{SchemaEntry, SchemaMode, SchemastoreConfig};
30
31// ---------------------------------------------------------------------------
32// Shared publisher config types: RepositoryConfig, CommitAuthorConfig
33// ---------------------------------------------------------------------------
34
35/// Shared repository configuration used by all git-based publishers
36/// (Homebrew, Scoop, Winget, Krew, Nix). A repository reference.
37#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
38#[serde(default, deny_unknown_fields)]
39pub struct RepositoryConfig {
40    /// Repository owner (GitHub user or organization).
41    pub owner: Option<String>,
42    /// Repository name.
43    pub name: Option<String>,
44    /// Auth token for the repository. Falls back to env-based resolution.
45    pub token: Option<String>,
46    /// Token type: "github" (default), "gitlab", "gitea".
47    pub token_type: Option<String>,
48    /// Branch to push to (default: repo default branch).
49    pub branch: Option<String>,
50    /// Git-specific settings for SSH-based publishing.
51    pub git: Option<GitRepoConfig>,
52    /// Pull request settings for fork-based workflows.
53    pub pull_request: Option<PullRequestConfig>,
54}
55
56/// Git-specific repository settings for SSH-based publishing.
57#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
58#[serde(default, deny_unknown_fields)]
59pub struct GitRepoConfig {
60    /// Git URL (e.g. `ssh://git@github.com/owner/repo.git`).
61    pub url: Option<String>,
62    /// Custom SSH command (e.g. `ssh -i /path/to/key`).
63    pub ssh_command: Option<String>,
64    /// Path to SSH private key file.
65    pub private_key: Option<String>,
66}
67
68/// Pull request configuration for fork-based publisher workflows.
69#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
70#[serde(default, deny_unknown_fields)]
71pub struct PullRequestConfig {
72    /// Enable PR creation instead of direct push.
73    pub enabled: Option<bool>,
74    /// Create PR as draft.
75    pub draft: Option<bool>,
76    /// Body text for the pull request.
77    pub body: Option<String>,
78    /// Target base repository/branch for the PR.
79    pub base: Option<PullRequestBaseConfig>,
80}
81
82/// Target base for pull requests (upstream repo to PR against).
83#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
84#[serde(default, deny_unknown_fields)]
85pub struct PullRequestBaseConfig {
86    /// Owner of the upstream repository to PR against.
87    pub owner: Option<String>,
88    /// Name of the upstream repository to PR against.
89    pub name: Option<String>,
90    /// Base branch of the upstream repository to target with the PR.
91    pub branch: Option<String>,
92}
93
94/// Shared commit author configuration with optional GPG/SSH signing.
95/// Commit-author identity for publisher commits.
96#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
97#[serde(default, deny_unknown_fields)]
98pub struct CommitAuthorConfig {
99    /// Git commit author display name.
100    pub name: Option<String>,
101    /// Git commit author email address.
102    pub email: Option<String>,
103    /// Commit signing configuration.
104    pub signing: Option<CommitSigningConfig>,
105    /// When true, omit the explicit `-c user.name=` / `-c user.email=`
106    /// overrides at commit time and let the running git client use the
107    /// invoking GitHub App's identity (i.e. the `<app-slug>[bot]@users.noreply.github.com`
108    /// account that the GitHub Actions checkout step has already configured
109    /// in the repo's local git config).
110    ///
111    /// The use-github-app-token toggle
112    /// uses the local git identity; the canonical use-case is
113    /// PRs against `homebrew/homebrew-core` / `kubernetes-sigs/krew-index`
114    /// / `microsoft/winget-pkgs` opened from a GitHub App workflow, where
115    /// EasyCLA / DCO / signed-commit policies require the App's identity
116    /// (rather than a per-user bot identity) to land the merge.
117    #[serde(default)]
118    pub use_github_app_token: bool,
119}
120
121impl CommitAuthorConfig {
122    /// Fill in the anodizer default name/email when either field is empty.
123    /// The commit-author defaulting, which
124    /// runs during the Default pass — so validation messages that reference
125    /// commit-author identity see non-empty strings rather than blanks.
126    pub fn normalize_defaults(&mut self) {
127        if self.name.as_deref().is_none_or(str::is_empty) {
128            self.name = Some("anodizer".to_string());
129        }
130        if self.email.as_deref().is_none_or(str::is_empty) {
131            self.email = Some("bot@anodizer.dev".to_string());
132        }
133    }
134}
135
136/// Commit signing configuration (GPG, x509, or SSH).
137#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
138#[serde(default, deny_unknown_fields)]
139pub struct CommitSigningConfig {
140    /// Enable commit signing.
141    pub enabled: Option<bool>,
142    /// Signing key identifier.
143    pub key: Option<String>,
144    /// Signing program (e.g. `gpg`, `gpg2`).
145    pub program: Option<String>,
146    /// Signing format: "openpgp" (default), "x509", or "ssh".
147    pub format: Option<String>,
148}
149
150// ---------------------------------------------------------------------------
151// PublishConfig
152// ---------------------------------------------------------------------------
153
154#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
155#[serde(default, deny_unknown_fields)]
156pub struct PublishConfig {
157    /// Publish to crates.io. Presence opts in; use `cargo: { skip: true }` to opt out.
158    pub cargo: Option<CargoPublishConfig>,
159    /// Homebrew formula publishing configuration.
160    pub homebrew: Option<HomebrewConfig>,
161    /// Homebrew Cask publishing configuration (macOS .app bundles).
162    ///
163    /// Uses the unified `HomebrewCaskConfig` which carries all fields from both
164    /// the per-crate cask config and the top-level `homebrew_casks:` config.
165    pub homebrew_cask: Option<HomebrewCaskConfig>,
166    /// Scoop manifest publishing configuration.
167    pub scoop: Option<ScoopConfig>,
168    /// Chocolatey package publishing configuration.
169    pub chocolatey: Option<ChocolateyConfig>,
170    /// WinGet manifest publishing configuration.
171    pub winget: Option<WingetConfig>,
172    /// AUR (Arch User Repository) binary package publishing configuration.
173    pub aur: Option<AurConfig>,
174    /// AUR source package publishing configuration (source-only PKGBUILD, not -bin).
175    pub aur_source: Option<AurSourceConfig>,
176    /// Krew (kubectl plugin manager) manifest publishing configuration.
177    pub krew: Option<KrewConfig>,
178    /// Nix derivation publishing configuration.
179    pub nix: Option<NixConfig>,
180}
181
182/// `cargo publish` flag surface.
183///
184/// Presence under `publish:` opts the crate in; use `skip: true` (or a
185/// truthy template) to opt out. There is no `enabled` field — presence is
186/// the on-switch.
187///
188/// Fields intentionally omitted because anodizer owns them:
189/// - `--package` / `--workspace` / `--exclude`: the top-level `crates[]`
190///   axis owns crate selection.
191/// - `--dry-run`: pipeline-level CLI ergonomics (`anodizer release --dry-run`).
192/// - `-v` / `-q` / `--color`: CLI ergonomics, not config.
193/// - `--config` / `-Z`: cargo CLI escape hatches; out of scope.
194#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
195#[serde(default, deny_unknown_fields)]
196pub struct CargoPublishConfig {
197    // ----- Registry selection -----
198    /// Alternate registry name from `~/.cargo/config.toml` (`--registry`).
199    pub registry: Option<String>,
200    /// Registry index URL (`--index`).
201    pub index: Option<String>,
202    /// Seconds to wait for the crates.io sparse index to publish a crate
203    /// before its dependents are pushed (anodizer-original — no `cargo
204    /// publish` equivalent).
205    pub index_timeout: Option<u64>,
206    /// Pre-publish gate that polls crates.io for every workspace-internal
207    /// dep of the crate being published, blocking until each is queryable
208    /// at its expected version. Required for multi-tag-multi-crate
209    /// workspaces (e.g. cfgd) where per-crate tags fire independent
210    /// `Release.yml` runs that would otherwise race the sparse-index
211    /// propagation.
212    ///
213    /// Single-crate workspaces and lockstep-bumped monorepos (anodizer
214    /// itself) leave this off — there is no inter-tag race to gate on.
215    pub wait_for_workspace_deps: Option<WaitForWorkspaceDepsConfig>,
216
217    // ----- Verify / dirty -----
218    /// Skip the local `cargo build --release` verification step (`--no-verify`).
219    pub no_verify: Option<bool>,
220    /// Allow publishing with an uncommitted working tree (`--allow-dirty`).
221    pub allow_dirty: Option<bool>,
222
223    // ----- Feature selection -----
224    /// Crate features to activate (`--features`).
225    pub features: Option<Vec<String>>,
226    /// Activate every feature, including `default` (`--all-features`).
227    pub all_features: Option<bool>,
228    /// Disable the `default` feature set (`--no-default-features`).
229    pub no_default_features: Option<bool>,
230
231    // ----- Compilation -----
232    /// Build target triple for the verification step (`--target`).
233    pub target: Option<String>,
234    /// Override the cargo target directory (`--target-dir`).
235    pub target_dir: Option<PathBuf>,
236    /// Number of parallel compile jobs for verification (`--jobs`).
237    pub jobs: Option<u32>,
238    /// Continue on errors when verifying multiple crates (`--keep-going`).
239    pub keep_going: Option<bool>,
240
241    // ----- Manifest -----
242    /// Path to the crate's `Cargo.toml` (`--manifest-path`).
243    pub manifest_path: Option<PathBuf>,
244    /// Require an up-to-date `Cargo.lock` matching the resolver (`--locked`).
245    pub locked: Option<bool>,
246    /// Require offline resolution; never hit the network (`--offline`).
247    pub offline: Option<bool>,
248    /// Both `--locked` and `--offline` (`--frozen`).
249    pub frozen: Option<bool>,
250
251    // ----- Peer-publisher pattern -----
252    /// Skip this publisher; supports template strings or bool.
253    /// Truthy renders disable the publisher without removing the block.
254    #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
255    pub skip: Option<StringOrBool>,
256    /// Override whether this publisher failing should fail the overall release.
257    ///
258    /// Default: `true` — a failure here aborts the release.
259    /// Set to `false` to log failures but continue.
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub required: Option<bool>,
262    /// Template-conditional gate: when the rendered result is falsy
263    /// (`"false"` / `"0"` / `"no"` / empty), the cargo publisher is
264    /// skipped. Render failure hard-errors. Config key: the publisher's `if:`.
265    #[serde(rename = "if")]
266    pub if_condition: Option<String>,
267}
268
269/// Pre-publish polling gate for `cargo publish`. When `enabled`, the cargo
270/// publisher reads its crate's manifest, identifies every dep that points
271/// at another crate in the same anodize workspace, and polls
272/// `https://index.crates.io/<prefix>/<name>` until each `(name, version)`
273/// pair is queryable. Only then does `cargo publish` run.
274///
275/// Default: disabled. Anodize's own workspaces publish lockstep with one
276/// tag; this feature only kicks in for multi-tag-multi-crate workspaces
277/// like cfgd where downstream crates can otherwise race the sparse-index
278/// propagation of their upstream deps.
279///
280/// Complementary to `cargo.index_timeout`: this gate runs BEFORE publish
281/// (waits for *upstream* deps to land), while `index_timeout` runs AFTER
282/// publish (waits for the *just-published* crate to land before the next
283/// dependent in the same run starts).
284///
285/// ```yaml
286/// publish:
287///   cargo:
288///     wait_for_workspace_deps:
289///       enabled: true
290///       poll_interval: 5s
291///       max_wait: 5m
292/// ```
293#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
294#[serde(default, deny_unknown_fields)]
295pub struct WaitForWorkspaceDepsConfig {
296    /// Master switch. Default `false` preserves today's behavior for
297    /// single-crate workspaces and lockstep monorepos.
298    pub enabled: Option<bool>,
299    /// Time between successive index probes. Humantime-style string
300    /// (e.g. `"5s"`, `"500ms"`, `"1m"`). Default: `"5s"`.
301    pub poll_interval: Option<HumanDuration>,
302    /// Hard ceiling on the total wait. The publisher bails with a clear
303    /// error once `max_wait` elapses without every dep appearing.
304    /// Humantime-style string (e.g. `"5m"`, `"30s"`). Default: `"5m"`.
305    pub max_wait: Option<HumanDuration>,
306}
307
308impl WaitForWorkspaceDepsConfig {
309    /// Default poll interval — short enough to feel snappy when the
310    /// upstream's publish lands quickly, long enough that a 5-minute
311    /// wait window costs at most 60 HTTP probes.
312    pub const DEFAULT_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
313
314    /// Default ceiling — five minutes matches the historical
315    /// `index_timeout` default and covers the worst-case sparse-index
316    /// CDN propagation window observed in practice.
317    pub const DEFAULT_MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(300);
318
319    /// Resolve `enabled`, defaulting to `false` (master switch off).
320    pub fn resolved_enabled(&self) -> bool {
321        self.enabled.unwrap_or(false)
322    }
323
324    /// Resolve `poll_interval`, falling back to
325    /// [`Self::DEFAULT_POLL_INTERVAL`].
326    pub fn resolved_poll_interval(&self) -> std::time::Duration {
327        self.poll_interval
328            .map(|d| d.duration())
329            .unwrap_or(Self::DEFAULT_POLL_INTERVAL)
330    }
331
332    /// Resolve `max_wait`, falling back to [`Self::DEFAULT_MAX_WAIT`].
333    pub fn resolved_max_wait(&self) -> std::time::Duration {
334        self.max_wait
335            .map(|d| d.duration())
336            .unwrap_or(Self::DEFAULT_MAX_WAIT)
337    }
338}