Skip to main content

release_kit/profile/
catalog.rs

1//! The capability catalog: which complete release-kit product each
2//! embedded source belongs to, which dimensions select it, and whether it
3//! is available at the dimensions a target resolved to.
4//!
5//! Availability belongs to a capability at its dimensions and never to a
6//! category alone: `release.automation` at `(python, github)` is available
7//! while the same capability at `(python, gitlab)` is unavailable. Every
8//! omission is recorded with one of the five reasons the vocabulary
9//! names, so a reader can tell a product nobody asked for from one this
10//! release cannot land.
11//!
12//! The catalog is pure: it reads the snippet paths it is given and the
13//! resolved parameters, and nothing else.
14//!
15//! SATISFIES project-profile:availability-belongs-to-a-capability-at-its-dimensions
16
17use serde::Serialize;
18
19use super::{Params, ReleaseMode};
20use crate::detect::Forge;
21use crate::projection::{CODE_SCANNING_DESTINATIONS, NIX_DESTINATIONS};
22
23/// The release guards every valid target selects.
24///
25/// What the release process needs in the target's own files: the routing
26/// block and the hook block, with the release lines and the hooks that
27/// guard the tags and the files release-kit owns.
28pub const GUARDS: &str = "git.guards";
29/// The opt-in workflow guards: the agent and commit policy lines of the
30/// routing block, the glossary, the workflow hooks, and the two workflow
31/// setup steps. Off for a fresh landing.
32pub const WORKFLOW_GUARDS: &str = "git.workflow-guards";
33/// The complete, active title gate at the forge.
34pub const TITLE_CHECK: &str = "git.title-check";
35/// The landed vulnerability reporting policy.
36pub const REPORTING_POLICY: &str = "security.reporting-policy";
37/// The release automation at one driver and one forge: the version
38/// source, the bot configuration, and the release workflow.
39pub const RELEASE_AUTOMATION: &str = "release.automation";
40/// The seeded package expression and the seed flake pair.
41pub const PACKAGING_NIX: &str = "packaging.nix";
42/// The `OpenSSF` Scorecard workflow.
43pub const SCORECARD: &str = "supply-chain.scorecard";
44/// The code scanning workflow, by provider.
45pub const CODE_SCANNING: &str = "supply-chain.code-scanning";
46
47/// Every capability, in the order a report lists them.
48pub const ALL: [&str; 8] = [
49    GUARDS,
50    WORKFLOW_GUARDS,
51    TITLE_CHECK,
52    REPORTING_POLICY,
53    RELEASE_AUTOMATION,
54    PACKAGING_NIX,
55    SCORECARD,
56    CODE_SCANNING,
57];
58
59/// The zone below `snippets/` that carries the release-less GitLab root
60/// pipeline: the minimal `.gitlab-ci.yml` that activates the title
61/// fragment where no release automation ships a root pipeline.
62pub const TITLE_GATE_ZONE: &str = "_title-gate";
63
64/// The one owner of every embedded snippet: which capability lands the
65/// file at `path`, the path relative to `snippets/`.
66///
67/// `None` is a source defect: a snippet no capability claims would land
68/// under no selection, and a test holds every embedded file to one
69/// owner.
70#[must_use]
71pub fn owner_of(path: &str) -> Option<&'static str> {
72    let mut segments = path.splitn(3, '/');
73    let (zone, _forge, destination) = (segments.next()?, segments.next()?, segments.next()?);
74    if zone == "_shared" {
75        return match destination {
76            "SECURITY.md" => Some(REPORTING_POLICY),
77            ".github/workflows/pr-title.yml" | ".gitlab/ci/mr-title.yml" => Some(TITLE_CHECK),
78            ".github/workflows/scorecard.yml" => Some(SCORECARD),
79            _ => None,
80        };
81    }
82    if zone == TITLE_GATE_ZONE {
83        return (destination == ".gitlab-ci.yml").then_some(TITLE_CHECK);
84    }
85    if zone.starts_with('_') {
86        return None;
87    }
88    if NIX_DESTINATIONS.contains(&destination) {
89        return Some(PACKAGING_NIX);
90    }
91    if CODE_SCANNING_DESTINATIONS
92        .iter()
93        .any(|(name, _)| *name == destination)
94    {
95        return Some(CODE_SCANNING);
96    }
97    Some(RELEASE_AUTOMATION)
98}
99
100/// The status of one capability for one target.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102#[serde(rename_all = "kebab-case")]
103pub enum Status {
104    /// The catalog has the complete contribution, and the target selects
105    /// it.
106    Selected,
107    /// The target did not request the optional capability.
108    NotRequested,
109    /// The capability lacks a dimension the target does not have, a forge
110    /// or a release driver.
111    NotApplicable,
112    /// The catalog knows the capability but not at these dimensions.
113    Unavailable,
114    /// A category name the catalog does not know.
115    Unknown,
116    /// Selected, but a target-owned destination blocks its activation:
117    /// the safe prerequisite files still land, and the omission names
118    /// the operator's one remaining edit.
119    Withheld,
120}
121
122impl Status {
123    /// The report form.
124    #[must_use]
125    pub const fn as_str(self) -> &'static str {
126        match self {
127            Self::Selected => "selected",
128            Self::NotRequested => "not-requested",
129            Self::NotApplicable => "not-applicable",
130            Self::Unavailable => "unavailable",
131            Self::Unknown => "unknown",
132            Self::Withheld => "withheld",
133        }
134    }
135}
136
137/// One capability's answer for one target.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
139pub struct Selection {
140    /// The capability id.
141    pub id: &'static str,
142    /// Its status.
143    pub status: Status,
144    /// Why, for every status but selected.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub reason: Option<String>,
147    /// The one edit the operator makes, for a withheld capability.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub action: Option<String>,
150    /// The embedded sources it lands, relative to `snippets/`, empty for
151    /// a capability the blocks land or one that lands nothing.
152    #[serde(skip)]
153    pub sources: Vec<String>,
154    /// The release driver the selection is keyed on, where the capability
155    /// has that dimension; the registry pins are keyed on it too.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub driver: Option<String>,
158    /// The provider the selection is keyed on, where the capability's
159    /// parameter names one. Two providers of one capability land different
160    /// files and run different tools, so the registry pins are keyed on it
161    /// too and a target records the pins of the provider it landed.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub provider: Option<String>,
164}
165
166impl Selection {
167    /// Whether the capability contributes destinations.
168    #[must_use]
169    pub const fn lands(&self) -> bool {
170        matches!(self.status, Status::Selected | Status::Withheld)
171    }
172
173    fn selected(id: &'static str, sources: Vec<String>, driver: Option<&str>) -> Self {
174        Self {
175            id,
176            status: Status::Selected,
177            reason: None,
178            action: None,
179            sources,
180            driver: driver.map(str::to_owned),
181            provider: None,
182        }
183    }
184
185    /// The same selection, keyed on the provider its parameter named.
186    fn keyed_on(mut self, provider: &str) -> Self {
187        self.provider = Some(provider.to_owned());
188        self
189    }
190
191    fn omitted(id: &'static str, status: Status, reason: impl Into<String>) -> Self {
192        Self {
193            id,
194            status,
195            reason: Some(reason.into()),
196            action: None,
197            sources: Vec::new(),
198            driver: None,
199            provider: None,
200        }
201    }
202}
203
204/// What the embedded sources can land: every snippet path, relative to
205/// `snippets/`, sorted.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Availability {
208    files: Vec<String>,
209}
210
211impl Availability {
212    /// The availability over an explicit snippet list, each path relative
213    /// to `snippets/`.
214    #[must_use]
215    pub fn over(files: Vec<String>) -> Self {
216        let mut files = files;
217        files.sort();
218        Self { files }
219    }
220
221    /// The availability the installed binary embeds.
222    #[must_use]
223    pub fn embedded() -> Self {
224        Self::over(
225            crate::embedded::walk(&crate::embedded::SNIPPETS)
226                .into_iter()
227                .map(|(path, _)| path)
228                .collect(),
229        )
230    }
231
232    /// Every embedded path, relative to `snippets/`.
233    #[must_use]
234    pub fn files(&self) -> &[String] {
235        &self.files
236    }
237
238    /// The files under one zone and forge that `capability` owns.
239    fn owned(&self, zone: &str, forge: &str, capability: &str) -> Vec<String> {
240        let prefix = format!("{zone}/{forge}/");
241        self.files
242            .iter()
243            .filter(|path| path.starts_with(&prefix) && owner_of(path) == Some(capability))
244            .cloned()
245            .collect()
246    }
247
248    /// The drivers the sources know: every non-underscore zone.
249    #[must_use]
250    pub fn drivers(&self) -> Vec<String> {
251        let mut out: Vec<String> = Vec::new();
252        for path in &self.files {
253            if let Some((zone, _)) = path.split_once('/')
254                && !zone.starts_with('_')
255                && !out.iter().any(|known| known == zone)
256            {
257                out.push(zone.to_owned());
258            }
259        }
260        out
261    }
262
263    /// The `(driver, forge)` tuples at which the release automation is
264    /// available, in path order, as `driver, forge` strings.
265    #[must_use]
266    pub fn automation_tuples(&self) -> Vec<String> {
267        let mut out: Vec<String> = Vec::new();
268        for path in &self.files {
269            let mut segments = path.splitn(3, '/');
270            if let (Some(driver), Some(forge), Some(_)) =
271                (segments.next(), segments.next(), segments.next())
272                && !driver.starts_with('_')
273                && owner_of(path) == Some(RELEASE_AUTOMATION)
274            {
275                let entry = format!("{driver}, {forge}");
276                if !out.contains(&entry) {
277                    out.push(entry);
278                }
279            }
280        }
281        out
282    }
283}
284
285/// The release drivers this binary's sources know.
286#[must_use]
287pub fn known_drivers() -> Vec<String> {
288    Availability::embedded().drivers()
289}
290
291/// Whether `name` is a forge this binary has an adapter for.
292fn known_forge(name: &str) -> bool {
293    Forge::parse(name).is_some()
294}
295
296/// Select every capability for `params` against `availability`.
297///
298/// The order is [`ALL`]. The local guards are always selected; every
299/// other capability answers by its dimensions and its request.
300#[must_use]
301#[allow(
302    clippy::too_many_lines,
303    reason = "one pass answers every capability, and splitting it would separate a selection from the dimensions that decided it"
304)]
305pub fn select(params: &Params, availability: &Availability) -> Vec<Selection> {
306    let forge = params.forge();
307    let driver = params.driver();
308    let known_drivers = availability.drivers();
309    let driver_known = driver.is_some_and(|d| known_drivers.iter().any(|k| k == d));
310    let forge_known = forge.is_some_and(known_forge);
311
312    let mut out = Vec::with_capacity(ALL.len());
313    out.push(Selection::selected(GUARDS, Vec::new(), None));
314    out.push(if params.workflow_guards() {
315        Selection::selected(WORKFLOW_GUARDS, Vec::new(), None)
316    } else {
317        Selection::omitted(
318            WORKFLOW_GUARDS,
319            Status::NotRequested,
320            "capabilities.workflow_guards is false",
321        )
322    });
323
324    // The release automation first, because the title gate's root
325    // pipeline depends on whether it lands.
326    let automation = match (params.release_mode(), driver, forge) {
327        (ReleaseMode::External, _, _) => Selection::omitted(
328            RELEASE_AUTOMATION,
329            Status::NotRequested,
330            "the release is external: the target releases through a process release-kit does not drive",
331        ),
332        (ReleaseMode::None, _, _) => Selection::omitted(
333            RELEASE_AUTOMATION,
334            Status::NotRequested,
335            "the release mode is none",
336        ),
337        (ReleaseMode::Automatic, None, _) => Selection::omitted(
338            RELEASE_AUTOMATION,
339            Status::NotApplicable,
340            "an automatic release names no driver",
341        ),
342        (ReleaseMode::Automatic, _, None) => Selection::omitted(
343            RELEASE_AUTOMATION,
344            Status::NotApplicable,
345            "the profile names no forge",
346        ),
347        (ReleaseMode::Automatic, Some(driver), Some(forge)) => {
348            if !driver_known {
349                Selection::omitted(
350                    RELEASE_AUTOMATION,
351                    Status::Unknown,
352                    format!(
353                        "the driver {driver} is not one this release knows; the bindings are: {}",
354                        known_drivers.join(", ")
355                    ),
356                )
357            } else if !forge_known {
358                Selection::omitted(
359                    RELEASE_AUTOMATION,
360                    Status::Unknown,
361                    format!(
362                        "the forge {forge} is not one this release drives; the forges are: github, gitlab"
363                    ),
364                )
365            } else {
366                let sources = availability.owned(driver, forge, RELEASE_AUTOMATION);
367                if sources.is_empty() {
368                    Selection::omitted(
369                        RELEASE_AUTOMATION,
370                        Status::Unavailable,
371                        format!(
372                            "the release automation at ({driver}, {forge}) has no landable files; the available tuples are: {}",
373                            availability.automation_tuples().join("; ")
374                        ),
375                    )
376                } else {
377                    Selection::selected(RELEASE_AUTOMATION, sources, Some(driver))
378                }
379            }
380        }
381    };
382    let automation_lands = automation.status == Status::Selected;
383
384    // The title gate: complete where the forge is known.
385    out.push(match forge {
386        None => Selection::omitted(
387            TITLE_CHECK,
388            Status::NotApplicable,
389            "the profile names no forge",
390        ),
391        Some(forge) if !forge_known => Selection::omitted(
392            TITLE_CHECK,
393            Status::Unknown,
394            format!(
395                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
396            ),
397        ),
398        Some(forge) => {
399            let mut sources = availability.owned("_shared", forge, TITLE_CHECK);
400            if !automation_lands {
401                sources.extend(availability.owned(TITLE_GATE_ZONE, forge, TITLE_CHECK));
402            }
403            if sources.is_empty() {
404                Selection::omitted(
405                    TITLE_CHECK,
406                    Status::Unavailable,
407                    format!("this release ships no title gate for {forge}"),
408                )
409            } else {
410                Selection::selected(TITLE_CHECK, sources, None)
411            }
412        }
413    });
414
415    // The reporting policy.
416    out.push(match forge {
417        _ if !params.reporting_policy() => Selection::omitted(
418            REPORTING_POLICY,
419            Status::NotRequested,
420            "capabilities.reporting_policy is false",
421        ),
422        None => Selection::omitted(
423            REPORTING_POLICY,
424            Status::NotApplicable,
425            "the profile names no forge, and the policy names the forge's private channel",
426        ),
427        Some(forge) if !forge_known => Selection::omitted(
428            REPORTING_POLICY,
429            Status::Unknown,
430            format!(
431                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
432            ),
433        ),
434        Some(forge) => {
435            let sources = availability.owned("_shared", forge, REPORTING_POLICY);
436            if sources.is_empty() {
437                Selection::omitted(
438                    REPORTING_POLICY,
439                    Status::Unavailable,
440                    format!("this release ships no reporting policy for {forge}"),
441                )
442            } else {
443                Selection::selected(REPORTING_POLICY, sources, None)
444            }
445        }
446    });
447
448    out.push(automation);
449
450    // The Nix packaging, keyed on the release driver and the forge.
451    out.push(match (params.nix_packaging(), driver, forge) {
452        (false, _, _) => Selection::omitted(
453            PACKAGING_NIX,
454            Status::NotRequested,
455            "capabilities.nix_packaging is false",
456        ),
457        (true, None, _) => Selection::omitted(
458            PACKAGING_NIX,
459            Status::NotApplicable,
460            "the seed is keyed on an automatic release driver, and the profile names none",
461        ),
462        (true, _, None) => Selection::omitted(
463            PACKAGING_NIX,
464            Status::NotApplicable,
465            "the profile names no forge",
466        ),
467        (true, Some(driver), Some(forge)) => {
468            if !driver_known || !forge_known {
469                Selection::omitted(
470                    PACKAGING_NIX,
471                    Status::Unknown,
472                    format!("({driver}, {forge}) names a category this release does not know"),
473                )
474            } else {
475                let sources = availability.owned(driver, forge, PACKAGING_NIX);
476                if sources.is_empty() {
477                    Selection::omitted(
478                        PACKAGING_NIX,
479                        Status::Unavailable,
480                        format!("the {driver} binding ships no Nix seed on {forge}"),
481                    )
482                } else {
483                    Selection::selected(PACKAGING_NIX, sources, Some(driver))
484                }
485            }
486        }
487    });
488
489    // The Scorecard workflow, GitHub's alone.
490    out.push(match forge {
491        _ if !params.scorecard() => Selection::omitted(
492            SCORECARD,
493            Status::NotRequested,
494            "capabilities.scorecard is false",
495        ),
496        None => Selection::omitted(
497            SCORECARD,
498            Status::NotApplicable,
499            "the profile names no forge",
500        ),
501        Some(forge) if !forge_known => Selection::omitted(
502            SCORECARD,
503            Status::Unknown,
504            format!(
505                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
506            ),
507        ),
508        Some(forge) => {
509            let sources = availability.owned("_shared", forge, SCORECARD);
510            if sources.is_empty() {
511                Selection::omitted(
512                    SCORECARD,
513                    Status::Unavailable,
514                    format!(
515                        "the Scorecard workflow is GitHub's alone, and the {forge} zone ships none"
516                    ),
517                )
518            } else {
519                Selection::selected(SCORECARD, sources, None)
520            }
521        }
522    });
523
524    // The code scanning workflow, keyed on the driver, the forge, and the
525    // provider.
526    out.push(match (params.code_scanning(), driver, forge) {
527        (None, _, _) => Selection::omitted(
528            CODE_SCANNING,
529            Status::NotRequested,
530            "capabilities.code_scanning is off",
531        ),
532        (Some(_), None, _) => Selection::omitted(
533            CODE_SCANNING,
534            Status::NotApplicable,
535            "a scanner reads the release driver's language, and the profile names no driver",
536        ),
537        (Some(_), _, None) => Selection::omitted(
538            CODE_SCANNING,
539            Status::NotApplicable,
540            "the profile names no forge",
541        ),
542        (Some(provider), Some(driver), Some(forge)) => {
543            if !driver_known || !forge_known {
544                Selection::omitted(
545                    CODE_SCANNING,
546                    Status::Unknown,
547                    format!("({driver}, {forge}) names a category this release does not know"),
548                )
549            } else if let Some(reason) = crate::projection::code_scanning_incompatibility(
550                Some(provider),
551                Some(driver),
552                Some(forge),
553            ) {
554                Selection::omitted(CODE_SCANNING, Status::Unavailable, reason)
555            } else {
556                let sources: Vec<String> = availability
557                    .owned(driver, forge, CODE_SCANNING)
558                    .into_iter()
559                    .filter(|path| {
560                        CODE_SCANNING_DESTINATIONS
561                            .iter()
562                            .any(|(name, owner)| path.ends_with(name) && *owner == provider)
563                    })
564                    .collect();
565                if sources.is_empty() {
566                    Selection::omitted(
567                        CODE_SCANNING,
568                        Status::Unavailable,
569                        format!(
570                            "the {driver} binding ships no {} workflow on {forge}",
571                            provider.as_str()
572                        ),
573                    )
574                } else {
575                    Selection::selected(CODE_SCANNING, sources, Some(driver))
576                        .keyed_on(provider.as_str())
577                }
578            }
579        }
580    });
581    debug_assert_eq!(out.len(), ALL.len());
582    out
583}
584
585/// The pin keys one selection matches in the registry's `used_by`.
586///
587/// Three of them: the bare capability id, the id qualified by the driver
588/// where the capability has that dimension, and that qualified again by
589/// the provider where its parameter names one.
590///
591/// Three widths rather than one, so a pin declares the narrowest truth it
592/// can. A tool every provider runs takes the bare id; one a single
593/// provider runs takes the widest key, and a target that landed the other
594/// provider records neither the tool nor its staleness baseline.
595#[must_use]
596pub fn pin_keys(selection: &Selection) -> Vec<String> {
597    let mut keys = vec![selection.id.to_owned()];
598    if let Some(driver) = &selection.driver {
599        keys.push(format!("{}/{driver}", selection.id));
600        if let Some(provider) = &selection.provider {
601            keys.push(format!("{}/{driver}/{provider}", selection.id));
602        }
603    }
604    keys
605}
606
607#[cfg(test)]
608mod tests {
609    use super::{
610        ALL, Availability, CODE_SCANNING, GUARDS, PACKAGING_NIX, RELEASE_AUTOMATION,
611        REPORTING_POLICY, SCORECARD, Status, TITLE_CHECK, WORKFLOW_GUARDS, owner_of, select,
612    };
613    use crate::landing::Params;
614    use crate::landing::manifest::{Provider, Style};
615    use crate::profile::ReleaseMode;
616
617    fn status_of(selections: &[super::Selection], id: &str) -> Status {
618        selections
619            .iter()
620            .find(|s| s.id == id)
621            .expect("every capability answers")
622            .status
623    }
624
625    /// Every embedded snippet has exactly one owner, and no shared path
626    /// falls to the release automation by default.
627    #[test]
628    fn every_embedded_snippet_has_one_owner() {
629        let availability = Availability::embedded();
630        assert!(!availability.files().is_empty());
631        for path in availability.files() {
632            let owner = owner_of(path);
633            assert!(owner.is_some(), "{path}: no capability owns it");
634            assert!(
635                ALL.contains(&owner.unwrap_or_default()),
636                "{path}: an unlisted owner"
637            );
638        }
639        assert_eq!(
640            owner_of("_shared/github/SECURITY.md"),
641            Some(REPORTING_POLICY)
642        );
643        assert_eq!(
644            owner_of("_shared/github/.github/workflows/scorecard.yml"),
645            Some(SCORECARD)
646        );
647        assert_eq!(owner_of("rust/github/flake.nix"), Some(PACKAGING_NIX));
648        assert_eq!(
649            owner_of("rust/github/.github/workflows/code-scanning-codeql.yml"),
650            Some(CODE_SCANNING)
651        );
652        assert_eq!(
653            owner_of("rust/github/release-plz.toml"),
654            Some(RELEASE_AUTOMATION)
655        );
656        assert_eq!(owner_of("_shared/github/unknown.txt"), None);
657    }
658
659    /// SATISFIES project-profile:availability-belongs-to-a-capability-at-its-dimensions
660    #[test]
661    fn the_catalog_answers_availability_per_tuple() {
662        let availability = Availability::embedded();
663        let mut github = Params::for_test("acme/widget", Some(Style::Trunk));
664        github.set_pair_for_test("python", "github");
665        let mut gitlab = github.clone();
666        gitlab.set_pair_for_test("python", "gitlab");
667        let on_github = select(&github, &availability);
668        let on_gitlab = select(&gitlab, &availability);
669        assert_eq!(status_of(&on_github, RELEASE_AUTOMATION), Status::Selected);
670        assert_eq!(
671            status_of(&on_gitlab, RELEASE_AUTOMATION),
672            Status::Unavailable
673        );
674        let reason = on_gitlab
675            .iter()
676            .find(|s| s.id == RELEASE_AUTOMATION)
677            .and_then(|s| s.reason.clone())
678            .expect("an unavailable capability states why");
679        assert!(reason.contains("rust, gitlab"), "{reason}");
680        assert!(reason.contains("python, github"), "{reason}");
681        // The title gate still lands on GitLab, with the release-less root
682        // pipeline beside the fragment.
683        let title = on_gitlab
684            .iter()
685            .find(|s| s.id == TITLE_CHECK)
686            .expect("the title gate answers");
687        assert_eq!(title.status, Status::Selected);
688        assert!(
689            title
690                .sources
691                .iter()
692                .any(|s| s == "_title-gate/gitlab/.gitlab-ci.yml"),
693            "{:?}",
694            title.sources
695        );
696    }
697
698    /// A no-forge, no-release input selects the local guards alone and
699    /// marks every forge capability not applicable.
700    #[test]
701    fn a_no_forge_input_selects_the_guards_alone() {
702        let params = Params::for_test_release_less(&[], None, ReleaseMode::None);
703        let selections = select(&params, &Availability::embedded());
704        assert_eq!(status_of(&selections, GUARDS), Status::Selected);
705        for id in [
706            TITLE_CHECK,
707            REPORTING_POLICY,
708            RELEASE_AUTOMATION,
709            PACKAGING_NIX,
710        ] {
711            assert_ne!(status_of(&selections, id), Status::Selected, "{id}");
712        }
713        assert_eq!(status_of(&selections, TITLE_CHECK), Status::NotApplicable);
714        assert_eq!(
715            status_of(&selections, RELEASE_AUTOMATION),
716            Status::NotRequested
717        );
718        assert_eq!(status_of(&selections, PACKAGING_NIX), Status::NotRequested);
719    }
720
721    /// The workflow guards answer their request alone, and the release
722    /// guards land whatever it says.
723    #[test]
724    fn the_workflow_guards_are_selected_only_when_requested() {
725        let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
726        let selections = select(&params, &Availability::embedded());
727        assert_eq!(status_of(&selections, WORKFLOW_GUARDS), Status::Selected);
728        params.set_workflow_guards_for_test(false);
729        let selections = select(&params, &Availability::embedded());
730        assert_eq!(
731            status_of(&selections, WORKFLOW_GUARDS),
732            Status::NotRequested
733        );
734        assert_eq!(status_of(&selections, GUARDS), Status::Selected);
735    }
736
737    /// An unknown forge keeps the guards and reports the rest unknown.
738    #[test]
739    fn an_unknown_forge_reads_as_unknown_and_lands_the_guards() {
740        let mut params = Params::for_test_release_less(&[], Some("codeberg"), ReleaseMode::None);
741        params.set_scorecard_for_test(true);
742        let selections = select(&params, &Availability::embedded());
743        assert_eq!(status_of(&selections, GUARDS), Status::Selected);
744        assert_eq!(status_of(&selections, TITLE_CHECK), Status::Unknown);
745        assert_eq!(status_of(&selections, SCORECARD), Status::Unknown);
746    }
747
748    /// The scanner follows the provider and the pair, and a request the
749    /// pair cannot run reports `Unavailable` with the reason the landing
750    /// then omits it by. Nothing refuses on it.
751    #[test]
752    fn code_scanning_selects_the_providers_own_file() {
753        let availability = Availability::embedded();
754        let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
755        params.set_code_scanning_for_test(Some(Provider::Semgrep));
756        let selections = select(&params, &availability);
757        let scanning = selections
758            .iter()
759            .find(|s| s.id == CODE_SCANNING)
760            .expect("answers");
761        assert_eq!(scanning.status, Status::Selected);
762        assert_eq!(
763            scanning.sources,
764            vec!["rust/github/.github/workflows/code-scanning-semgrep.yml".to_owned()]
765        );
766        params.set_pair_for_test("bash", "github");
767        let selections = select(&params, &availability);
768        assert_eq!(status_of(&selections, CODE_SCANNING), Status::Unavailable);
769    }
770}