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}