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}
151
152impl Selection {
153    /// Whether the capability contributes destinations.
154    #[must_use]
155    pub const fn lands(&self) -> bool {
156        matches!(self.status, Status::Selected | Status::Withheld)
157    }
158
159    fn selected(id: &'static str, sources: Vec<String>, driver: Option<&str>) -> Self {
160        Self {
161            id,
162            status: Status::Selected,
163            reason: None,
164            action: None,
165            sources,
166            driver: driver.map(str::to_owned),
167        }
168    }
169
170    fn omitted(id: &'static str, status: Status, reason: impl Into<String>) -> Self {
171        Self {
172            id,
173            status,
174            reason: Some(reason.into()),
175            action: None,
176            sources: Vec::new(),
177            driver: None,
178        }
179    }
180}
181
182/// What the embedded sources can land: every snippet path, relative to
183/// `snippets/`, sorted.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Availability {
186    files: Vec<String>,
187}
188
189impl Availability {
190    /// The availability over an explicit snippet list, each path relative
191    /// to `snippets/`.
192    #[must_use]
193    pub fn over(files: Vec<String>) -> Self {
194        let mut files = files;
195        files.sort();
196        Self { files }
197    }
198
199    /// The availability the installed binary embeds.
200    #[must_use]
201    pub fn embedded() -> Self {
202        Self::over(
203            crate::embedded::walk(&crate::embedded::SNIPPETS)
204                .into_iter()
205                .map(|(path, _)| path)
206                .collect(),
207        )
208    }
209
210    /// Every embedded path, relative to `snippets/`.
211    #[must_use]
212    pub fn files(&self) -> &[String] {
213        &self.files
214    }
215
216    /// The files under one zone and forge that `capability` owns.
217    fn owned(&self, zone: &str, forge: &str, capability: &str) -> Vec<String> {
218        let prefix = format!("{zone}/{forge}/");
219        self.files
220            .iter()
221            .filter(|path| path.starts_with(&prefix) && owner_of(path) == Some(capability))
222            .cloned()
223            .collect()
224    }
225
226    /// The drivers the sources know: every non-underscore zone.
227    #[must_use]
228    pub fn drivers(&self) -> Vec<String> {
229        let mut out: Vec<String> = Vec::new();
230        for path in &self.files {
231            if let Some((zone, _)) = path.split_once('/')
232                && !zone.starts_with('_')
233                && !out.iter().any(|known| known == zone)
234            {
235                out.push(zone.to_owned());
236            }
237        }
238        out
239    }
240
241    /// The `(driver, forge)` tuples at which the release automation is
242    /// available, in path order, as `driver, forge` strings.
243    #[must_use]
244    pub fn automation_tuples(&self) -> Vec<String> {
245        let mut out: Vec<String> = Vec::new();
246        for path in &self.files {
247            let mut segments = path.splitn(3, '/');
248            if let (Some(driver), Some(forge), Some(_)) =
249                (segments.next(), segments.next(), segments.next())
250                && !driver.starts_with('_')
251                && owner_of(path) == Some(RELEASE_AUTOMATION)
252            {
253                let entry = format!("{driver}, {forge}");
254                if !out.contains(&entry) {
255                    out.push(entry);
256                }
257            }
258        }
259        out
260    }
261}
262
263/// The release drivers this binary's sources know.
264#[must_use]
265pub fn known_drivers() -> Vec<String> {
266    Availability::embedded().drivers()
267}
268
269/// Whether `name` is a forge this binary has an adapter for.
270fn known_forge(name: &str) -> bool {
271    Forge::parse(name).is_some()
272}
273
274/// Select every capability for `params` against `availability`.
275///
276/// The order is [`ALL`]. The local guards are always selected; every
277/// other capability answers by its dimensions and its request.
278#[must_use]
279#[allow(
280    clippy::too_many_lines,
281    reason = "one pass answers every capability, and splitting it would separate a selection from the dimensions that decided it"
282)]
283pub fn select(params: &Params, availability: &Availability) -> Vec<Selection> {
284    let forge = params.forge();
285    let driver = params.driver();
286    let known_drivers = availability.drivers();
287    let driver_known = driver.is_some_and(|d| known_drivers.iter().any(|k| k == d));
288    let forge_known = forge.is_some_and(known_forge);
289
290    let mut out = Vec::with_capacity(ALL.len());
291    out.push(Selection::selected(GUARDS, Vec::new(), None));
292
293    // The release automation first, because the title gate's root
294    // pipeline depends on whether it lands.
295    let automation = match (params.release_mode(), driver, forge) {
296        (ReleaseMode::External, _, _) => Selection::omitted(
297            RELEASE_AUTOMATION,
298            Status::NotRequested,
299            "the release is external: the target releases through a process release-kit does not drive",
300        ),
301        (ReleaseMode::None, _, _) => Selection::omitted(
302            RELEASE_AUTOMATION,
303            Status::NotRequested,
304            "the release mode is none",
305        ),
306        (ReleaseMode::Automatic, None, _) => Selection::omitted(
307            RELEASE_AUTOMATION,
308            Status::NotApplicable,
309            "an automatic release names no driver",
310        ),
311        (ReleaseMode::Automatic, _, None) => Selection::omitted(
312            RELEASE_AUTOMATION,
313            Status::NotApplicable,
314            "the profile names no forge",
315        ),
316        (ReleaseMode::Automatic, Some(driver), Some(forge)) => {
317            if !driver_known {
318                Selection::omitted(
319                    RELEASE_AUTOMATION,
320                    Status::Unknown,
321                    format!(
322                        "the driver {driver} is not one this release knows; the bindings are: {}",
323                        known_drivers.join(", ")
324                    ),
325                )
326            } else if !forge_known {
327                Selection::omitted(
328                    RELEASE_AUTOMATION,
329                    Status::Unknown,
330                    format!(
331                        "the forge {forge} is not one this release drives; the forges are: github, gitlab"
332                    ),
333                )
334            } else {
335                let sources = availability.owned(driver, forge, RELEASE_AUTOMATION);
336                if sources.is_empty() {
337                    Selection::omitted(
338                        RELEASE_AUTOMATION,
339                        Status::Unavailable,
340                        format!(
341                            "the release automation at ({driver}, {forge}) has no landable files; the available tuples are: {}",
342                            availability.automation_tuples().join("; ")
343                        ),
344                    )
345                } else {
346                    Selection::selected(RELEASE_AUTOMATION, sources, Some(driver))
347                }
348            }
349        }
350    };
351    let automation_lands = automation.status == Status::Selected;
352
353    // The title gate: complete where the forge is known.
354    out.push(match forge {
355        None => Selection::omitted(
356            TITLE_CHECK,
357            Status::NotApplicable,
358            "the profile names no forge",
359        ),
360        Some(forge) if !forge_known => Selection::omitted(
361            TITLE_CHECK,
362            Status::Unknown,
363            format!(
364                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
365            ),
366        ),
367        Some(forge) => {
368            let mut sources = availability.owned("_shared", forge, TITLE_CHECK);
369            if !automation_lands {
370                sources.extend(availability.owned(TITLE_GATE_ZONE, forge, TITLE_CHECK));
371            }
372            if sources.is_empty() {
373                Selection::omitted(
374                    TITLE_CHECK,
375                    Status::Unavailable,
376                    format!("this release ships no title gate for {forge}"),
377                )
378            } else {
379                Selection::selected(TITLE_CHECK, sources, None)
380            }
381        }
382    });
383
384    // The reporting policy.
385    out.push(match forge {
386        _ if !params.reporting_policy() => Selection::omitted(
387            REPORTING_POLICY,
388            Status::NotRequested,
389            "capabilities.reporting_policy is false",
390        ),
391        None => Selection::omitted(
392            REPORTING_POLICY,
393            Status::NotApplicable,
394            "the profile names no forge, and the policy names the forge's private channel",
395        ),
396        Some(forge) if !forge_known => Selection::omitted(
397            REPORTING_POLICY,
398            Status::Unknown,
399            format!(
400                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
401            ),
402        ),
403        Some(forge) => {
404            let sources = availability.owned("_shared", forge, REPORTING_POLICY);
405            if sources.is_empty() {
406                Selection::omitted(
407                    REPORTING_POLICY,
408                    Status::Unavailable,
409                    format!("this release ships no reporting policy for {forge}"),
410                )
411            } else {
412                Selection::selected(REPORTING_POLICY, sources, None)
413            }
414        }
415    });
416
417    out.push(automation);
418
419    // The Nix packaging, keyed on the release driver and the forge.
420    out.push(match (params.nix_packaging(), driver, forge) {
421        (false, _, _) => Selection::omitted(
422            PACKAGING_NIX,
423            Status::NotRequested,
424            "capabilities.nix_packaging is false",
425        ),
426        (true, None, _) => Selection::omitted(
427            PACKAGING_NIX,
428            Status::NotApplicable,
429            "the seed is keyed on an automatic release driver, and the profile names none",
430        ),
431        (true, _, None) => Selection::omitted(
432            PACKAGING_NIX,
433            Status::NotApplicable,
434            "the profile names no forge",
435        ),
436        (true, Some(driver), Some(forge)) => {
437            if !driver_known || !forge_known {
438                Selection::omitted(
439                    PACKAGING_NIX,
440                    Status::Unknown,
441                    format!("({driver}, {forge}) names a category this release does not know"),
442                )
443            } else {
444                let sources = availability.owned(driver, forge, PACKAGING_NIX);
445                if sources.is_empty() {
446                    Selection::omitted(
447                        PACKAGING_NIX,
448                        Status::Unavailable,
449                        format!("the {driver} binding ships no Nix seed on {forge}"),
450                    )
451                } else {
452                    Selection::selected(PACKAGING_NIX, sources, Some(driver))
453                }
454            }
455        }
456    });
457
458    // The Scorecard workflow, GitHub's alone.
459    out.push(match forge {
460        _ if !params.scorecard() => Selection::omitted(
461            SCORECARD,
462            Status::NotRequested,
463            "capabilities.scorecard is false",
464        ),
465        None => Selection::omitted(
466            SCORECARD,
467            Status::NotApplicable,
468            "the profile names no forge",
469        ),
470        Some(forge) if !forge_known => Selection::omitted(
471            SCORECARD,
472            Status::Unknown,
473            format!(
474                "the forge {forge} is not one this release drives; the forges are: github, gitlab"
475            ),
476        ),
477        Some(forge) => {
478            let sources = availability.owned("_shared", forge, SCORECARD);
479            if sources.is_empty() {
480                Selection::omitted(
481                    SCORECARD,
482                    Status::Unavailable,
483                    format!(
484                        "the Scorecard workflow is GitHub's alone, and the {forge} zone ships none"
485                    ),
486                )
487            } else {
488                Selection::selected(SCORECARD, sources, None)
489            }
490        }
491    });
492
493    // The code scanning workflow, keyed on the driver, the forge, and the
494    // provider.
495    out.push(match (params.code_scanning(), driver, forge) {
496        (None, _, _) => Selection::omitted(
497            CODE_SCANNING,
498            Status::NotRequested,
499            "capabilities.code_scanning is off",
500        ),
501        (Some(_), None, _) => Selection::omitted(
502            CODE_SCANNING,
503            Status::NotApplicable,
504            "a scanner reads the release driver's language, and the profile names no driver",
505        ),
506        (Some(_), _, None) => Selection::omitted(
507            CODE_SCANNING,
508            Status::NotApplicable,
509            "the profile names no forge",
510        ),
511        (Some(provider), Some(driver), Some(forge)) => {
512            if !driver_known || !forge_known {
513                Selection::omitted(
514                    CODE_SCANNING,
515                    Status::Unknown,
516                    format!("({driver}, {forge}) names a category this release does not know"),
517                )
518            } else if let Some(reason) = crate::projection::code_scanning_incompatibility(
519                Some(provider),
520                Some(driver),
521                Some(forge),
522            ) {
523                Selection::omitted(CODE_SCANNING, Status::Unavailable, reason)
524            } else {
525                let sources: Vec<String> = availability
526                    .owned(driver, forge, CODE_SCANNING)
527                    .into_iter()
528                    .filter(|path| {
529                        CODE_SCANNING_DESTINATIONS
530                            .iter()
531                            .any(|(name, owner)| path.ends_with(name) && *owner == provider)
532                    })
533                    .collect();
534                if sources.is_empty() {
535                    Selection::omitted(
536                        CODE_SCANNING,
537                        Status::Unavailable,
538                        format!(
539                            "the {driver} binding ships no {} workflow on {forge}",
540                            provider.as_str()
541                        ),
542                    )
543                } else {
544                    Selection::selected(CODE_SCANNING, sources, Some(driver))
545                }
546            }
547        }
548    });
549    debug_assert_eq!(out.len(), ALL.len());
550    out
551}
552
553/// The pin keys one selection matches in the registry's `used_by`: the
554/// bare capability id, and the id qualified by the driver where the
555/// capability has that dimension.
556#[must_use]
557pub fn pin_keys(selection: &Selection) -> Vec<String> {
558    let mut keys = vec![selection.id.to_owned()];
559    if let Some(driver) = &selection.driver {
560        keys.push(format!("{}/{driver}", selection.id));
561    }
562    keys
563}
564
565#[cfg(test)]
566mod tests {
567    use super::{
568        ALL, Availability, CODE_SCANNING, GUARDS, PACKAGING_NIX, RELEASE_AUTOMATION,
569        REPORTING_POLICY, SCORECARD, Status, TITLE_CHECK, owner_of, select,
570    };
571    use crate::landing::Params;
572    use crate::landing::manifest::{Provider, Style};
573    use crate::profile::ReleaseMode;
574
575    fn status_of(selections: &[super::Selection], id: &str) -> Status {
576        selections
577            .iter()
578            .find(|s| s.id == id)
579            .expect("every capability answers")
580            .status
581    }
582
583    /// Every embedded snippet has exactly one owner, and no shared path
584    /// falls to the release automation by default.
585    #[test]
586    fn every_embedded_snippet_has_one_owner() {
587        let availability = Availability::embedded();
588        assert!(!availability.files().is_empty());
589        for path in availability.files() {
590            let owner = owner_of(path);
591            assert!(owner.is_some(), "{path}: no capability owns it");
592            assert!(
593                ALL.contains(&owner.unwrap_or_default()),
594                "{path}: an unlisted owner"
595            );
596        }
597        assert_eq!(
598            owner_of("_shared/github/SECURITY.md"),
599            Some(REPORTING_POLICY)
600        );
601        assert_eq!(
602            owner_of("_shared/github/.github/workflows/scorecard.yml"),
603            Some(SCORECARD)
604        );
605        assert_eq!(owner_of("rust/github/flake.nix"), Some(PACKAGING_NIX));
606        assert_eq!(
607            owner_of("rust/github/.github/workflows/code-scanning-codeql.yml"),
608            Some(CODE_SCANNING)
609        );
610        assert_eq!(
611            owner_of("rust/github/release-plz.toml"),
612            Some(RELEASE_AUTOMATION)
613        );
614        assert_eq!(owner_of("_shared/github/unknown.txt"), None);
615    }
616
617    /// SATISFIES project-profile:availability-belongs-to-a-capability-at-its-dimensions
618    #[test]
619    fn the_catalog_answers_availability_per_tuple() {
620        let availability = Availability::embedded();
621        let mut github = Params::for_test("acme/widget", Some(Style::Trunk));
622        github.set_pair_for_test("python", "github");
623        let mut gitlab = github.clone();
624        gitlab.set_pair_for_test("python", "gitlab");
625        let on_github = select(&github, &availability);
626        let on_gitlab = select(&gitlab, &availability);
627        assert_eq!(status_of(&on_github, RELEASE_AUTOMATION), Status::Selected);
628        assert_eq!(
629            status_of(&on_gitlab, RELEASE_AUTOMATION),
630            Status::Unavailable
631        );
632        let reason = on_gitlab
633            .iter()
634            .find(|s| s.id == RELEASE_AUTOMATION)
635            .and_then(|s| s.reason.clone())
636            .expect("an unavailable capability states why");
637        assert!(reason.contains("rust, gitlab"), "{reason}");
638        assert!(reason.contains("python, github"), "{reason}");
639        // The title gate still lands on GitLab, with the release-less root
640        // pipeline beside the fragment.
641        let title = on_gitlab
642            .iter()
643            .find(|s| s.id == TITLE_CHECK)
644            .expect("the title gate answers");
645        assert_eq!(title.status, Status::Selected);
646        assert!(
647            title
648                .sources
649                .iter()
650                .any(|s| s == "_title-gate/gitlab/.gitlab-ci.yml"),
651            "{:?}",
652            title.sources
653        );
654    }
655
656    /// A no-forge, no-release input selects the local guards alone and
657    /// marks every forge capability not applicable.
658    #[test]
659    fn a_no_forge_input_selects_the_guards_alone() {
660        let params = Params::for_test_release_less(&[], None, ReleaseMode::None);
661        let selections = select(&params, &Availability::embedded());
662        assert_eq!(status_of(&selections, GUARDS), Status::Selected);
663        for id in [
664            TITLE_CHECK,
665            REPORTING_POLICY,
666            RELEASE_AUTOMATION,
667            PACKAGING_NIX,
668        ] {
669            assert_ne!(status_of(&selections, id), Status::Selected, "{id}");
670        }
671        assert_eq!(status_of(&selections, TITLE_CHECK), Status::NotApplicable);
672        assert_eq!(
673            status_of(&selections, RELEASE_AUTOMATION),
674            Status::NotRequested
675        );
676        assert_eq!(status_of(&selections, PACKAGING_NIX), Status::NotRequested);
677    }
678
679    /// An unknown forge keeps the guards and reports the rest unknown.
680    #[test]
681    fn an_unknown_forge_reads_as_unknown_and_lands_the_guards() {
682        let mut params = Params::for_test_release_less(&[], Some("codeberg"), ReleaseMode::None);
683        params.set_scorecard_for_test(true);
684        let selections = select(&params, &Availability::embedded());
685        assert_eq!(status_of(&selections, GUARDS), Status::Selected);
686        assert_eq!(status_of(&selections, TITLE_CHECK), Status::Unknown);
687        assert_eq!(status_of(&selections, SCORECARD), Status::Unknown);
688    }
689
690    /// The scanner follows the provider and the pair, and a request the
691    /// pair cannot run reports `Unavailable` with the reason the landing
692    /// then omits it by. Nothing refuses on it.
693    #[test]
694    fn code_scanning_selects_the_providers_own_file() {
695        let availability = Availability::embedded();
696        let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
697        params.set_code_scanning_for_test(Some(Provider::Semgrep));
698        let selections = select(&params, &availability);
699        let scanning = selections
700            .iter()
701            .find(|s| s.id == CODE_SCANNING)
702            .expect("answers");
703        assert_eq!(scanning.status, Status::Selected);
704        assert_eq!(
705            scanning.sources,
706            vec!["rust/github/.github/workflows/code-scanning-semgrep.yml".to_owned()]
707        );
708        params.set_pair_for_test("bash", "github");
709        let selections = select(&params, &availability);
710        assert_eq!(status_of(&selections, CODE_SCANNING), Status::Unavailable);
711    }
712}