use serde::Serialize;
use super::{Params, ReleaseMode};
use crate::detect::Forge;
use crate::landing::{Integration, MergeForm};
use crate::projection::{CODE_SCANNING_DESTINATIONS, NIX_DESTINATIONS};
pub const GUARDS: &str = "git.guards";
pub const WORKFLOW_GUARDS: &str = "git.workflow-guards";
pub const TITLE_CHECK: &str = "git.title-check";
pub const COMMIT_CHECK: &str = "git.commit-check";
pub const REPORTING_POLICY: &str = "security.reporting-policy";
pub const RELEASE_AUTOMATION: &str = "release.automation";
pub const PACKAGING_NIX: &str = "packaging.nix";
pub const SCORECARD: &str = "supply-chain.scorecard";
pub const CODE_SCANNING: &str = "supply-chain.code-scanning";
pub const ALL: [&str; 9] = [
GUARDS,
WORKFLOW_GUARDS,
TITLE_CHECK,
COMMIT_CHECK,
REPORTING_POLICY,
RELEASE_AUTOMATION,
PACKAGING_NIX,
SCORECARD,
CODE_SCANNING,
];
pub const TITLE_GATE_ZONE: &str = "_title-gate";
pub const COMMIT_GATE_ZONE: &str = "_commit-gate";
#[must_use]
pub fn owner_of(path: &str) -> Option<&'static str> {
let mut segments = path.splitn(3, '/');
let (zone, _forge, destination) = (segments.next()?, segments.next()?, segments.next()?);
if zone == "_shared" {
return match destination {
"SECURITY.md" => Some(REPORTING_POLICY),
".github/workflows/pr-title.yml" | ".gitlab/ci/mr-title.yml" => Some(TITLE_CHECK),
".github/workflows/pr-commits.yml" | ".gitlab/ci/mr-commits.yml" => Some(COMMIT_CHECK),
".github/workflows/scorecard.yml" => Some(SCORECARD),
_ => None,
};
}
if zone == TITLE_GATE_ZONE {
return (destination == ".gitlab-ci.yml").then_some(TITLE_CHECK);
}
if zone == COMMIT_GATE_ZONE {
return (destination == ".gitlab-ci.yml").then_some(COMMIT_CHECK);
}
if zone.starts_with('_') {
return None;
}
if NIX_DESTINATIONS.contains(&destination) {
return Some(PACKAGING_NIX);
}
if CODE_SCANNING_DESTINATIONS
.iter()
.any(|(name, _)| *name == destination)
{
return Some(CODE_SCANNING);
}
Some(RELEASE_AUTOMATION)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
Selected,
NotRequested,
NotApplicable,
Unavailable,
Unknown,
Withheld,
}
impl Status {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Selected => "selected",
Self::NotRequested => "not-requested",
Self::NotApplicable => "not-applicable",
Self::Unavailable => "unavailable",
Self::Unknown => "unknown",
Self::Withheld => "withheld",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Selection {
pub id: &'static str,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(skip)]
pub sources: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}
impl Selection {
#[must_use]
pub const fn lands(&self) -> bool {
matches!(self.status, Status::Selected | Status::Withheld)
}
fn selected(id: &'static str, sources: Vec<String>, driver: Option<&str>) -> Self {
Self {
id,
status: Status::Selected,
reason: None,
action: None,
sources,
driver: driver.map(str::to_owned),
provider: None,
}
}
fn keyed_on(mut self, provider: &str) -> Self {
self.provider = Some(provider.to_owned());
self
}
fn omitted(id: &'static str, status: Status, reason: impl Into<String>) -> Self {
Self {
id,
status,
reason: Some(reason.into()),
action: None,
sources: Vec::new(),
driver: None,
provider: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Availability {
files: Vec<String>,
}
impl Availability {
#[must_use]
pub fn over(files: Vec<String>) -> Self {
let mut files = files;
files.sort();
Self { files }
}
#[must_use]
pub fn embedded() -> Self {
Self::over(
crate::embedded::walk(&crate::embedded::SNIPPETS)
.into_iter()
.map(|(path, _)| path)
.collect(),
)
}
#[must_use]
pub fn files(&self) -> &[String] {
&self.files
}
fn owned(&self, zone: &str, forge: &str, capability: &str) -> Vec<String> {
let prefix = format!("{zone}/{forge}/");
self.files
.iter()
.filter(|path| path.starts_with(&prefix) && owner_of(path) == Some(capability))
.cloned()
.collect()
}
#[must_use]
pub fn drivers(&self) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for path in &self.files {
if let Some((zone, _)) = path.split_once('/')
&& !zone.starts_with('_')
&& !out.iter().any(|known| known == zone)
{
out.push(zone.to_owned());
}
}
out
}
#[must_use]
pub fn automation_tuples(&self) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for path in &self.files {
let mut segments = path.splitn(3, '/');
if let (Some(driver), Some(forge), Some(_)) =
(segments.next(), segments.next(), segments.next())
&& !driver.starts_with('_')
&& owner_of(path) == Some(RELEASE_AUTOMATION)
{
let entry = format!("{driver}, {forge}");
if !out.contains(&entry) {
out.push(entry);
}
}
}
out
}
}
#[must_use]
pub const fn forge_gate_lands(integration: Integration, mode: ReleaseMode) -> bool {
matches!(integration, Integration::Forge) || matches!(mode, ReleaseMode::Automatic)
}
#[must_use]
pub fn known_drivers() -> Vec<String> {
Availability::embedded().drivers()
}
pub fn registry_answer_fits(
mode: ReleaseMode,
driver: Option<&str>,
answer: crate::profile::RegistryAnswer,
) -> Result<(), String> {
let shape = format!(
"profile.release.registry takes {}, under profile.release.mode = \"automatic\" alone",
crate::profile::RegistryAnswer::SHAPE
);
match (mode, driver) {
(ReleaseMode::Automatic, Some(driver)) if answer.fits(driver) => Ok(()),
(ReleaseMode::Automatic, Some(other)) => Err(format!(
"profile.release.registry is {} while profile.release.driver is {other}; {shape}",
answer.as_str()
)),
(ReleaseMode::Automatic, None) => Err(format!(
"profile.release.registry is set while no profile.release.driver is named; {shape}"
)),
(mode, _) => Err(format!(
"profile.release.registry is set while profile.release.mode is {}; {shape}",
mode.as_str()
)),
}
}
pub fn build_answer_fits(mode: ReleaseMode, driver: Option<&str>) -> Result<(), String> {
let shape = format!(
"profile.release.build names the command and the packages of a release whose pipeline builds the distribution, under profile.release.mode = \"automatic\" with profile.release.driver = \"{}\" alone",
crate::profile::BuildAnswer::DRIVER
);
match (mode, driver) {
(ReleaseMode::Automatic, Some(crate::profile::BuildAnswer::DRIVER)) => Ok(()),
(ReleaseMode::Automatic, Some(other)) => Err(format!(
"profile.release.build is set while profile.release.driver is {other}; {shape}"
)),
(ReleaseMode::Automatic, None) => Err(format!(
"profile.release.build is set while no profile.release.driver is named; {shape}"
)),
(mode, _) => Err(format!(
"profile.release.build is set while profile.release.mode is {}; {shape}",
mode.as_str()
)),
}
}
fn known_forge(name: &str) -> bool {
Forge::parse(name).is_some()
}
#[must_use]
#[allow(
clippy::too_many_lines,
reason = "one pass answers every capability, and splitting it would separate a selection from the dimensions that decided it"
)]
pub fn select(params: &Params, availability: &Availability) -> Vec<Selection> {
let forge = params.forge();
let driver = params.driver();
let known_drivers = availability.drivers();
let driver_known = driver.is_some_and(|d| known_drivers.iter().any(|k| k == d));
let forge_known = forge.is_some_and(known_forge);
let mut out = Vec::with_capacity(ALL.len());
out.push(Selection::selected(GUARDS, Vec::new(), None));
out.push(if params.workflow_guards() {
Selection::selected(WORKFLOW_GUARDS, Vec::new(), None)
} else {
Selection::omitted(
WORKFLOW_GUARDS,
Status::NotRequested,
"capabilities.workflow_guards is false",
)
});
let automation = match (params.release_mode(), driver, forge) {
(ReleaseMode::External, _, _) => Selection::omitted(
RELEASE_AUTOMATION,
Status::NotRequested,
"the release is external: the target releases through a process release-kit does not drive",
),
(ReleaseMode::None, _, _) => Selection::omitted(
RELEASE_AUTOMATION,
Status::NotRequested,
"the release mode is none",
),
(ReleaseMode::Automatic, None, _) => Selection::omitted(
RELEASE_AUTOMATION,
Status::NotApplicable,
"an automatic release names no driver",
),
(ReleaseMode::Automatic, _, None) => Selection::omitted(
RELEASE_AUTOMATION,
Status::NotApplicable,
"the profile names no forge",
),
(ReleaseMode::Automatic, Some(driver), Some(forge)) => {
if !driver_known {
Selection::omitted(
RELEASE_AUTOMATION,
Status::Unknown,
format!(
"the driver {driver} is not one this release knows; the bindings are: {}",
known_drivers.join(", ")
),
)
} else if !forge_known {
Selection::omitted(
RELEASE_AUTOMATION,
Status::Unknown,
format!(
"the forge {forge} is not one this release drives; the forges are: github, gitlab"
),
)
} else {
let sources = availability.owned(driver, forge, RELEASE_AUTOMATION);
if sources.is_empty() {
Selection::omitted(
RELEASE_AUTOMATION,
Status::Unavailable,
format!(
"the release automation at ({driver}, {forge}) has no landable files; the available tuples are: {}",
availability.automation_tuples().join("; ")
),
)
} else {
Selection::selected(RELEASE_AUTOMATION, sources, Some(driver))
}
}
}
};
let automation_lands = automation.status == Status::Selected;
let (gate, zone, other, why_not) = match params.merge_form() {
MergeForm::Squash => (
TITLE_CHECK,
TITLE_GATE_ZONE,
COMMIT_CHECK,
"git.merge_form is squash, so the title gate judges the request",
),
MergeForm::Rebase => (
COMMIT_CHECK,
COMMIT_GATE_ZONE,
TITLE_CHECK,
"git.merge_form is rebase, so the commit gate judges every commit of the request",
),
};
let no_gate = (!forge_gate_lands(params.integration(), params.release_mode())).then(|| {
format!(
"no forge gate lands: git.integration is local and the release is not automatic (profile.release.mode is {}), so no request reaches the trunk for a gate to judge",
params.release_mode().as_str()
)
});
let selected_gate = match forge {
None => Selection::omitted(gate, Status::NotApplicable, "the profile names no forge"),
Some(forge) if !forge_known => Selection::omitted(
gate,
Status::Unknown,
format!(
"the forge {forge} is not one this release drives; the forges are: github, gitlab"
),
),
Some(_) if no_gate.is_some() => Selection::omitted(
gate,
Status::NotApplicable,
no_gate.clone().unwrap_or_default(),
),
Some(forge) => {
let mut sources = availability.owned("_shared", forge, gate);
if !automation_lands {
sources.extend(availability.owned(zone, forge, gate));
}
if sources.is_empty() {
Selection::omitted(
gate,
Status::Unavailable,
format!(
"this release ships no {} gate for {forge}",
if gate == TITLE_CHECK {
"title"
} else {
"commit"
}
),
)
} else {
Selection::selected(gate, sources, None)
}
}
};
let other_gate = no_gate.map_or_else(
|| Selection::omitted(other, Status::NotRequested, why_not),
|reason| Selection::omitted(other, Status::NotApplicable, reason),
);
if gate == TITLE_CHECK {
out.push(selected_gate);
out.push(other_gate);
} else {
out.push(other_gate);
out.push(selected_gate);
}
out.push(match forge {
_ if !params.reporting_policy() => Selection::omitted(
REPORTING_POLICY,
Status::NotRequested,
"capabilities.reporting_policy is false",
),
None => Selection::omitted(
REPORTING_POLICY,
Status::NotApplicable,
"the profile names no forge, and the policy names the forge's private channel",
),
Some(forge) if !forge_known => Selection::omitted(
REPORTING_POLICY,
Status::Unknown,
format!(
"the forge {forge} is not one this release drives; the forges are: github, gitlab"
),
),
Some(forge) => {
let sources = availability.owned("_shared", forge, REPORTING_POLICY);
if sources.is_empty() {
Selection::omitted(
REPORTING_POLICY,
Status::Unavailable,
format!("this release ships no reporting policy for {forge}"),
)
} else {
Selection::selected(REPORTING_POLICY, sources, None)
}
}
});
out.push(automation);
out.push(match (params.nix_packaging(), driver, forge) {
(false, _, _) => Selection::omitted(
PACKAGING_NIX,
Status::NotRequested,
"capabilities.nix_packaging is false",
),
(true, None, _) => Selection::omitted(
PACKAGING_NIX,
Status::NotApplicable,
"the seed is keyed on an automatic release driver, and the profile names none",
),
(true, _, None) => Selection::omitted(
PACKAGING_NIX,
Status::NotApplicable,
"the profile names no forge",
),
(true, Some(driver), Some(forge)) => {
if !driver_known || !forge_known {
Selection::omitted(
PACKAGING_NIX,
Status::Unknown,
format!("({driver}, {forge}) names a category this release does not know"),
)
} else {
let sources = availability.owned(driver, forge, PACKAGING_NIX);
if sources.is_empty() {
Selection::omitted(
PACKAGING_NIX,
Status::Unavailable,
format!("the {driver} binding ships no Nix seed on {forge}"),
)
} else {
Selection::selected(PACKAGING_NIX, sources, Some(driver))
}
}
}
});
out.push(match forge {
_ if !params.scorecard() => Selection::omitted(
SCORECARD,
Status::NotRequested,
"capabilities.scorecard is false",
),
None => Selection::omitted(
SCORECARD,
Status::NotApplicable,
"the profile names no forge",
),
Some(forge) if !forge_known => Selection::omitted(
SCORECARD,
Status::Unknown,
format!(
"the forge {forge} is not one this release drives; the forges are: github, gitlab"
),
),
Some(forge) => {
let sources = availability.owned("_shared", forge, SCORECARD);
if sources.is_empty() {
Selection::omitted(
SCORECARD,
Status::Unavailable,
format!(
"the Scorecard workflow is GitHub's alone, and the {forge} zone ships none"
),
)
} else {
Selection::selected(SCORECARD, sources, None)
}
}
});
out.push(match (params.code_scanning(), driver, forge) {
(None, _, _) => Selection::omitted(
CODE_SCANNING,
Status::NotRequested,
"capabilities.code_scanning is off",
),
(Some(_), None, _) => Selection::omitted(
CODE_SCANNING,
Status::NotApplicable,
"a scanner reads the release driver's language, and the profile names no driver",
),
(Some(_), _, None) => Selection::omitted(
CODE_SCANNING,
Status::NotApplicable,
"the profile names no forge",
),
(Some(provider), Some(driver), Some(forge)) => {
if !driver_known || !forge_known {
Selection::omitted(
CODE_SCANNING,
Status::Unknown,
format!("({driver}, {forge}) names a category this release does not know"),
)
} else if let Some(reason) = crate::projection::code_scanning_incompatibility(
Some(provider),
Some(driver),
Some(forge),
) {
Selection::omitted(CODE_SCANNING, Status::Unavailable, reason)
} else {
let sources: Vec<String> = availability
.owned(driver, forge, CODE_SCANNING)
.into_iter()
.filter(|path| {
CODE_SCANNING_DESTINATIONS
.iter()
.any(|(name, owner)| path.ends_with(name) && *owner == provider)
})
.collect();
if sources.is_empty() {
Selection::omitted(
CODE_SCANNING,
Status::Unavailable,
format!(
"the {driver} binding ships no {} workflow on {forge}",
provider.as_str()
),
)
} else {
Selection::selected(CODE_SCANNING, sources, Some(driver))
.keyed_on(provider.as_str())
}
}
}
});
debug_assert_eq!(out.len(), ALL.len());
out
}
#[must_use]
pub fn pin_keys(selection: &Selection) -> Vec<String> {
let mut keys = vec![selection.id.to_owned()];
if let Some(driver) = &selection.driver {
keys.push(format!("{}/{driver}", selection.id));
if let Some(provider) = &selection.provider {
keys.push(format!("{}/{driver}/{provider}", selection.id));
}
}
keys
}
#[cfg(test)]
mod tests {
use super::{
ALL, Availability, CODE_SCANNING, COMMIT_CHECK, GUARDS, PACKAGING_NIX, RELEASE_AUTOMATION,
REPORTING_POLICY, SCORECARD, Status, TITLE_CHECK, WORKFLOW_GUARDS, owner_of, select,
};
use crate::landing::Params;
use crate::landing::manifest::{Provider, Style};
use crate::profile::ReleaseMode;
fn status_of(selections: &[super::Selection], id: &str) -> Status {
selections
.iter()
.find(|s| s.id == id)
.expect("every capability answers")
.status
}
#[test]
fn every_embedded_snippet_has_one_owner() {
let availability = Availability::embedded();
assert!(!availability.files().is_empty());
for path in availability.files() {
let owner = owner_of(path);
assert!(owner.is_some(), "{path}: no capability owns it");
assert!(
ALL.contains(&owner.unwrap_or_default()),
"{path}: an unlisted owner"
);
}
assert_eq!(
owner_of("_shared/github/SECURITY.md"),
Some(REPORTING_POLICY)
);
assert_eq!(
owner_of("_shared/github/.github/workflows/scorecard.yml"),
Some(SCORECARD)
);
assert_eq!(owner_of("rust/github/flake.nix"), Some(PACKAGING_NIX));
assert_eq!(
owner_of("rust/github/.github/workflows/code-scanning-codeql.yml"),
Some(CODE_SCANNING)
);
assert_eq!(
owner_of("rust/github/release-plz.toml"),
Some(RELEASE_AUTOMATION)
);
assert_eq!(owner_of("_shared/github/unknown.txt"), None);
}
#[test]
fn the_catalog_answers_availability_per_tuple() {
let availability = Availability::embedded();
let mut rust = Params::for_test("acme/widget", Some(Style::Trunk));
rust.set_pair_for_test("rust", "gitlab");
rust.set_code_scanning_for_test(Some(crate::projection::Provider::Semgrep));
let mut python = rust.clone();
python.set_pair_for_test("python", "gitlab");
assert_eq!(
status_of(&select(&rust, &availability), CODE_SCANNING),
Status::Selected
);
let on_python = select(&python, &availability);
assert_eq!(status_of(&on_python, CODE_SCANNING), Status::Unavailable);
let mut github = python.clone();
github.set_pair_for_test("python", "github");
for params in [&python, &github] {
assert_eq!(
status_of(&select(params, &availability), RELEASE_AUTOMATION),
Status::Selected
);
}
let narrowed = Availability::over(
availability
.files()
.iter()
.filter(|path| !path.starts_with("python/gitlab/"))
.cloned()
.collect(),
);
let on_gitlab = select(&python, &narrowed);
assert_eq!(
status_of(&on_gitlab, RELEASE_AUTOMATION),
Status::Unavailable
);
let reason = on_gitlab
.iter()
.find(|s| s.id == RELEASE_AUTOMATION)
.and_then(|s| s.reason.clone())
.expect("an unavailable capability states why");
assert!(reason.contains("rust, gitlab"), "{reason}");
assert!(reason.contains("python, github"), "{reason}");
let tuples = reason
.split_once("the available tuples are:")
.map(|(_, tuples)| tuples)
.expect("the reason names the tuples");
assert!(!tuples.contains("python, gitlab"), "{reason}");
let title = on_gitlab
.iter()
.find(|s| s.id == TITLE_CHECK)
.expect("the title gate answers");
assert_eq!(title.status, Status::Selected);
assert!(
title
.sources
.iter()
.any(|s| s == "_title-gate/gitlab/.gitlab-ci.yml"),
"{:?}",
title.sources
);
}
#[test]
fn a_no_forge_input_selects_the_guards_alone() {
let params = Params::for_test_release_less(&[], None, ReleaseMode::None);
let selections = select(¶ms, &Availability::embedded());
assert_eq!(status_of(&selections, GUARDS), Status::Selected);
for id in [
TITLE_CHECK,
REPORTING_POLICY,
RELEASE_AUTOMATION,
PACKAGING_NIX,
] {
assert_ne!(status_of(&selections, id), Status::Selected, "{id}");
}
assert_eq!(status_of(&selections, TITLE_CHECK), Status::NotApplicable);
assert_eq!(
status_of(&selections, RELEASE_AUTOMATION),
Status::NotRequested
);
assert_eq!(status_of(&selections, PACKAGING_NIX), Status::NotRequested);
}
#[test]
fn the_workflow_guards_are_selected_only_when_requested() {
let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
let selections = select(¶ms, &Availability::embedded());
assert_eq!(status_of(&selections, WORKFLOW_GUARDS), Status::Selected);
params.set_workflow_guards_for_test(false);
let selections = select(¶ms, &Availability::embedded());
assert_eq!(
status_of(&selections, WORKFLOW_GUARDS),
Status::NotRequested
);
assert_eq!(status_of(&selections, GUARDS), Status::Selected);
}
#[test]
fn workflow_only_local_landing_selects_no_forge_gate() {
use crate::landing::{Integration, MergeForm};
let availability = Availability::embedded();
for forge in ["github", "gitlab"] {
for form in [MergeForm::Squash, MergeForm::Rebase] {
for (mode, integration, lands) in [
(ReleaseMode::None, Integration::Local, false),
(ReleaseMode::External, Integration::Local, false),
(ReleaseMode::None, Integration::Forge, true),
(ReleaseMode::External, Integration::Forge, true),
] {
let mut params = Params::for_test_release_less(&[], Some(forge), mode);
params.set_integration_for_test(integration);
params.set_merge_form_for_test(form);
let selections = select(¶ms, &availability);
let (gate, other) = match form {
MergeForm::Squash => (TITLE_CHECK, COMMIT_CHECK),
MergeForm::Rebase => (COMMIT_CHECK, TITLE_CHECK),
};
let at = format!("{forge} {form:?} {mode:?} {integration:?}");
let selection = selections
.iter()
.find(|s| s.id == gate)
.expect("every capability answers");
assert_eq!(selection.lands(), lands, "{at}");
if !lands {
assert_eq!(selection.status, Status::NotApplicable, "{at}");
assert!(selection.sources.is_empty(), "{at}");
let reason = selection.reason.as_deref().unwrap_or_default();
assert!(reason.contains("no forge gate lands"), "{at}: {reason}");
assert!(
reason.contains("git.integration is local"),
"{at}: {reason}"
);
}
let unnamed = selections
.iter()
.find(|s| s.id == other)
.expect("every capability answers");
let reason = unnamed.reason.as_deref().unwrap_or_default();
if lands {
assert_eq!(unnamed.status, Status::NotRequested, "{at}");
assert!(reason.contains("git.merge_form is"), "{at}: {reason}");
} else {
assert_eq!(unnamed.status, Status::NotApplicable, "{at}");
assert!(reason.contains("no forge gate lands"), "{at}: {reason}");
assert!(!reason.contains("merge_form"), "{at}: {reason}");
}
}
}
let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
params.set_pair_for_test("rust", forge);
params.set_integration_for_test(Integration::Local);
let selections = select(¶ms, &availability);
assert_eq!(
status_of(&selections, TITLE_CHECK),
Status::Selected,
"{forge}"
);
}
}
#[test]
fn an_unknown_forge_reads_as_unknown_and_lands_the_guards() {
let mut params = Params::for_test_release_less(&[], Some("codeberg"), ReleaseMode::None);
params.set_scorecard_for_test(true);
let selections = select(¶ms, &Availability::embedded());
assert_eq!(status_of(&selections, GUARDS), Status::Selected);
assert_eq!(status_of(&selections, TITLE_CHECK), Status::Unknown);
assert_eq!(status_of(&selections, SCORECARD), Status::Unknown);
}
#[test]
fn code_scanning_selects_the_providers_own_file() {
let availability = Availability::embedded();
let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
params.set_code_scanning_for_test(Some(Provider::Semgrep));
let selections = select(¶ms, &availability);
let scanning = selections
.iter()
.find(|s| s.id == CODE_SCANNING)
.expect("answers");
assert_eq!(scanning.status, Status::Selected);
assert_eq!(
scanning.sources,
vec!["rust/github/.github/workflows/code-scanning-semgrep.yml".to_owned()]
);
params.set_pair_for_test("bash", "github");
let selections = select(¶ms, &availability);
assert_eq!(status_of(&selections, CODE_SCANNING), Status::Unavailable);
}
#[test]
fn commit_gate_replaces_the_title_gate_under_rebase() {
use crate::landing::{Integration, MergeForm};
let availability = Availability::embedded();
for integration in [Integration::Forge, Integration::Local] {
for (driver, forge) in [("rust", "github"), ("rust", "gitlab"), ("python", "gitlab")] {
let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
params.set_pair_for_test(driver, forge);
params.set_integration_for_test(integration);
let squash = select(¶ms, &availability);
assert_eq!(status_of(&squash, TITLE_CHECK), Status::Selected);
assert_eq!(status_of(&squash, COMMIT_CHECK), Status::NotRequested);
params.set_merge_form_for_test(MergeForm::Rebase);
let rebase = select(¶ms, &availability);
assert_eq!(status_of(&rebase, TITLE_CHECK), Status::NotRequested);
let gate = rebase
.iter()
.find(|s| s.id == COMMIT_CHECK)
.expect("the commit gate answers");
assert_eq!(gate.status, Status::Selected, "{driver} {forge}");
assert!(
gate.sources.iter().all(|source| !source.contains("title")),
"{:?}",
gate.sources
);
let fragment = if forge == "github" {
"_shared/github/.github/workflows/pr-commits.yml"
} else {
"_shared/gitlab/.gitlab/ci/mr-commits.yml"
};
assert!(
gate.sources.iter().any(|s| s == fragment),
"{:?}",
gate.sources
);
assert!(
!gate
.sources
.iter()
.any(|s| s == "_commit-gate/gitlab/.gitlab-ci.yml"),
"{driver} {forge}: {:?}",
gate.sources
);
}
}
assert_eq!(
owner_of("_shared/github/.github/workflows/pr-commits.yml"),
Some(COMMIT_CHECK)
);
assert_eq!(
owner_of("_commit-gate/gitlab/.gitlab-ci.yml"),
Some(COMMIT_CHECK)
);
assert_eq!(ALL.len(), 9);
}
}