anodizer_core/context/state.rs
1use super::*;
2
3/// Stage→stage handoff state produced by stages and consumed by later
4/// stages (as opposed to `config` / `options` which are pipeline inputs,
5/// or `artifacts` which has its own registry). The changelog stage
6/// writes here, the release stage reads here.
7#[derive(Debug, Default)]
8pub struct StageOutputs {
9 /// Set by the changelog stage when `use: github-native` is configured.
10 /// The release stage reads this to set `generate_release_notes(true)`
11 /// on the GitHub API.
12 pub github_native_changelog: bool,
13 /// Per-crate rendered changelog body, keyed by crate name.
14 pub changelogs: HashMap<String, String>,
15 /// Rendered `changelog.header` value, populated by the changelog stage.
16 /// The release stage uses it as a fallback when `release.header` is
17 /// unset so YAML-configured changelog headers reach the GitHub release
18 /// body (the release-header content-loading behaviour).
19 pub changelog_header: Option<String>,
20 /// Rendered `changelog.footer` value, populated by the changelog stage.
21 /// Same fallback semantics as `changelog_header`.
22 pub changelog_footer: Option<String>,
23 /// Per-publisher post-publish polling results, written by the publish
24 /// stage's chocolatey / winget polling fan-out and consumed by the
25 /// release-summary renderer. Stored as opaque JSON to keep core free
26 /// of stage-publish types (the `PostPublishResult` type lives in
27 /// `anodizer-stage-publish::post_publish::status` and serializes
28 /// stably). Empty when polling was disabled or no eligible
29 /// publishers ran.
30 pub post_publish_results: Vec<serde_json::Value>,
31}
32
33/// Callback that re-runs release-content verification against the already
34/// published reversible surface and reports whether it passed. Stored on
35/// [`Context`] so the publish dispatcher can gate one-way-door publishers on
36/// a fresh verify without `stage-publish` depending on `stage-verify-release`.
37/// `Arc` so it can be cheaply cloned out of `&mut Context` before invocation.
38pub type VerifyGate = std::sync::Arc<dyn Fn(&mut Context) -> anyhow::Result<bool> + Send + Sync>;
39
40impl Context {
41 /// Publisher-facing override: when `Publisher::run` returns `Ok`
42 /// but the terminal outcome is something other than `Succeeded`
43 /// (chocolatey moderation skip, winget/krew/homebrew
44 /// PR-already-exists skip, …) call this before returning so
45 /// dispatch records the correct `PublisherOutcome` on the report.
46 /// Without this, dispatch defaults to `Succeeded` on any Ok and
47 /// the summary table silently misreports the skip as success.
48 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
49 self.pending_outcome = Some(outcome);
50 }
51
52 /// Dispatch-side consumer: take the pending outcome override (if
53 /// any) recorded by the publisher's `run`. Single-shot — the slot
54 /// is empty after this call.
55 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
56 self.pending_outcome.take()
57 }
58
59 /// Publisher-side recorder: stash the partial evidence accumulated
60 /// before a failing `run` returns `Err`, so dispatch can attach it to
61 /// the failed report row and rollback has the authoritative record of
62 /// what went live. See [`Context::pending_evidence`].
63 pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
64 self.pending_evidence = Some(evidence);
65 }
66
67 /// Dispatch-side consumer: take the partial evidence (if any) a
68 /// publisher recorded before failing. Single-shot — empty after this
69 /// call.
70 pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
71 self.pending_evidence.take()
72 }
73
74 /// Borrow the publisher dispatch report set by `PublishStage::run`,
75 /// or `None` if the publish stage hasn't run yet (or was skipped).
76 pub fn publish_report(&self) -> Option<&PublishReport> {
77 self.publish_report.as_ref()
78 }
79
80 /// Whether the publish stage entered its body this run (even if it
81 /// aborted before dispatching any publisher).
82 pub fn publish_attempted(&self) -> bool {
83 self.publish_attempted
84 }
85
86 /// Record that the publish stage entered its body. Called by
87 /// `PublishStage::run` ahead of its pre-dispatch guards so guard
88 /// aborts are distinguishable from a skipped stage.
89 pub fn set_publish_attempted(&mut self) {
90 self.publish_attempted = true;
91 }
92
93 /// Store the publisher dispatch report. Overwrites any prior value.
94 ///
95 /// Written by the publish stage during a normal release run; rehydrated by
96 /// `--announce-only` from the on-disk `<dist>/run-<id>/report.json` so the
97 /// announce stage sees an equivalent context without re-publishing.
98 pub fn set_publish_report(&mut self, r: PublishReport) {
99 self.publish_report = Some(r);
100 }
101
102 /// Borrow the set of crate names the build stage actually built, or
103 /// `None` if the build stage has not run in this pipeline (merge mode).
104 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
105 self.built_crate_names.as_ref()
106 }
107
108 /// Record the distinct crate names that received at least one in-scope
109 /// build job. Called once by the build stage after job planning.
110 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
111 self.built_crate_names = Some(names);
112 }
113
114 /// Record a working-tree path anodizer itself wrote during this run
115 /// (repo-relative, `/`-separated). Called by the writers of expected
116 /// in-run mutations (the binstall metadata emitter) so cleanliness
117 /// guards can distinguish the tool's own residue from operator drift.
118 pub fn record_tree_mutation(&mut self, rel_path: impl Into<String>) {
119 self.tree_mutations.insert(rel_path.into());
120 }
121
122 /// Repo-relative paths anodizer itself wrote this run (see
123 /// [`Context::record_tree_mutation`]).
124 pub fn tree_mutations(&self) -> &std::collections::BTreeSet<String> {
125 &self.tree_mutations
126 }
127
128 /// Record an intentional skip from a per-sub-config loop
129 /// (`signs`, `docker_signs`, `publishers`, …). `stage` identifies the
130 /// owning stage, `label` identifies the sub-config (id / name / index),
131 /// `reason` is short user-facing text. Duplicate (stage, label, reason)
132 /// tuples are dropped on insert so a per-artifact inner loop cannot emit
133 /// N copies of the same skip message.
134 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
135 self.skip_memento.remember(stage, label, reason);
136 }
137}