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}