Skip to main content

release_kit/
registry.rs

1//! The pinned-tool registry, parsed from the embedded `versions.toml`.
2//!
3//! One registry serves three readers: `rk versions` prints it raw, a
4//! landing copies the relevant pins into the record, and `rk status`
5//! compares a record's pins against it offline. Parsing happens at
6//! runtime over the embedded bytes, so what the readers see is
7//! necessarily what the binary carries.
8
9use serde::Deserialize;
10
11use crate::embedded;
12
13/// One pinned tool, with the fields the binary's readers use; the
14/// registry's prose fields stay in the raw print.
15#[derive(Debug, Clone, Deserialize)]
16pub struct Pin {
17    /// The tool's name, the key a record's `pins` map uses.
18    pub name: String,
19    /// The pinned version.
20    pub version: String,
21    /// The workflow reference — `owner/action@ref` — where the tool is a
22    /// GitHub Action. The ref here is the discovery ref a freshness check
23    /// reads; the commit below is what the workflows execute.
24    #[serde(default)]
25    pub action: Option<String>,
26    /// The immutable execution commit the workflows pin, where the tool
27    /// is an action.
28    #[serde(default)]
29    pub commit: Option<String>,
30    /// How the discovery ref moves: a moving major or minor tag, an
31    /// exact tag, or a maintained branch. Movement is an update signal,
32    /// never evidence of an attack.
33    #[serde(default)]
34    pub ref_class: Option<String>,
35    /// The capabilities that use the tool: a capability id, or the id
36    /// qualified by the release driver as `release.automation/rust` where
37    /// the capability has that dimension.
38    #[serde(default)]
39    pub used_by: Vec<String>,
40    /// The URL a freshness check queries, where one exists.
41    #[serde(default)]
42    pub check: Option<String>,
43}
44
45/// The registry's parsed shape; only the fields named here are read.
46#[derive(Debug, Deserialize)]
47struct Registry {
48    /// Every `[[tool]]` entry.
49    tool: Vec<Pin>,
50}
51
52/// Every pin the embedded registry declares, in authored order.
53///
54/// The embedded registry is authored in this repository and held valid by
55/// a test, so a parse failure is a build defect; this resolves it to an
56/// empty list rather than panicking, and the test is what catches it.
57#[must_use]
58pub fn pins() -> Vec<Pin> {
59    parse(embedded::VERSIONS)
60}
61
62/// The pins the selected capabilities use, keyed for a landing record:
63/// every pin whose `used_by` names a selected capability's id, bare or
64/// qualified by its driver, in authored order and each once.
65#[must_use]
66pub fn pins_for(selected: &[crate::profile::catalog::Selection]) -> Vec<Pin> {
67    let keys: Vec<String> = selected
68        .iter()
69        .filter(|selection| selection.lands())
70        .flat_map(crate::profile::catalog::pin_keys)
71        .collect();
72    pins()
73        .into_iter()
74        .filter(|pin| pin.used_by.iter().any(|user| keys.contains(user)))
75        .collect()
76}
77
78/// The pinned version of one tool, where the registry names it.
79#[must_use]
80pub fn version_of(name: &str) -> Option<String> {
81    pins()
82        .into_iter()
83        .find(|pin| pin.name == name)
84        .map(|pin| pin.version)
85}
86
87fn parse(text: &str) -> Vec<Pin> {
88    toml::from_str::<Registry>(text)
89        .map(|registry| registry.tool)
90        .unwrap_or_default()
91}
92
93#[cfg(test)]
94mod tests {
95    use super::{pins, pins_for};
96
97    /// The embedded registry parses, and every entry carries the fields
98    /// the readers depend on; a `versions.toml` edit that breaks parsing
99    /// fails here instead of silently emptying every reader.
100    #[test]
101    fn the_embedded_registry_parses_with_every_field() {
102        let pins = pins();
103        assert!(!pins.is_empty(), "the registry parsed to nothing");
104        for pin in &pins {
105            assert!(!pin.version.is_empty(), "{}: no version", pin.name);
106            assert!(!pin.used_by.is_empty(), "{}: no used_by", pin.name);
107            assert!(
108                pin.check.is_some() || (pin.action.is_some() && pin.commit.is_some()),
109                "{}: no check URL and no ref to resolve",
110                pin.name
111            );
112        }
113    }
114
115    /// Every `used_by` entry names a capability this binary catalogs,
116    /// qualified by a driver the sources know where it carries one.
117    #[test]
118    fn every_used_by_entry_names_a_catalogued_capability() {
119        let drivers = crate::profile::catalog::known_drivers();
120        for pin in pins() {
121            for user in &pin.used_by {
122                let (id, driver) = user
123                    .split_once('/')
124                    .map_or((user.as_str(), None), |(id, driver)| (id, Some(driver)));
125                assert!(
126                    crate::profile::catalog::ALL.contains(&id),
127                    "{}: used_by names {user}, which is no capability",
128                    pin.name
129                );
130                if let Some(driver) = driver {
131                    assert!(
132                        drivers.iter().any(|known| known == driver),
133                        "{}: used_by names the driver {driver}, which no binding ships",
134                        pin.name
135                    );
136                }
137            }
138        }
139    }
140
141    #[test]
142    fn pins_filter_by_selected_capability() {
143        let params =
144            crate::landing::Params::for_test("acme/widget", Some(crate::landing::Style::Trunk));
145        let selected = crate::profile::catalog::select(
146            &params,
147            &crate::profile::catalog::Availability::embedded(),
148        );
149        let rust: Vec<String> = pins_for(&selected)
150            .into_iter()
151            .map(|pin| pin.name)
152            .collect();
153        assert!(rust.contains(&"release-plz".to_owned()));
154        assert!(rust.contains(&"cargo-dist".to_owned()));
155        assert!(rust.contains(&"conventional-pre-commit".to_owned()));
156        assert!(!rust.contains(&"git-cliff".to_owned()));
157        assert!(!rust.contains(&"scorecard-action".to_owned()));
158        // A guards-only target records the hook pins and nothing else.
159        let guards = crate::landing::Params::for_test_release_less(
160            &[],
161            None,
162            crate::profile::ReleaseMode::None,
163        );
164        let selected = crate::profile::catalog::select(
165            &guards,
166            &crate::profile::catalog::Availability::embedded(),
167        );
168        let names: Vec<String> = pins_for(&selected)
169            .into_iter()
170            .map(|pin| pin.name)
171            .collect();
172        assert_eq!(names, ["conventional-pre-commit", "pre-commit-hooks"]);
173    }
174}