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}