release_kit/stage/
read.rs1use std::path::Path;
13
14use camino::Utf8Path;
15
16use super::{Parameters, RECEIPT_NAME, Receipt};
17use crate::diagnostic::{Diagnostic, Reason};
18use crate::error::RkError;
19use crate::landing::Params;
20use crate::profile::Inputs;
21
22pub fn load(root: &Utf8Path, canonical_target: &Path) -> Result<Receipt, RkError> {
30 let path = root.join(RECEIPT_NAME);
31 let receipt: Receipt = std::fs::read(&path)
32 .ok()
33 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
34 .ok_or_else(|| {
35 RkError::missing(
36 Diagnostic::new(
37 Reason::TargetNotFound,
38 format!("{path} is no stage receipt this binary reads"),
39 )
40 .expected("the root of a stage `rk stage` wrote")
41 .action("rk stage --target <target> writes one"),
42 )
43 })?;
44 if receipt.rk_version != env!("CARGO_PKG_VERSION") {
45 return Err(RkError::refusal(
46 Diagnostic::new(
47 Reason::StateDrift,
48 format!(
49 "the stage at {root} was written by rk {}, and this is rk {}; nothing was written",
50 receipt.rk_version,
51 env!("CARGO_PKG_VERSION")
52 ),
53 )
54 .expected("a stage this binary wrote")
55 .action("stage again with this binary, then land from that stage")
56 .target_state("unchanged"),
57 ));
58 }
59 let target = canonical_target.display().to_string();
60 if receipt.target != target {
61 return Err(RkError::refusal(
62 Diagnostic::new(
63 Reason::StateDrift,
64 format!(
65 "the stage at {root} was taken for {}, not {target}; nothing was written",
66 receipt.target
67 ),
68 )
69 .expected("a stage taken for this target")
70 .action(format!("rk stage --target {target} writes one"))
71 .target_state("unchanged"),
72 ));
73 }
74 Ok(receipt)
75}
76
77#[must_use]
79pub fn inputs(parameters: &Parameters) -> Inputs<'_> {
80 let release = ¶meters.profile.release;
81 Inputs {
82 technologies: ¶meters.profile.technologies,
83 forge: parameters.profile.forge.as_deref(),
84 repo: (!parameters.repo.is_empty()).then_some(parameters.repo.as_str()),
85 release_mode: Some(release.mode),
86 release_driver: release.driver.as_deref(),
87 style: release.style,
88 trunk: Some(parameters.git.trunk.as_str()),
89 checkout_mode: Some(parameters.git.checkout_mode),
90 integration: Some(parameters.git.integration),
91 merge_form: Some(parameters.git.merge_form),
92 required_check: (!parameters.required_check.is_empty())
93 .then_some(parameters.required_check.as_str()),
94 required_workflow: (!parameters.required_workflow.is_empty())
95 .then_some(parameters.required_workflow.as_str()),
96 build_command: release
97 .build
98 .as_ref()
99 .and_then(|build| build.command.as_deref()),
100 build_packages: release
101 .build
102 .as_ref()
103 .and_then(|build| build.packages.as_deref()),
104 nix: Some(parameters.capabilities.nix_packaging),
105 reporting_policy: Some(parameters.capabilities.reporting_policy),
106 scorecard: Some(parameters.capabilities.scorecard),
107 workflow_guards: parameters.capabilities.workflow_guards,
108 code_scanning: Some(parameters.capabilities.code_scanning),
109 }
110}
111
112pub fn confirm(params: &Params, receipt: &Receipt, root: &Utf8Path) -> Result<(), RkError> {
120 let resolved = serde_json::to_value(Parameters::from(params)).map_err(anyhow::Error::from)?;
121 let staged = serde_json::to_value(&receipt.parameters).map_err(anyhow::Error::from)?;
122 if resolved == staged {
123 return Ok(());
124 }
125 Err(RkError::refusal(
126 Diagnostic::new(
127 Reason::StateDrift,
128 format!(
129 "the target's answers moved since the stage at {root} was written, and nothing was written"
130 ),
131 )
132 .expected("the answers the stage resolved")
133 .action("stage again, compare, then land from the new stage")
134 .target_state("unchanged"),
135 ))
136}
137
138#[must_use]
141pub fn answered_twice() -> RkError {
142 RkError::Usage(
143 "--stage carries every answer the stage resolved; pass the answers to rk stage instead"
144 .to_owned(),
145 )
146}