release_kit/commands/
versions.rs1use serde::Serialize;
14
15use crate::cli::versions::VersionsArgs;
16use crate::error::RkError;
17use crate::output::Output;
18use crate::{embedded, registry};
19
20#[derive(Debug, Serialize)]
22struct PinResult {
23 tool: String,
25 pinned: String,
27 result: &'static str,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 available: Option<String>,
33}
34
35#[derive(Debug, Serialize)]
37struct Report {
38 schema: &'static str,
40 pins: Vec<PinResult>,
42}
43
44pub fn run(args: &VersionsArgs) -> Result<(), RkError> {
51 if !args.check {
52 Output::human().result_raw(embedded::VERSIONS);
53 return Ok(());
54 }
55 let out = Output::new(args.json);
56 let mut results = Vec::new();
57 for pin in registry::pins() {
58 let result = pin.check.as_deref().map_or_else(
59 || PinResult {
60 tool: pin.name.clone(),
61 pinned: pin.version.clone(),
62 result: "source-unreachable",
63 available: None,
64 },
65 |url| check_one(&pin.name, &pin.version, url),
66 );
67 out.result_line(match (&result.result, &result.available) {
68 (&"update-available", Some(available)) => format!(
69 "update-available {} {} pinned, {available} at the source",
70 result.tool, result.pinned
71 ),
72 _ => format!("{} {} {}", result.result, result.tool, result.pinned),
73 });
74 results.push(result);
75 }
76 out.next(&[
77 "a pin update is a reviewed change to versions.toml, with its checked date".to_owned(),
78 ]);
79 out.emit(&Report {
80 schema: "rk.versions-check/1",
81 pins: results,
82 })
83}
84
85fn check_one(tool: &str, pinned: &str, url: &str) -> PinResult {
87 let result = |result, available| PinResult {
88 tool: tool.to_owned(),
89 pinned: pinned.to_owned(),
90 result,
91 available,
92 };
93 let curl = std::env::var_os("RK_CURL_BIN").unwrap_or_else(|| "curl".into());
94 let fetched = std::process::Command::new(curl)
95 .args(["-fsSL", "--max-time", "10", url])
96 .output();
97 let body = match fetched {
98 Ok(output) if output.status.success() => output.stdout,
99 _ => return result("source-unreachable", None),
100 };
101 let Some(available) = latest_version(&body) else {
102 return result("source-unparsable", None);
103 };
104 if is_current(pinned, &available) {
105 result("current", Some(available))
106 } else {
107 result("update-available", Some(available))
108 }
109}
110
111fn latest_version(body: &[u8]) -> Option<String> {
114 let value: serde_json::Value = serde_json::from_slice(body).ok()?;
115 let raw = value
116 .get("crate")
117 .and_then(|krate| krate.get("max_stable_version"))
118 .or_else(|| value.get("tag_name"))
119 .and_then(serde_json::Value::as_str)?;
120 let start = raw.find(|c: char| c.is_ascii_digit())?;
123 Some(raw[start..].to_owned())
124}
125
126fn is_current(pinned: &str, available: &str) -> bool {
129 if pinned == available {
130 return true;
131 }
132 !pinned.contains('.') && available.split('.').next() == Some(pinned)
133}
134
135#[cfg(test)]
136mod tests {
137 #![allow(clippy::expect_used)]
138
139 use super::{PinResult, Report, is_current, latest_version};
140
141 #[test]
142 fn a_source_version_is_read_from_both_answer_shapes() {
143 assert_eq!(
144 latest_version(br#"{"crate":{"max_stable_version":"0.3.170"}}"#),
145 Some("0.3.170".to_owned())
146 );
147 assert_eq!(
148 latest_version(br#"{"tag_name":"v2.13.1"}"#),
149 Some("2.13.1".to_owned())
150 );
151 assert_eq!(
152 latest_version(br#"{"tag_name":"release-plz-v0.3.160"}"#),
153 Some("0.3.160".to_owned())
154 );
155 assert_eq!(latest_version(b"not json"), None);
156 assert_eq!(latest_version(br#"{"unrelated":true}"#), None);
157 }
158
159 #[test]
160 fn a_major_only_pin_is_current_within_its_major() {
161 assert!(is_current("0.3.160", "0.3.160"));
162 assert!(!is_current("0.3.160", "0.3.170"));
163 assert!(is_current("4", "4.3.1"));
164 assert!(!is_current("4", "5.0.0"));
165 }
166
167 #[test]
169 fn the_versions_check_schema_snapshot_holds() {
170 let report = Report {
171 schema: "rk.versions-check/1",
172 pins: vec![PinResult {
173 tool: "release-plz".into(),
174 pinned: "0.3.160".into(),
175 result: "update-available",
176 available: Some("0.3.170".into()),
177 }],
178 };
179 assert_eq!(
180 serde_json::to_string(&report).expect("a report serializes"),
181 r#"{"schema":"rk.versions-check/1","pins":[{"tool":"release-plz","pinned":"0.3.160","result":"update-available","available":"0.3.170"}]}"#
182 );
183 }
184}