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, the id
36    /// qualified by the release driver as `release.automation/rust` where
37    /// the capability has that dimension, or that qualified again by the
38    /// provider as `supply-chain.code-scanning/rust/codeql` where its
39    /// parameter names one. A pin declares the narrowest of the three it
40    /// can, so a target records the tools its own landing runs.
41    #[serde(default)]
42    pub used_by: Vec<String>,
43    /// The URL a freshness check queries, where one exists.
44    #[serde(default)]
45    pub check: Option<String>,
46}
47
48/// The registry's parsed shape; only the fields named here are read.
49#[derive(Debug, Deserialize)]
50struct Registry {
51    /// Every `[[tool]]` entry.
52    tool: Vec<Pin>,
53}
54
55/// Every pin the embedded registry declares, in authored order.
56///
57/// The embedded registry is authored in this repository and held valid by
58/// a test, so a parse failure is a build defect; this resolves it to an
59/// empty list rather than panicking, and the test is what catches it.
60#[must_use]
61pub fn pins() -> Vec<Pin> {
62    parse(embedded::VERSIONS)
63}
64
65/// The pins the selected capabilities use, keyed for a landing record.
66///
67/// Every pin whose `used_by` names a selected capability's id, bare or
68/// qualified by its driver and its provider, in authored order and each
69/// once.
70#[must_use]
71pub fn pins_for(selected: &[crate::profile::catalog::Selection]) -> Vec<Pin> {
72    let keys: Vec<String> = selected
73        .iter()
74        .filter(|selection| selection.lands())
75        .flat_map(crate::profile::catalog::pin_keys)
76        .collect();
77    pins()
78        .into_iter()
79        .filter(|pin| pin.used_by.iter().any(|user| keys.contains(user)))
80        .collect()
81}
82
83/// The pinned version of one tool, where the registry names it.
84#[must_use]
85pub fn version_of(name: &str) -> Option<String> {
86    pins()
87        .into_iter()
88        .find(|pin| pin.name == name)
89        .map(|pin| pin.version)
90}
91
92fn parse(text: &str) -> Vec<Pin> {
93    toml::from_str::<Registry>(text)
94        .map(|registry| registry.tool)
95        .unwrap_or_default()
96}
97
98#[cfg(test)]
99mod tests {
100    use super::{pins, pins_for};
101
102    /// The embedded registry parses, and every entry carries the fields
103    /// the readers depend on; a `versions.toml` edit that breaks parsing
104    /// fails here instead of silently emptying every reader.
105    #[test]
106    fn the_embedded_registry_parses_with_every_field() {
107        let pins = pins();
108        assert!(!pins.is_empty(), "the registry parsed to nothing");
109        for pin in &pins {
110            assert!(!pin.version.is_empty(), "{}: no version", pin.name);
111            assert!(!pin.used_by.is_empty(), "{}: no used_by", pin.name);
112            assert!(
113                pin.check.is_some() || (pin.action.is_some() && pin.commit.is_some()),
114                "{}: no check URL and no ref to resolve",
115                pin.name
116            );
117        }
118    }
119
120    /// Every `used_by` entry names a capability this binary catalogs,
121    /// qualified by a driver the sources know and a provider the landing
122    /// parameter admits, where it carries either.
123    #[test]
124    fn every_used_by_entry_names_a_catalogued_capability() {
125        let drivers = crate::profile::catalog::known_drivers();
126        for pin in pins() {
127            for user in &pin.used_by {
128                let mut parts = user.split('/');
129                let id = parts.next().unwrap_or_default();
130                let driver = parts.next();
131                let provider = parts.next();
132                assert!(
133                    parts.next().is_none(),
134                    "{}: used_by names {user}, which carries more than a capability, a driver, and a provider",
135                    pin.name
136                );
137                assert!(
138                    crate::profile::catalog::ALL.contains(&id),
139                    "{}: used_by names {user}, which is no capability",
140                    pin.name
141                );
142                if let Some(driver) = driver {
143                    assert!(
144                        drivers.iter().any(|known| known == driver),
145                        "{}: used_by names the driver {driver}, which no binding ships",
146                        pin.name
147                    );
148                }
149                if let Some(provider) = provider {
150                    assert!(
151                        crate::landing::manifest::Provider::parse(provider)
152                            .is_ok_and(|parsed| parsed.is_some()),
153                        "{}: used_by names the provider {provider}, which no parameter admits",
154                        pin.name
155                    );
156                }
157            }
158        }
159    }
160
161    /// A pin keyed on one provider reaches that provider's landing and no
162    /// other, so a target records the tools its own workflow runs.
163    #[test]
164    fn a_provider_keyed_pin_reaches_that_provider_alone() {
165        let names = |provider| {
166            let mut params =
167                crate::landing::Params::for_test("acme/widget", Some(crate::landing::Style::Trunk));
168            params.set_code_scanning_for_test(Some(provider));
169            let selected = crate::profile::catalog::select(
170                &params,
171                &crate::profile::catalog::Availability::embedded(),
172            );
173            pins_for(&selected)
174                .into_iter()
175                .map(|pin| pin.name)
176                .collect::<Vec<String>>()
177        };
178        let codeql = names(crate::landing::manifest::Provider::CodeQl);
179        let semgrep = names(crate::landing::manifest::Provider::Semgrep);
180        assert!(
181            codeql.iter().any(|name| name == "codeql-analyze"),
182            "{codeql:?}"
183        );
184        assert!(
185            !codeql.iter().any(|name| name == "semgrep-image"),
186            "a codeql landing records no semgrep tool: {codeql:?}"
187        );
188        assert!(
189            semgrep.iter().any(|name| name == "semgrep-image"),
190            "{semgrep:?}"
191        );
192        assert!(
193            !semgrep.iter().any(|name| name == "codeql-init"),
194            "a semgrep landing records no codeql tool: {semgrep:?}"
195        );
196        for pins in [&codeql, &semgrep] {
197            assert!(
198                pins.iter().any(|name| name == "checkout"),
199                "either provider checks out: {pins:?}"
200            );
201        }
202    }
203
204    #[test]
205    fn pins_filter_by_selected_capability() {
206        let params =
207            crate::landing::Params::for_test("acme/widget", Some(crate::landing::Style::Trunk));
208        let selected = crate::profile::catalog::select(
209            &params,
210            &crate::profile::catalog::Availability::embedded(),
211        );
212        let rust: Vec<String> = pins_for(&selected)
213            .into_iter()
214            .map(|pin| pin.name)
215            .collect();
216        assert!(rust.contains(&"release-plz".to_owned()));
217        assert!(rust.contains(&"cargo-dist".to_owned()));
218        assert!(rust.contains(&"conventional-pre-commit".to_owned()));
219        assert!(!rust.contains(&"git-cliff".to_owned()));
220        assert!(!rust.contains(&"scorecard-action".to_owned()));
221        // A guards-only target records the hook pins and nothing else.
222        let guards = crate::landing::Params::for_test_release_less(
223            &[],
224            None,
225            crate::profile::ReleaseMode::None,
226        );
227        let selected = crate::profile::catalog::select(
228            &guards,
229            &crate::profile::catalog::Availability::embedded(),
230        );
231        let names: Vec<String> = pins_for(&selected)
232            .into_iter()
233            .map(|pin| pin.name)
234            .collect();
235        assert_eq!(names, ["conventional-pre-commit", "pre-commit-hooks"]);
236    }
237}