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