Skip to main content

anodizer_core/context/
options.rs

1use super::*;
2
3pub struct ContextOptions {
4    pub snapshot: bool,
5    pub nightly: bool,
6    pub dry_run: bool,
7    pub quiet: bool,
8    pub verbose: bool,
9    pub debug: bool,
10    pub skip_stages: Vec<String>,
11    /// `--publishers`: per-publisher allowlist. Empty means "no allowlist" —
12    /// every publisher runs (subject to `skip_stages`). Non-empty restricts
13    /// the publish stage to exactly the named publishers. Entries are
14    /// canonical publisher names (`Publisher::name()`, e.g. `npm`, `cargo`).
15    /// Orthogonal to `selected_crates` (which scopes crates, not publishers)
16    /// and to `skip_stages` (the unified denylist, which always wins — see
17    /// [`Context::publisher_deselected`]).
18    pub publisher_allowlist: Vec<String>,
19    pub selected_crates: Vec<String>,
20    pub token: Option<String>,
21    /// Maximum number of parallel build jobs (minimum 1).
22    pub parallelism: usize,
23    /// When set, build only for this single host target triple.
24    pub single_target: Option<String>,
25    /// Path to a custom release notes file (overrides changelog).
26    pub release_notes_path: Option<PathBuf>,
27    /// When true, abort immediately on first error during publishing.
28    pub fail_fast: bool,
29    /// Partial build target for split/merge mode. When set, the build stage
30    /// filters targets to only those matching this partial target.
31    pub partial_target: Option<PartialTarget>,
32    /// When true, running with `--merge` flag (merging artifacts from split builds).
33    pub merge: bool,
34    /// `--publish-only`: load artifacts from a preserved dist (written
35    /// by `anodize check determinism --preserve-dist=...`) and run
36    /// only the sign + publish pipeline. The CLI dispatcher uses this
37    /// flag in `setup_env` to defer the GitHub-token check to the
38    /// config-derived environment preflight (the github-release
39    /// publisher's token ladder plus the sign stage's `KeyEnv`
40    /// requirements), which validates token and sign-key material in
41    /// one collect-all pass and bails fail-closed on missing values.
42    /// Without this deferral, `setup_env`'s token check would fire FIRST
43    /// and pre-empt that richer, per-publisher preflight.
44    pub publish_only: bool,
45    /// `--preflight-secrets`: a check-only secrets gate. Like
46    /// [`Self::publish_only`], it defers `setup_env`'s GitHub-token hard
47    /// error to the config-derived environment preflight (run in
48    /// `SecretsOnly` scope), which validates the token ladder alongside
49    /// every other runner-agnostic credential and then exits with zero
50    /// mutations. Without this deferral, `setup_env` would bail on the
51    /// missing token before the secrets gate could report the full set.
52    pub preflight_secrets: bool,
53    /// Explicit project root directory. When set, stages use this instead of
54    /// discovering the repo root via `git rev-parse --show-toplevel`.
55    pub project_root: Option<PathBuf>,
56    /// Strict mode: configured features that would silently skip become errors.
57    pub strict: bool,
58    /// `--strict-preflight`: preflight-scoped strictness. Promotes
59    /// indeterminate publisher-state / probe outcomes (Unknown state, 5xx /
60    /// rate-limit / network failure / undeterminable permissions) to hard
61    /// blockers without widening the global `--strict` semantics. Effective
62    /// preflight strictness ([`Context::preflight_is_strict`]) ORs this with
63    /// `strict` and the config-level `preflight.strict`.
64    pub strict_preflight: bool,
65    /// `--resume-release`: opt-in to continue into a release left over from
66    /// a prior failed attempt. Bypasses the leftover-assets pre-check that
67    /// bails when an existing release already has assets and
68    /// `replace_existing_artifacts` is false.
69    pub resume_release: bool,
70    /// `--replace-existing`: CLI override that forces
71    /// `release.replace_existing_artifacts: true` regardless of config.
72    /// The release stage ORs this with the config value.
73    pub replace_existing_artifacts: bool,
74    /// `--no-post-publish-poll`: skip post-publish polling for the
75    /// chocolatey moderation queue and the winget PR validation pipeline.
76    /// When `true`, the polling runner emits `PostPublishStatus::NotPolled`
77    /// (pending immediately) for every publisher rather than waiting on a
78    /// terminal state. Lets CI users with no patience for long-running
79    /// waits opt out without scattering `post_publish_poll.enabled: false`
80    /// across every publisher block.
81    pub skip_post_publish_poll: bool,
82    /// Whether the publisher dispatcher gates irreversible Submitter
83    /// publishers (chocolatey, winget, AUR-source, krew, snapcraft) on
84    /// the success of every required Assets/Manager publisher that ran
85    /// before them. `None` defaults to `Some(true)` (gate on). The CLI
86    /// flag `--no-gate-submitter` flips this to `Some(false)`. See
87    /// `stage-publish::dispatch::DispatchOptions::gate_submitter` for
88    /// the gating mechanics.
89    pub gate_submitter: Option<bool>,
90    /// `--rollback=<none|best-effort>`: post-publish rollback policy.
91    /// `None` means "resolve from preflight state at dispatch time"
92    /// (best-effort when preflight ran clean, none otherwise with a
93    /// warn). Consumed by the rollback-dispatch task.
94    pub rollback_mode: Option<RollbackMode>,
95    /// `--simulate-failure=<publisher>` (repeatable, hidden, env-gated
96    /// behind `ANODIZE_TEST_HARNESS=1`): names of publishers whose
97    /// `run()` should be skipped and a synthetic `Failed("simulated
98    /// failure: <name>")` recorded in the report instead. Lets the
99    /// failure-mode test harness exercise gate / rollback / report
100    /// paths deterministically without monkey-patching production
101    /// publisher code.
102    pub simulate_failure_publishers: Vec<String>,
103    /// `--rollback-only`: skip publish; re-attempt rollback from a
104    /// prior run report. Requires `from_run` to identify which prior
105    /// run's `report.json` to load. The actual replay logic lands in
106    /// a follow-up task; this field is plumbed so the flag is visible
107    /// in `--help` today.
108    pub rollback_only: bool,
109    /// `--allow-rerun`: force `PublishStage::run` to proceed even
110    /// when a prior `report.json` exists for the current `run_id`.
111    /// The default (false) refuses re-runs to prevent PR-based
112    /// publishers (homebrew / scoop / nix / krew / MCP) from
113    /// duplicating their pull requests against the same tag.
114    ///
115    /// Operators recovering from a partial failure should prefer
116    /// `--rollback-only --from-run=<id>` (which has its own
117    /// idempotency guard via `dist/run-<id>/rollback.json`). The
118    /// rerun flag is an escape hatch for advanced cases where the
119    /// operator has manually verified no duplicate-publish risk
120    /// exists.
121    ///
122    /// Audit ref: 2026-05-15 release-resilience-review finding I4.
123    pub allow_rerun: bool,
124    /// `--show-skipped`: surface the per-crate "no `<publisher>` config
125    /// block" skip lines at default verbosity. In workspace mode every
126    /// PR-based publisher (homebrew / nix / scoop / aur / winget / krew /
127    /// chocolatey) visits every selected crate and skips the ones whose
128    /// config lacks its block; at default verbosity those no-op skips are
129    /// routed to debug (invisible unless `--debug`) so they do not bury the
130    /// real output. Setting this flag forces them back to status — the
131    /// diagnostic escape hatch for "why didn't publisher X run for crate Y?".
132    pub show_skipped: bool,
133    /// `--from-run=<id>`: prior run id whose `report.json` to load
134    /// when running in `--rollback-only` mode. clap enforces the
135    /// `requires = "rollback_only"` relationship at parse time.
136    pub from_run: Option<String>,
137    /// `--allow-nondeterministic <name>=<reason>` (repeatable):
138    /// runtime non-determinism opt-outs for specific artifacts. The
139    /// determinism stage suppresses its non-determinism error for
140    /// any matching artifact name, recording the supplied reason in
141    /// the report. Mutually exclusive with `--strict` at the clap
142    /// layer.
143    pub runtime_nondeterministic_allowlist: Vec<(String, String)>,
144    /// `--summary-json=<path>`: when set, the per-publisher run
145    /// summary is written to this path. Consumed by the run-summary
146    /// task.
147    pub summary_json_path: Option<PathBuf>,
148    /// `--allow-ai-failure`: when true, a failure inside the
149    /// `changelog.ai` enhancement step (transport, non-2xx, parse) is
150    /// logged as a warning and the pre-AI release notes are kept
151    /// verbatim. Default `false` (fail-closed) follows the conventional
152    /// "any hook failure aborts" pattern: a silent fall-back to the
153    /// raw notes ships the wrong body without the operator noticing.
154    pub allow_ai_failure: bool,
155    /// `changelog --from <ref>`: explicit lower bound (range start) for
156    /// changelog commit collection. When set, the changelog stage uses this
157    /// ref as the previous tag instead of auto-discovering the latest matching
158    /// tag. A dedicated option (rather than the always-auto-populated
159    /// `PreviousTag` template var) so a full release run's per-crate
160    /// auto-discovery is never overridden — only an explicit `--from` is.
161    pub changelog_from: Option<String>,
162    /// `changelog ..` / `changelog ..<ref>`: an explicit empty lower bound,
163    /// meaning "from the beginning of history" with no auto-discovered
164    /// previous tag. When `true`, the changelog stage skips tag
165    /// auto-discovery entirely so the range covers all reachable commits up
166    /// to the upper bound — distinguishing the explicit empty-from form from
167    /// an omitted range (which still resolves to the last release tag).
168    pub changelog_full_history: bool,
169    /// `changelog <from>..<to>` / `changelog <tag>`: an explicit UPPER bound
170    /// (range end) for changelog commit collection. When set, the changelog
171    /// stage walks `<from>..<to>` instead of `<from>..HEAD`, so commits AFTER
172    /// `<to>` are excluded. A dedicated option (rather than the always-populated
173    /// `Tag` template var) so the pending / snapshot window — where `Tag`
174    /// resolves to the latest EXISTING tag yet the range must still run to
175    /// HEAD — is never silently bounded to that tag. `None` keeps the upper
176    /// bound at `HEAD` (the pending window since the last release).
177    pub changelog_to: Option<String>,
178    /// Marks the run as the standalone `changelog --format release-notes`
179    /// LOCAL preview, NOT the `release`/`tag` pipeline. The standalone command
180    /// is an inspection tool: it must render the pending window from local git
181    /// with no release-time preconditions, so this flag relaxes three guards
182    /// that are correct for a real release but wrong for a preview:
183    ///   - the tag-must-point-at-HEAD + dirty-tree bails in
184    ///     `resolve_git_context` (a preview must not require a checkout or a
185    ///     clean tree),
186    ///   - the snapshot-skip config gate in the changelog stage (a preview
187    ///     must render without `changelog.snapshot: true`),
188    ///   - the `use: github-native` branch (a preview renders from local git
189    ///     instead of requiring a token / emitting empty bodies).
190    ///
191    /// ONLY the standalone changelog command sets this; the release/tag
192    /// pipelines leave it `false` so their guards stay fully intact.
193    pub changelog_preview: bool,
194    /// Marks the run as the standalone `anodizer notify` command — a
195    /// side-channel that sends a one-off message through the configured
196    /// announce integrations, NOT part of the `release` pipeline.
197    ///
198    /// notify is routinely invoked as an `on_error:` hook AFTER a release has
199    /// failed mid-flight, when the working tree is dirty (partial `dist/`,
200    /// in-flight writeback) and HEAD may not sit on the release tag. A
201    /// notification must never be blocked by repo state — losing the alert is
202    /// the worst outcome — so this flag relaxes the three release-time git
203    /// preconditions in `resolve_git_context` that are correct for a real
204    /// release but wrong for a notification: the no-tag bail (falls back to the
205    /// `v0.0.0` synthetic tag), the tag-must-point-at-HEAD bail, and the
206    /// dirty-tree bail. ONLY the notify command sets this; every release/tag
207    /// path leaves it `false` so their guards stay intact.
208    pub notify: bool,
209    /// `--allow-snapshot-publish`: downgrade the publish stage's non-release
210    /// version guard from a hard bail to a warning.
211    ///
212    /// By default the publish, blob, and announce stages REFUSE to ship a
213    /// non-release version (snapshot / dirty / `0.0.0`-sentinel — see
214    /// [`crate::version::guard_release_version`] /
215    /// [`crate::version::is_release_version`]) to an external, often
216    /// irreversible, channel. The canonical accident this prevents: a CI run
217    /// that resolved `0.0.0~SNAPSHOT-<sha>` and pushed it to a package
218    /// registry. This flag is the deliberate opt-in for the legitimate
219    /// "publish a snapshot to a private channel" case; it is the ONLY thing
220    /// required to opt in (the version is not re-stated). Default `false`
221    /// (fail-closed).
222    pub allow_snapshot_publish: bool,
223}
224
225impl Default for ContextOptions {
226    fn default() -> Self {
227        Self {
228            snapshot: false,
229            nightly: false,
230            dry_run: false,
231            quiet: false,
232            verbose: false,
233            debug: false,
234            skip_stages: Vec::new(),
235            publisher_allowlist: Vec::new(),
236            selected_crates: Vec::new(),
237            token: None,
238            parallelism: 4,
239            single_target: None,
240            release_notes_path: None,
241            fail_fast: false,
242            partial_target: None,
243            merge: false,
244            publish_only: false,
245            preflight_secrets: false,
246            project_root: None,
247            strict: false,
248            strict_preflight: false,
249            resume_release: false,
250            replace_existing_artifacts: false,
251            skip_post_publish_poll: false,
252            gate_submitter: None,
253            rollback_mode: None,
254            simulate_failure_publishers: Vec::new(),
255            rollback_only: false,
256            allow_rerun: false,
257            show_skipped: false,
258            from_run: None,
259            runtime_nondeterministic_allowlist: Vec::new(),
260            summary_json_path: None,
261            allow_ai_failure: false,
262            changelog_from: None,
263            changelog_full_history: false,
264            changelog_to: None,
265            changelog_preview: false,
266            notify: false,
267            allow_snapshot_publish: false,
268        }
269    }
270}