1use serde::Serialize;
18
19use super::{Params, ReleaseMode};
20use crate::detect::Forge;
21use crate::landing::{Integration, MergeForm};
22use crate::projection::{CODE_SCANNING_DESTINATIONS, NIX_DESTINATIONS};
23
24pub const GUARDS: &str = "git.guards";
30pub const WORKFLOW_GUARDS: &str = "git.workflow-guards";
34pub const TITLE_CHECK: &str = "git.title-check";
38pub const COMMIT_CHECK: &str = "git.commit-check";
42pub const REPORTING_POLICY: &str = "security.reporting-policy";
44pub const RELEASE_AUTOMATION: &str = "release.automation";
47pub const PACKAGING_NIX: &str = "packaging.nix";
49pub const SCORECARD: &str = "supply-chain.scorecard";
51pub const CODE_SCANNING: &str = "supply-chain.code-scanning";
53
54pub const ALL: [&str; 9] = [
56 GUARDS,
57 WORKFLOW_GUARDS,
58 TITLE_CHECK,
59 COMMIT_CHECK,
60 REPORTING_POLICY,
61 RELEASE_AUTOMATION,
62 PACKAGING_NIX,
63 SCORECARD,
64 CODE_SCANNING,
65];
66
67pub const TITLE_GATE_ZONE: &str = "_title-gate";
71
72pub const COMMIT_GATE_ZONE: &str = "_commit-gate";
76
77#[must_use]
84pub fn owner_of(path: &str) -> Option<&'static str> {
85 let mut segments = path.splitn(3, '/');
86 let (zone, _forge, destination) = (segments.next()?, segments.next()?, segments.next()?);
87 if zone == "_shared" {
88 return match destination {
89 "SECURITY.md" => Some(REPORTING_POLICY),
90 ".github/workflows/pr-title.yml" | ".gitlab/ci/mr-title.yml" => Some(TITLE_CHECK),
91 ".github/workflows/pr-commits.yml" | ".gitlab/ci/mr-commits.yml" => Some(COMMIT_CHECK),
92 ".github/workflows/scorecard.yml" => Some(SCORECARD),
93 _ => None,
94 };
95 }
96 if zone == TITLE_GATE_ZONE {
97 return (destination == ".gitlab-ci.yml").then_some(TITLE_CHECK);
98 }
99 if zone == COMMIT_GATE_ZONE {
100 return (destination == ".gitlab-ci.yml").then_some(COMMIT_CHECK);
101 }
102 if zone.starts_with('_') {
103 return None;
104 }
105 if NIX_DESTINATIONS.contains(&destination) {
106 return Some(PACKAGING_NIX);
107 }
108 if CODE_SCANNING_DESTINATIONS
109 .iter()
110 .any(|(name, _)| *name == destination)
111 {
112 return Some(CODE_SCANNING);
113 }
114 Some(RELEASE_AUTOMATION)
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
119#[serde(rename_all = "kebab-case")]
120pub enum Status {
121 Selected,
124 NotRequested,
126 NotApplicable,
129 Unavailable,
131 Unknown,
133 Withheld,
137}
138
139impl Status {
140 #[must_use]
142 pub const fn as_str(self) -> &'static str {
143 match self {
144 Self::Selected => "selected",
145 Self::NotRequested => "not-requested",
146 Self::NotApplicable => "not-applicable",
147 Self::Unavailable => "unavailable",
148 Self::Unknown => "unknown",
149 Self::Withheld => "withheld",
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
156pub struct Selection {
157 pub id: &'static str,
159 pub status: Status,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub reason: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub action: Option<String>,
167 #[serde(skip)]
170 pub sources: Vec<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
174 pub driver: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
180 pub provider: Option<String>,
181}
182
183impl Selection {
184 #[must_use]
186 pub const fn lands(&self) -> bool {
187 matches!(self.status, Status::Selected | Status::Withheld)
188 }
189
190 fn selected(id: &'static str, sources: Vec<String>, driver: Option<&str>) -> Self {
191 Self {
192 id,
193 status: Status::Selected,
194 reason: None,
195 action: None,
196 sources,
197 driver: driver.map(str::to_owned),
198 provider: None,
199 }
200 }
201
202 fn keyed_on(mut self, provider: &str) -> Self {
204 self.provider = Some(provider.to_owned());
205 self
206 }
207
208 fn omitted(id: &'static str, status: Status, reason: impl Into<String>) -> Self {
209 Self {
210 id,
211 status,
212 reason: Some(reason.into()),
213 action: None,
214 sources: Vec::new(),
215 driver: None,
216 provider: None,
217 }
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Availability {
225 files: Vec<String>,
226}
227
228impl Availability {
229 #[must_use]
232 pub fn over(files: Vec<String>) -> Self {
233 let mut files = files;
234 files.sort();
235 Self { files }
236 }
237
238 #[must_use]
240 pub fn embedded() -> Self {
241 Self::over(
242 crate::embedded::walk(&crate::embedded::SNIPPETS)
243 .into_iter()
244 .map(|(path, _)| path)
245 .collect(),
246 )
247 }
248
249 #[must_use]
251 pub fn files(&self) -> &[String] {
252 &self.files
253 }
254
255 fn owned(&self, zone: &str, forge: &str, capability: &str) -> Vec<String> {
257 let prefix = format!("{zone}/{forge}/");
258 self.files
259 .iter()
260 .filter(|path| path.starts_with(&prefix) && owner_of(path) == Some(capability))
261 .cloned()
262 .collect()
263 }
264
265 #[must_use]
267 pub fn drivers(&self) -> Vec<String> {
268 let mut out: Vec<String> = Vec::new();
269 for path in &self.files {
270 if let Some((zone, _)) = path.split_once('/')
271 && !zone.starts_with('_')
272 && !out.iter().any(|known| known == zone)
273 {
274 out.push(zone.to_owned());
275 }
276 }
277 out
278 }
279
280 #[must_use]
283 pub fn automation_tuples(&self) -> Vec<String> {
284 let mut out: Vec<String> = Vec::new();
285 for path in &self.files {
286 let mut segments = path.splitn(3, '/');
287 if let (Some(driver), Some(forge), Some(_)) =
288 (segments.next(), segments.next(), segments.next())
289 && !driver.starts_with('_')
290 && owner_of(path) == Some(RELEASE_AUTOMATION)
291 {
292 let entry = format!("{driver}, {forge}");
293 if !out.contains(&entry) {
294 out.push(entry);
295 }
296 }
297 }
298 out
299 }
300}
301
302#[must_use]
316pub const fn forge_gate_lands(integration: Integration, mode: ReleaseMode) -> bool {
317 matches!(integration, Integration::Forge) || matches!(mode, ReleaseMode::Automatic)
318}
319
320#[must_use]
322pub fn known_drivers() -> Vec<String> {
323 Availability::embedded().drivers()
324}
325
326fn known_forge(name: &str) -> bool {
328 Forge::parse(name).is_some()
329}
330
331#[must_use]
338#[allow(
339 clippy::too_many_lines,
340 reason = "one pass answers every capability, and splitting it would separate a selection from the dimensions that decided it"
341)]
342pub fn select(params: &Params, availability: &Availability) -> Vec<Selection> {
343 let forge = params.forge();
344 let driver = params.driver();
345 let known_drivers = availability.drivers();
346 let driver_known = driver.is_some_and(|d| known_drivers.iter().any(|k| k == d));
347 let forge_known = forge.is_some_and(known_forge);
348
349 let mut out = Vec::with_capacity(ALL.len());
350 out.push(Selection::selected(GUARDS, Vec::new(), None));
351 out.push(if params.workflow_guards() {
352 Selection::selected(WORKFLOW_GUARDS, Vec::new(), None)
353 } else {
354 Selection::omitted(
355 WORKFLOW_GUARDS,
356 Status::NotRequested,
357 "capabilities.workflow_guards is false",
358 )
359 });
360
361 let automation = match (params.release_mode(), driver, forge) {
364 (ReleaseMode::External, _, _) => Selection::omitted(
365 RELEASE_AUTOMATION,
366 Status::NotRequested,
367 "the release is external: the target releases through a process release-kit does not drive",
368 ),
369 (ReleaseMode::None, _, _) => Selection::omitted(
370 RELEASE_AUTOMATION,
371 Status::NotRequested,
372 "the release mode is none",
373 ),
374 (ReleaseMode::Automatic, None, _) => Selection::omitted(
375 RELEASE_AUTOMATION,
376 Status::NotApplicable,
377 "an automatic release names no driver",
378 ),
379 (ReleaseMode::Automatic, _, None) => Selection::omitted(
380 RELEASE_AUTOMATION,
381 Status::NotApplicable,
382 "the profile names no forge",
383 ),
384 (ReleaseMode::Automatic, Some(driver), Some(forge)) => {
385 if !driver_known {
386 Selection::omitted(
387 RELEASE_AUTOMATION,
388 Status::Unknown,
389 format!(
390 "the driver {driver} is not one this release knows; the bindings are: {}",
391 known_drivers.join(", ")
392 ),
393 )
394 } else if !forge_known {
395 Selection::omitted(
396 RELEASE_AUTOMATION,
397 Status::Unknown,
398 format!(
399 "the forge {forge} is not one this release drives; the forges are: github, gitlab"
400 ),
401 )
402 } else {
403 let sources = availability.owned(driver, forge, RELEASE_AUTOMATION);
404 if sources.is_empty() {
405 Selection::omitted(
406 RELEASE_AUTOMATION,
407 Status::Unavailable,
408 format!(
409 "the release automation at ({driver}, {forge}) has no landable files; the available tuples are: {}",
410 availability.automation_tuples().join("; ")
411 ),
412 )
413 } else {
414 Selection::selected(RELEASE_AUTOMATION, sources, Some(driver))
415 }
416 }
417 }
418 };
419 let automation_lands = automation.status == Status::Selected;
420
421 let (gate, zone, other, why_not) = match params.merge_form() {
426 MergeForm::Squash => (
427 TITLE_CHECK,
428 TITLE_GATE_ZONE,
429 COMMIT_CHECK,
430 "git.merge_form is squash, so the title gate judges the request",
431 ),
432 MergeForm::Rebase => (
433 COMMIT_CHECK,
434 COMMIT_GATE_ZONE,
435 TITLE_CHECK,
436 "git.merge_form is rebase, so the commit gate judges every commit of the request",
437 ),
438 };
439 let no_gate = (!forge_gate_lands(params.integration(), params.release_mode())).then(|| {
442 format!(
443 "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",
444 params.release_mode().as_str()
445 )
446 });
447 let selected_gate = match forge {
448 None => Selection::omitted(gate, Status::NotApplicable, "the profile names no forge"),
449 Some(forge) if !forge_known => Selection::omitted(
450 gate,
451 Status::Unknown,
452 format!(
453 "the forge {forge} is not one this release drives; the forges are: github, gitlab"
454 ),
455 ),
456 Some(_) if no_gate.is_some() => Selection::omitted(
457 gate,
458 Status::NotApplicable,
459 no_gate.clone().unwrap_or_default(),
460 ),
461 Some(forge) => {
462 let mut sources = availability.owned("_shared", forge, gate);
463 if !automation_lands {
464 sources.extend(availability.owned(zone, forge, gate));
465 }
466 if sources.is_empty() {
467 Selection::omitted(
468 gate,
469 Status::Unavailable,
470 format!(
471 "this release ships no {} gate for {forge}",
472 if gate == TITLE_CHECK {
473 "title"
474 } else {
475 "commit"
476 }
477 ),
478 )
479 } else {
480 Selection::selected(gate, sources, None)
481 }
482 }
483 };
484 let other_gate = no_gate.map_or_else(
485 || Selection::omitted(other, Status::NotRequested, why_not),
486 |reason| Selection::omitted(other, Status::NotApplicable, reason),
487 );
488 if gate == TITLE_CHECK {
489 out.push(selected_gate);
490 out.push(other_gate);
491 } else {
492 out.push(other_gate);
493 out.push(selected_gate);
494 }
495
496 out.push(match forge {
498 _ if !params.reporting_policy() => Selection::omitted(
499 REPORTING_POLICY,
500 Status::NotRequested,
501 "capabilities.reporting_policy is false",
502 ),
503 None => Selection::omitted(
504 REPORTING_POLICY,
505 Status::NotApplicable,
506 "the profile names no forge, and the policy names the forge's private channel",
507 ),
508 Some(forge) if !forge_known => Selection::omitted(
509 REPORTING_POLICY,
510 Status::Unknown,
511 format!(
512 "the forge {forge} is not one this release drives; the forges are: github, gitlab"
513 ),
514 ),
515 Some(forge) => {
516 let sources = availability.owned("_shared", forge, REPORTING_POLICY);
517 if sources.is_empty() {
518 Selection::omitted(
519 REPORTING_POLICY,
520 Status::Unavailable,
521 format!("this release ships no reporting policy for {forge}"),
522 )
523 } else {
524 Selection::selected(REPORTING_POLICY, sources, None)
525 }
526 }
527 });
528
529 out.push(automation);
530
531 out.push(match (params.nix_packaging(), driver, forge) {
533 (false, _, _) => Selection::omitted(
534 PACKAGING_NIX,
535 Status::NotRequested,
536 "capabilities.nix_packaging is false",
537 ),
538 (true, None, _) => Selection::omitted(
539 PACKAGING_NIX,
540 Status::NotApplicable,
541 "the seed is keyed on an automatic release driver, and the profile names none",
542 ),
543 (true, _, None) => Selection::omitted(
544 PACKAGING_NIX,
545 Status::NotApplicable,
546 "the profile names no forge",
547 ),
548 (true, Some(driver), Some(forge)) => {
549 if !driver_known || !forge_known {
550 Selection::omitted(
551 PACKAGING_NIX,
552 Status::Unknown,
553 format!("({driver}, {forge}) names a category this release does not know"),
554 )
555 } else {
556 let sources = availability.owned(driver, forge, PACKAGING_NIX);
557 if sources.is_empty() {
558 Selection::omitted(
559 PACKAGING_NIX,
560 Status::Unavailable,
561 format!("the {driver} binding ships no Nix seed on {forge}"),
562 )
563 } else {
564 Selection::selected(PACKAGING_NIX, sources, Some(driver))
565 }
566 }
567 }
568 });
569
570 out.push(match forge {
572 _ if !params.scorecard() => Selection::omitted(
573 SCORECARD,
574 Status::NotRequested,
575 "capabilities.scorecard is false",
576 ),
577 None => Selection::omitted(
578 SCORECARD,
579 Status::NotApplicable,
580 "the profile names no forge",
581 ),
582 Some(forge) if !forge_known => Selection::omitted(
583 SCORECARD,
584 Status::Unknown,
585 format!(
586 "the forge {forge} is not one this release drives; the forges are: github, gitlab"
587 ),
588 ),
589 Some(forge) => {
590 let sources = availability.owned("_shared", forge, SCORECARD);
591 if sources.is_empty() {
592 Selection::omitted(
593 SCORECARD,
594 Status::Unavailable,
595 format!(
596 "the Scorecard workflow is GitHub's alone, and the {forge} zone ships none"
597 ),
598 )
599 } else {
600 Selection::selected(SCORECARD, sources, None)
601 }
602 }
603 });
604
605 out.push(match (params.code_scanning(), driver, forge) {
608 (None, _, _) => Selection::omitted(
609 CODE_SCANNING,
610 Status::NotRequested,
611 "capabilities.code_scanning is off",
612 ),
613 (Some(_), None, _) => Selection::omitted(
614 CODE_SCANNING,
615 Status::NotApplicable,
616 "a scanner reads the release driver's language, and the profile names no driver",
617 ),
618 (Some(_), _, None) => Selection::omitted(
619 CODE_SCANNING,
620 Status::NotApplicable,
621 "the profile names no forge",
622 ),
623 (Some(provider), Some(driver), Some(forge)) => {
624 if !driver_known || !forge_known {
625 Selection::omitted(
626 CODE_SCANNING,
627 Status::Unknown,
628 format!("({driver}, {forge}) names a category this release does not know"),
629 )
630 } else if let Some(reason) = crate::projection::code_scanning_incompatibility(
631 Some(provider),
632 Some(driver),
633 Some(forge),
634 ) {
635 Selection::omitted(CODE_SCANNING, Status::Unavailable, reason)
636 } else {
637 let sources: Vec<String> = availability
638 .owned(driver, forge, CODE_SCANNING)
639 .into_iter()
640 .filter(|path| {
641 CODE_SCANNING_DESTINATIONS
642 .iter()
643 .any(|(name, owner)| path.ends_with(name) && *owner == provider)
644 })
645 .collect();
646 if sources.is_empty() {
647 Selection::omitted(
648 CODE_SCANNING,
649 Status::Unavailable,
650 format!(
651 "the {driver} binding ships no {} workflow on {forge}",
652 provider.as_str()
653 ),
654 )
655 } else {
656 Selection::selected(CODE_SCANNING, sources, Some(driver))
657 .keyed_on(provider.as_str())
658 }
659 }
660 }
661 });
662 debug_assert_eq!(out.len(), ALL.len());
663 out
664}
665
666#[must_use]
677pub fn pin_keys(selection: &Selection) -> Vec<String> {
678 let mut keys = vec![selection.id.to_owned()];
679 if let Some(driver) = &selection.driver {
680 keys.push(format!("{}/{driver}", selection.id));
681 if let Some(provider) = &selection.provider {
682 keys.push(format!("{}/{driver}/{provider}", selection.id));
683 }
684 }
685 keys
686}
687
688#[cfg(test)]
689mod tests {
690 use super::{
691 ALL, Availability, CODE_SCANNING, COMMIT_CHECK, GUARDS, PACKAGING_NIX, RELEASE_AUTOMATION,
692 REPORTING_POLICY, SCORECARD, Status, TITLE_CHECK, WORKFLOW_GUARDS, owner_of, select,
693 };
694 use crate::landing::Params;
695 use crate::landing::manifest::{Provider, Style};
696 use crate::profile::ReleaseMode;
697
698 fn status_of(selections: &[super::Selection], id: &str) -> Status {
699 selections
700 .iter()
701 .find(|s| s.id == id)
702 .expect("every capability answers")
703 .status
704 }
705
706 #[test]
709 fn every_embedded_snippet_has_one_owner() {
710 let availability = Availability::embedded();
711 assert!(!availability.files().is_empty());
712 for path in availability.files() {
713 let owner = owner_of(path);
714 assert!(owner.is_some(), "{path}: no capability owns it");
715 assert!(
716 ALL.contains(&owner.unwrap_or_default()),
717 "{path}: an unlisted owner"
718 );
719 }
720 assert_eq!(
721 owner_of("_shared/github/SECURITY.md"),
722 Some(REPORTING_POLICY)
723 );
724 assert_eq!(
725 owner_of("_shared/github/.github/workflows/scorecard.yml"),
726 Some(SCORECARD)
727 );
728 assert_eq!(owner_of("rust/github/flake.nix"), Some(PACKAGING_NIX));
729 assert_eq!(
730 owner_of("rust/github/.github/workflows/code-scanning-codeql.yml"),
731 Some(CODE_SCANNING)
732 );
733 assert_eq!(
734 owner_of("rust/github/release-plz.toml"),
735 Some(RELEASE_AUTOMATION)
736 );
737 assert_eq!(owner_of("_shared/github/unknown.txt"), None);
738 }
739
740 #[test]
742 fn the_catalog_answers_availability_per_tuple() {
743 let availability = Availability::embedded();
744 let mut github = Params::for_test("acme/widget", Some(Style::Trunk));
745 github.set_pair_for_test("python", "github");
746 let mut gitlab = github.clone();
747 gitlab.set_pair_for_test("python", "gitlab");
748 let on_github = select(&github, &availability);
749 let on_gitlab = select(&gitlab, &availability);
750 assert_eq!(status_of(&on_github, RELEASE_AUTOMATION), Status::Selected);
751 assert_eq!(
752 status_of(&on_gitlab, RELEASE_AUTOMATION),
753 Status::Unavailable
754 );
755 let reason = on_gitlab
756 .iter()
757 .find(|s| s.id == RELEASE_AUTOMATION)
758 .and_then(|s| s.reason.clone())
759 .expect("an unavailable capability states why");
760 assert!(reason.contains("rust, gitlab"), "{reason}");
761 assert!(reason.contains("python, github"), "{reason}");
762 let title = on_gitlab
765 .iter()
766 .find(|s| s.id == TITLE_CHECK)
767 .expect("the title gate answers");
768 assert_eq!(title.status, Status::Selected);
769 assert!(
770 title
771 .sources
772 .iter()
773 .any(|s| s == "_title-gate/gitlab/.gitlab-ci.yml"),
774 "{:?}",
775 title.sources
776 );
777 }
778
779 #[test]
782 fn a_no_forge_input_selects_the_guards_alone() {
783 let params = Params::for_test_release_less(&[], None, ReleaseMode::None);
784 let selections = select(¶ms, &Availability::embedded());
785 assert_eq!(status_of(&selections, GUARDS), Status::Selected);
786 for id in [
787 TITLE_CHECK,
788 REPORTING_POLICY,
789 RELEASE_AUTOMATION,
790 PACKAGING_NIX,
791 ] {
792 assert_ne!(status_of(&selections, id), Status::Selected, "{id}");
793 }
794 assert_eq!(status_of(&selections, TITLE_CHECK), Status::NotApplicable);
795 assert_eq!(
796 status_of(&selections, RELEASE_AUTOMATION),
797 Status::NotRequested
798 );
799 assert_eq!(status_of(&selections, PACKAGING_NIX), Status::NotRequested);
800 }
801
802 #[test]
805 fn the_workflow_guards_are_selected_only_when_requested() {
806 let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
807 let selections = select(¶ms, &Availability::embedded());
808 assert_eq!(status_of(&selections, WORKFLOW_GUARDS), Status::Selected);
809 params.set_workflow_guards_for_test(false);
810 let selections = select(¶ms, &Availability::embedded());
811 assert_eq!(
812 status_of(&selections, WORKFLOW_GUARDS),
813 Status::NotRequested
814 );
815 assert_eq!(status_of(&selections, GUARDS), Status::Selected);
816 }
817
818 #[test]
826 fn workflow_only_local_landing_selects_no_forge_gate() {
827 use crate::landing::{Integration, MergeForm};
828 let availability = Availability::embedded();
829 for forge in ["github", "gitlab"] {
830 for form in [MergeForm::Squash, MergeForm::Rebase] {
831 for (mode, integration, lands) in [
832 (ReleaseMode::None, Integration::Local, false),
833 (ReleaseMode::External, Integration::Local, false),
834 (ReleaseMode::None, Integration::Forge, true),
835 (ReleaseMode::External, Integration::Forge, true),
836 ] {
837 let mut params = Params::for_test_release_less(&[], Some(forge), mode);
838 params.set_integration_for_test(integration);
839 params.set_merge_form_for_test(form);
840 let selections = select(¶ms, &availability);
841 let (gate, other) = match form {
842 MergeForm::Squash => (TITLE_CHECK, COMMIT_CHECK),
843 MergeForm::Rebase => (COMMIT_CHECK, TITLE_CHECK),
844 };
845 let at = format!("{forge} {form:?} {mode:?} {integration:?}");
846 let selection = selections
847 .iter()
848 .find(|s| s.id == gate)
849 .expect("every capability answers");
850 assert_eq!(selection.lands(), lands, "{at}");
851 if !lands {
852 assert_eq!(selection.status, Status::NotApplicable, "{at}");
853 assert!(selection.sources.is_empty(), "{at}");
854 let reason = selection.reason.as_deref().unwrap_or_default();
855 assert!(reason.contains("no forge gate lands"), "{at}: {reason}");
856 assert!(
857 reason.contains("git.integration is local"),
858 "{at}: {reason}"
859 );
860 }
861 let unnamed = selections
864 .iter()
865 .find(|s| s.id == other)
866 .expect("every capability answers");
867 let reason = unnamed.reason.as_deref().unwrap_or_default();
868 if lands {
869 assert_eq!(unnamed.status, Status::NotRequested, "{at}");
870 assert!(reason.contains("git.merge_form is"), "{at}: {reason}");
871 } else {
872 assert_eq!(unnamed.status, Status::NotApplicable, "{at}");
873 assert!(reason.contains("no forge gate lands"), "{at}: {reason}");
874 assert!(!reason.contains("merge_form"), "{at}: {reason}");
875 }
876 }
877 }
878 let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
881 params.set_pair_for_test("rust", forge);
882 params.set_integration_for_test(Integration::Local);
883 let selections = select(¶ms, &availability);
884 assert_eq!(
885 status_of(&selections, TITLE_CHECK),
886 Status::Selected,
887 "{forge}"
888 );
889 }
890 }
891
892 #[test]
894 fn an_unknown_forge_reads_as_unknown_and_lands_the_guards() {
895 let mut params = Params::for_test_release_less(&[], Some("codeberg"), ReleaseMode::None);
896 params.set_scorecard_for_test(true);
897 let selections = select(¶ms, &Availability::embedded());
898 assert_eq!(status_of(&selections, GUARDS), Status::Selected);
899 assert_eq!(status_of(&selections, TITLE_CHECK), Status::Unknown);
900 assert_eq!(status_of(&selections, SCORECARD), Status::Unknown);
901 }
902
903 #[test]
907 fn code_scanning_selects_the_providers_own_file() {
908 let availability = Availability::embedded();
909 let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
910 params.set_code_scanning_for_test(Some(Provider::Semgrep));
911 let selections = select(¶ms, &availability);
912 let scanning = selections
913 .iter()
914 .find(|s| s.id == CODE_SCANNING)
915 .expect("answers");
916 assert_eq!(scanning.status, Status::Selected);
917 assert_eq!(
918 scanning.sources,
919 vec!["rust/github/.github/workflows/code-scanning-semgrep.yml".to_owned()]
920 );
921 params.set_pair_for_test("bash", "github");
922 let selections = select(¶ms, &availability);
923 assert_eq!(status_of(&selections, CODE_SCANNING), Status::Unavailable);
924 }
925
926 #[test]
933 fn commit_gate_replaces_the_title_gate_under_rebase() {
934 use crate::landing::{Integration, MergeForm};
935 let availability = Availability::embedded();
936 for integration in [Integration::Forge, Integration::Local] {
937 for (driver, forge) in [("rust", "github"), ("rust", "gitlab"), ("python", "gitlab")] {
938 let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
939 params.set_pair_for_test(driver, forge);
940 params.set_integration_for_test(integration);
941 let squash = select(¶ms, &availability);
942 assert_eq!(status_of(&squash, TITLE_CHECK), Status::Selected);
943 assert_eq!(status_of(&squash, COMMIT_CHECK), Status::NotRequested);
944
945 params.set_merge_form_for_test(MergeForm::Rebase);
946 let rebase = select(¶ms, &availability);
947 assert_eq!(status_of(&rebase, TITLE_CHECK), Status::NotRequested);
948 let gate = rebase
949 .iter()
950 .find(|s| s.id == COMMIT_CHECK)
951 .expect("the commit gate answers");
952 assert_eq!(gate.status, Status::Selected, "{driver} {forge}");
953 assert!(
954 gate.sources.iter().all(|source| !source.contains("title")),
955 "{:?}",
956 gate.sources
957 );
958 let fragment = if forge == "github" {
959 "_shared/github/.github/workflows/pr-commits.yml"
960 } else {
961 "_shared/gitlab/.gitlab/ci/mr-commits.yml"
962 };
963 assert!(
964 gate.sources.iter().any(|s| s == fragment),
965 "{:?}",
966 gate.sources
967 );
968 assert_eq!(
971 gate.sources
972 .iter()
973 .any(|s| s == "_commit-gate/gitlab/.gitlab-ci.yml"),
974 driver == "python",
975 "{:?}",
976 gate.sources
977 );
978 }
979 }
980 assert_eq!(
981 owner_of("_shared/github/.github/workflows/pr-commits.yml"),
982 Some(COMMIT_CHECK)
983 );
984 assert_eq!(
985 owner_of("_commit-gate/gitlab/.gitlab-ci.yml"),
986 Some(COMMIT_CHECK)
987 );
988 assert_eq!(ALL.len(), 9);
989 }
990}