1use camino::{Utf8Path, Utf8PathBuf};
13use serde::Serialize;
14
15use crate::cli::stage::{StageAction, StageArgs};
16use crate::diagnostic::{Diagnostic, Reason};
17use crate::error::RkError;
18use crate::landing::manifest::{self, Style, Workflow};
19use crate::landing::{self, Params};
20use crate::output::Output;
21use crate::projection::{Projection, ProjectionInput, TargetEvidence};
22use crate::release::EmbeddedReleaseSource;
23use crate::stage::{self, OutputSource, Receipt, clean};
24
25#[derive(Debug, Serialize)]
28struct Report<'a> {
29 #[serde(flatten)]
31 receipt: &'a Receipt,
32 output_source: &'static str,
34 next: Vec<String>,
36}
37
38#[derive(Debug, Serialize)]
40struct CleanReport {
41 schema: &'static str,
43 stage_root: String,
45 target: String,
47 removed: bool,
49 recovery: String,
51 next: Vec<String>,
53}
54
55pub fn run(args: &StageArgs) -> Result<(), RkError> {
65 match &args.action {
66 Some(StageAction::Clean { path, json }) => remove(Output::new(*json), path),
67 None => create(args),
68 }
69}
70
71fn create(args: &StageArgs) -> Result<(), RkError> {
72 let out = Output::new(args.json);
73 if !args.target.is_dir() {
74 return Err(RkError::missing(
75 Diagnostic::new(
76 Reason::TargetNotFound,
77 format!("target {} is not a directory", args.target),
78 )
79 .expected("an existing repository to stage the candidate for"),
80 ));
81 }
82 let target = canonical(&args.target)?;
83 let config = crate::config::load(target.as_std_path())?;
84 let record = match manifest::load(&target) {
88 Ok(record) => record,
89 Err(error) => {
90 out.warn(format!(
91 "the landing record was not read, so the candidate is computed as a first landing: {error}"
92 ));
93 None
94 }
95 };
96 let receipt_schema_version = stage::recorded_schema_version(&target);
97 let params = Params::resolve(
98 &EmbeddedReleaseSource,
99 &target,
100 &landing::Inputs {
101 tech: args.tech.as_deref(),
102 forge: args.forge.as_deref(),
103 repo: args.repo.as_deref(),
104 workflow: args.workflow.as_deref().map(Workflow::parse).transpose()?,
105 style: args.style.as_deref().map(Style::parse).transpose()?,
106 nix: args.nix.then_some(true),
107 },
108 config.as_ref(),
109 record.as_ref(),
110 if record.is_some() {
111 landing::Purpose::Upgrade
112 } else {
113 landing::Purpose::Init
114 },
115 )?;
116 let evidence = TargetEvidence::gather(&target, record.as_ref())?;
117 let projection = Projection::compute(&ProjectionInput {
118 params: params.clone(),
119 evidence,
120 })?;
121 let (output, source) = stage::resolve_output(args.output.as_deref(), target.as_std_path())?;
122 let prepared = stage::prepare(&output, source, target.as_std_path())?;
123 let composed = stage::compose(
124 &projection,
125 ¶ms,
126 target.as_std_path(),
127 prepared.resolved(),
128 record.as_ref(),
129 receipt_schema_version,
130 );
131 stage::write(&prepared, &composed)?;
132 let receipt = &composed.receipt;
133 let next = vec![
134 format!(
135 "read {}/{} for the candidate inventory and its reasons",
136 receipt.stage_root,
137 stage::RECEIPT_NAME
138 ),
139 format!(
140 "compare {}/{} with {} before anything lands",
141 receipt.stage_root,
142 stage::ARTIFACTS_DIR,
143 receipt.target
144 ),
145 format!(
146 "rk stage clean {} removes the stage once the landing is verified",
147 receipt.stage_root
148 ),
149 ];
150 render(out, receipt, source);
151 out.next(&next);
152 out.emit(&Report {
153 receipt,
154 output_source: source.as_str(),
155 next,
156 })
157}
158
159fn render(out: Output, receipt: &Receipt, source: OutputSource) {
161 out.result_line(format!("stage: {}", receipt.stage_root));
162 out.result_line(format!("output: from {}", source.as_str()));
163 out.result_line(format!("target: {}", receipt.target));
164 let parameters = &receipt.parameters;
165 out.result_line(format!(
166 "parameters: tech {}, forge {}, repo {}, workflow {}, style {}, nix {}",
167 parameters.tech,
168 parameters.forge,
169 parameters.repo,
170 parameters.workflow.as_str(),
171 parameters.style.map_or("unresolved", Style::as_str),
172 if parameters.nix { "on" } else { "off" }
173 ));
174 out.result_line(format!(
175 "landing record: {}",
176 receipt.receipt_schema_version.map_or_else(
177 || "none".to_owned(),
178 |version| format!("schema_version {version}")
179 )
180 ));
181 out.result_line(format!("candidates: {}", receipt.candidates.len()));
182 for candidate in &receipt.candidates {
183 out.result_line(format!(
184 " {}/{} ({}, {})",
185 stage::ARTIFACTS_DIR,
186 candidate.destination,
187 candidate.kind.as_str(),
188 candidate.placement
189 ));
190 }
191 for note in &receipt.omissions {
192 out.result_line(format!("omitted {}: {}", note.destination, note.reason));
193 }
194 for note in &receipt.collisions {
195 out.result_line(format!("collision {}: {}", note.destination, note.reason));
196 }
197 for destination in &receipt.retired {
198 out.result_line(format!(
199 "retired {destination}: recorded, no longer produced; target-owned from the next landing"
200 ));
201 }
202 for destination in &receipt.seeded_present {
203 out.result_line(format!("seeded present {destination}: a landing keeps it"));
204 }
205 for destination in &receipt.state_present {
206 out.result_line(format!("state present {destination}: a landing keeps it"));
207 }
208 out.result_line(format!(
209 "reference: {}",
210 receipt
211 .reference
212 .iter()
213 .map(|root| format!("{}/{root}", stage::REFERENCE_DIR))
214 .collect::<Vec<_>>()
215 .join(", ")
216 ));
217}
218
219fn remove(out: Output, path: &Utf8Path) -> Result<(), RkError> {
220 let validated = clean::validate(path.as_std_path())?;
221 clean::remove(&validated)?;
222 let stage_root = validated.resolved().display().to_string();
223 let target = validated.target().to_owned();
224 let recovery = format!("rk stage --target {target} --output {stage_root}");
225 out.result_line(format!("removed {stage_root}"));
226 out.result_line(format!("recovery: {recovery} stages the candidate again"));
227 let next = vec![
228 format!("rk status --target {target} reports what the target holds"),
229 format!("rk stage --target {target} stages this binary's candidate again"),
230 ];
231 out.next(&next);
232 out.emit(&CleanReport {
233 schema: "rk.stage-clean/1",
234 stage_root,
235 target,
236 removed: true,
237 recovery,
238 next,
239 })
240}
241
242fn canonical(target: &Utf8Path) -> Result<Utf8PathBuf, RkError> {
244 let path = std::fs::canonicalize(target)?;
245 Utf8PathBuf::from_path_buf(path)
246 .map_err(|path| RkError::Usage(format!("the target path is not UTF-8: {}", path.display())))
247}
248
249#[cfg(test)]
250mod tests {
251 use super::{CleanReport, Report};
252 use crate::stage::{Parameters, Receipt, STAGE_SCHEMA};
253
254 #[test]
257 fn the_stage_report_schema_snapshot_holds() {
258 let receipt = Receipt {
259 schema: STAGE_SCHEMA.to_owned(),
260 rk_version: "0.0.0".into(),
261 target: "/tmp/t".into(),
262 stage_root: "/tmp/s".into(),
263 parameters: Parameters {
264 tech: "rust".into(),
265 forge: "github".into(),
266 repo: "acme/widget".into(),
267 workflow: crate::landing::Workflow::Branches,
268 style: None,
269 nix: true,
270 trunk: "main".into(),
271 line_prefix: "release/".into(),
272 security_contact: String::new(),
273 security_response: "best-effort".into(),
274 },
275 receipt_schema_version: None,
276 candidates: vec![],
277 omissions: vec![],
278 collisions: vec![],
279 retired: vec![],
280 seeded_present: vec![],
281 state_present: vec![],
282 reference: vec!["CHANGELOG.md".into()],
283 };
284 let report = Report {
285 receipt: &receipt,
286 output_source: "--output",
287 next: vec![
288 "rk stage clean /tmp/s removes the stage once the landing is verified".into(),
289 ],
290 };
291 assert_eq!(
292 serde_json::to_string(&report).expect("a report serializes"),
293 r#"{"schema":"rk.stage/1","rk_version":"0.0.0","target":"/tmp/t","stage_root":"/tmp/s","parameters":{"tech":"rust","forge":"github","repo":"acme/widget","workflow":"branches","style":null,"nix":true,"trunk":"main","line_prefix":"release/","security_contact":"","security_response":"best-effort"},"receipt_schema_version":null,"candidates":[],"omissions":[],"collisions":[],"retired":[],"seeded_present":[],"state_present":[],"reference":["CHANGELOG.md"],"output_source":"--output","next":["rk stage clean /tmp/s removes the stage once the landing is verified"]}"#
294 );
295 }
296
297 #[test]
299 fn the_stage_clean_report_schema_snapshot_holds() {
300 let report = CleanReport {
301 schema: "rk.stage-clean/1",
302 stage_root: "/tmp/s".into(),
303 target: "/tmp/t".into(),
304 removed: true,
305 recovery: "rk stage --target /tmp/t --output /tmp/s".into(),
306 next: vec!["rk status --target /tmp/t reports what the target holds".into()],
307 };
308 assert_eq!(
309 serde_json::to_string(&report).expect("a report serializes"),
310 r#"{"schema":"rk.stage-clean/1","stage_root":"/tmp/s","target":"/tmp/t","removed":true,"recovery":"rk stage --target /tmp/t --output /tmp/s","next":["rk status --target /tmp/t reports what the target holds"]}"#
311 );
312 }
313}