Skip to main content

anodizer_core/context/
state.rs

1use super::*;
2
3/// What the changelog stage resolved for one release range.
4///
5/// Recorded per crate (and once for a single-track workspace's aggregate)
6/// so a later stage can ask "did this range produce anything?" without
7/// re-deriving the walk or pattern-matching the rendered markdown.
8#[derive(Debug, Clone, Default)]
9pub struct ChangelogRangeSummary {
10    /// Entries left after grouping and filtering — the same count the
11    /// changelog stage's own empty-changelog warning gates on. Zero means the
12    /// release body carries no notes.
13    pub notable_entries: usize,
14    /// The tag the range started at, when one was resolved.
15    pub previous_tag: Option<String>,
16}
17
18/// Stage→stage handoff state produced by stages and consumed by later
19/// stages (as opposed to `config` / `options` which are pipeline inputs,
20/// or `artifacts` which has its own registry). The changelog stage
21/// writes here, the release stage reads here.
22#[derive(Debug, Default)]
23pub struct StageOutputs {
24    /// Set by the changelog stage when `use: github-native` is configured.
25    /// The release stage reads this to set `generate_release_notes(true)`
26    /// on the GitHub API.
27    pub github_native_changelog: bool,
28    /// Per-crate rendered changelog body, keyed by crate name.
29    pub changelogs: HashMap<String, String>,
30    /// The single AGGREGATE changelog body for a single-track workspace
31    /// (single-crate / lockstep / flat-aggregate). Set by the changelog stage
32    /// when [`ContextOptions::changelog_aggregate_set`] resolved the workspace
33    /// to single-track; it spans every crate directory over the whole release
34    /// range, so it is the correct GitHub release body no matter which crate
35    /// carries the `release:` block.
36    ///
37    /// The release stage and [`Context::populate_release_notes_var`] PREFER
38    /// this over the per-crate `changelogs` map. Without it a lockstep release
39    /// whose commits happened to miss the release crate's own directory (e.g.
40    /// changes under `crates/core` but the `release:` block lives on the binary
41    /// crate at `crates/cli`) would collapse to an empty "No notable changes"
42    /// body. `None` for a per-crate workspace, where each crate's own slice is
43    /// the right body.
44    pub release_body_changelog: Option<String>,
45    /// Rendered `changelog.header` value, populated by the changelog stage.
46    /// The release stage uses it as a fallback when `release.header` is
47    /// unset so YAML-configured changelog headers reach the GitHub release
48    /// body (the release-header content-loading behaviour).
49    pub changelog_header: Option<String>,
50    /// Rendered `changelog.footer` value, populated by the changelog stage.
51    /// Same fallback semantics as `changelog_header`.
52    pub changelog_footer: Option<String>,
53    /// Per-publisher post-publish polling results, written by the publish
54    /// stage's chocolatey / winget polling fan-out and consumed by the
55    /// release-summary renderer. Stored as opaque JSON to keep core free
56    /// of stage-publish types (the `PostPublishResult` type lives in
57    /// `anodizer-stage-publish::post_publish::status` and serializes
58    /// stably). Empty when polling was disabled or no eligible
59    /// publishers ran.
60    pub post_publish_results: Vec<serde_json::Value>,
61    /// Per-crate release-range summary, keyed by crate name. Written by the
62    /// changelog stage; `nightly.skip_if_no_changes` reads it. A crate absent
63    /// from the map had no changelog rendered (the stage was skipped), which
64    /// is NOT the same as a range that produced nothing.
65    pub changelog_ranges: HashMap<String, ChangelogRangeSummary>,
66    /// The aggregate range summary that accompanies
67    /// [`Self::release_body_changelog`] — set for a single-track workspace,
68    /// `None` for a per-crate one, on exactly the same condition.
69    pub release_body_range: Option<ChangelogRangeSummary>,
70    /// Crates whose release the release stage deliberately did not publish
71    /// (`release.skip`, `nightly.publish_release: false`,
72    /// `nightly.skip_if_no_changes`). No release exists for them, so the
73    /// verify-release gate must not go looking for one.
74    pub release_skipped_crates: Vec<String>,
75    /// Set by the release stage once its per-crate loop has run. The
76    /// `github-release` publisher delegates to that same stage, so without
77    /// this marker a pipeline that runs both creates every release — and
78    /// fires every nightly retention sweep — twice per run.
79    pub release_stage_ran: bool,
80}
81
82/// Callback that re-runs release-content verification against the already
83/// published reversible surface and reports whether it passed. Stored on
84/// [`Context`] so the publish dispatcher can gate one-way-door publishers on
85/// a fresh verify without `stage-publish` depending on `stage-verify-release`.
86/// `Arc` so it can be cheaply cloned out of `&mut Context` before invocation.
87pub type VerifyGate = std::sync::Arc<dyn Fn(&mut Context) -> anyhow::Result<bool> + Send + Sync>;
88
89impl Context {
90    /// Publisher-facing override: when `Publisher::run` returns `Ok`
91    /// but the terminal outcome is something other than `Succeeded`
92    /// (chocolatey moderation skip, winget/krew/homebrew
93    /// PR-already-exists skip, …) call this before returning so
94    /// dispatch records the correct `PublisherOutcome` on the report.
95    /// Without this, dispatch defaults to `Succeeded` on any Ok and
96    /// the summary table silently misreports the skip as success.
97    pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
98        self.pending_outcome = Some(outcome);
99    }
100
101    /// Dispatch-side consumer: take the pending outcome override (if
102    /// any) recorded by the publisher's `run`. Single-shot — the slot
103    /// is empty after this call.
104    pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
105        self.pending_outcome.take()
106    }
107
108    /// Publisher-side recorder: stash the partial evidence accumulated
109    /// before a failing `run` returns `Err`, so dispatch can attach it to
110    /// the failed report row and rollback has the authoritative record of
111    /// what went live. See [`Context::pending_evidence`].
112    pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
113        self.pending_evidence = Some(evidence);
114    }
115
116    /// Dispatch-side consumer: take the partial evidence (if any) a
117    /// publisher recorded before failing. Single-shot — empty after this
118    /// call.
119    pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
120        self.pending_evidence.take()
121    }
122
123    /// Borrow the publisher dispatch report set by `PublishStage::run`,
124    /// or `None` if the publish stage hasn't run yet (or was skipped).
125    pub fn publish_report(&self) -> Option<&PublishReport> {
126        self.publish_report.as_ref()
127    }
128
129    /// Whether the publish stage entered its body this run (even if it
130    /// aborted before dispatching any publisher).
131    pub fn publish_attempted(&self) -> bool {
132        self.publish_attempted
133    }
134
135    /// Record that the publish stage entered its body. Called by
136    /// `PublishStage::run` ahead of its pre-dispatch guards so guard
137    /// aborts are distinguishable from a skipped stage.
138    pub fn set_publish_attempted(&mut self) {
139        self.publish_attempted = true;
140    }
141
142    /// Store the publisher dispatch report. Overwrites any prior value.
143    ///
144    /// Written by the publish stage during a normal release run; rehydrated by
145    /// `--announce-only` from the on-disk `<dist>/run-<id>/report.json` so the
146    /// announce stage sees an equivalent context without re-publishing.
147    pub fn set_publish_report(&mut self, r: PublishReport) {
148        self.publish_report = Some(r);
149    }
150
151    /// Borrow the set of crate names the build stage actually built, or
152    /// `None` if the build stage has not run in this pipeline (merge mode).
153    pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
154        self.built_crate_names.as_ref()
155    }
156
157    /// Record the distinct crate names that received at least one in-scope
158    /// build job. Called once by the build stage after job planning.
159    pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
160        self.built_crate_names = Some(names);
161    }
162
163    /// Record a working-tree path anodizer itself wrote during this run
164    /// (repo-relative, `/`-separated). Called by the writers of expected
165    /// in-run mutations (the binstall metadata emitter) so cleanliness
166    /// guards can distinguish the tool's own residue from operator drift.
167    pub fn record_tree_mutation(&mut self, rel_path: impl Into<String>) {
168        self.tree_mutations.insert(rel_path.into());
169    }
170
171    /// Repo-relative paths anodizer itself wrote this run (see
172    /// [`Context::record_tree_mutation`]).
173    pub fn tree_mutations(&self) -> &std::collections::BTreeSet<String> {
174        &self.tree_mutations
175    }
176
177    /// Record an intentional skip from a per-sub-config loop
178    /// (`signs`, `docker_signs`, `publishers`, …). `stage` identifies the
179    /// owning stage, `label` identifies the sub-config (id / name / index),
180    /// `reason` is short user-facing text. Duplicate (stage, label, reason)
181    /// tuples are dropped on insert so a per-artifact inner loop cannot emit
182    /// N copies of the same skip message.
183    pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
184        self.skip_memento.remember(stage, label, reason);
185    }
186}