1use super::{Config, Protection, invalid};
4use crate::error::RkError;
5
6pub struct Floor {
8 pub key: &'static str,
10 pub minimum: &'static str,
12 pub heading: &'static str,
14 accepts: fn(&Protection) -> bool,
15}
16
17pub const FLOORS: &[Floor] = &[
19 Floor {
20 key: "protection.tag_pattern",
21 minimum: "refs/tags/v* or refs/tags/*, covering every published version",
22 heading: "A published version is immutable",
23 accepts: |p| matches!(p.tag_pattern.as_str(), "refs/tags/v*" | "refs/tags/*"),
24 },
25 Floor {
26 key: "protection.bypass_actors",
27 minimum: "empty",
28 heading: "Trunk is written through pull requests only",
29 accepts: |p| p.bypass_actors.is_empty(),
30 },
31 Floor {
32 key: "protection.allowed_merge_methods",
33 minimum: "exactly [squash]",
34 heading: "Trunk is written through pull requests only",
35 accepts: |p| p.allowed_merge_methods == ["squash"],
36 },
37 Floor {
38 key: "protection.strict_required_status_checks",
39 minimum: "true",
40 heading: "Trunk is written through pull requests only",
41 accepts: |p| p.strict_required_status_checks,
42 },
43 Floor {
44 key: "protection.owned_trunk_rules",
45 minimum: "contains deletion, non_fast_forward, pull_request, required_status_checks",
46 heading: "Trunk is written through pull requests only",
47 accepts: |p| {
48 [
49 "deletion",
50 "non_fast_forward",
51 "pull_request",
52 "required_status_checks",
53 ]
54 .iter()
55 .all(|rule| p.owned_trunk_rules.iter().any(|owned| owned == rule))
56 },
57 },
58 Floor {
59 key: "protection.required_approving_review_count",
60 minimum: "at least 0",
61 heading: "Trunk is written through pull requests only",
62 accepts: |p| p.required_approving_review_count >= 0,
63 },
64 Floor {
65 key: "protection.dismiss_stale_reviews_on_push",
66 minimum: "false; true is stricter",
67 heading: "Trunk is written through pull requests only",
68 accepts: |p| {
69 let _ = p.dismiss_stale_reviews_on_push;
70 true
71 },
72 },
73 Floor {
74 key: "protection.require_code_owner_review",
75 minimum: "false; true is stricter",
76 heading: "Trunk is written through pull requests only",
77 accepts: |p| {
78 let _ = p.require_code_owner_review;
79 true
80 },
81 },
82 Floor {
83 key: "protection.require_last_push_approval",
84 minimum: "false; true is stricter",
85 heading: "Trunk is written through pull requests only",
86 accepts: |p| {
87 let _ = p.require_last_push_approval;
88 true
89 },
90 },
91 Floor {
92 key: "protection.github.squash_title_source",
93 minimum: "PR_TITLE",
94 heading: "Trunk is written through pull requests only",
95 accepts: |p| p.github.squash_title_source == "PR_TITLE",
96 },
97 Floor {
98 key: "protection.github.squash_body_source",
99 minimum: "PR_BODY",
100 heading: "Trunk is written through pull requests only",
101 accepts: |p| p.github.squash_body_source == "PR_BODY",
102 },
103 Floor {
104 key: "protection.gitlab.merge_method",
105 minimum: "ff",
106 heading: "Trunk is written through pull requests only",
107 accepts: |p| p.gitlab.merge_method == "ff",
108 },
109 Floor {
110 key: "protection.gitlab.squash_option",
111 minimum: "always",
112 heading: "Trunk is written through pull requests only",
113 accepts: |p| p.gitlab.squash_option == "always",
114 },
115 Floor {
116 key: "protection.gitlab.squash_commit_template",
117 minimum: "references %{title}",
118 heading: "Trunk is written through pull requests only",
119 accepts: |p| p.gitlab.squash_commit_template.contains("%{title}"),
124 },
125 Floor {
126 key: "protection.gitlab.push_access_level",
127 minimum: "0 (no direct pushes)",
128 heading: "Trunk is written through pull requests only",
129 accepts: |p| p.gitlab.push_access_level == 0,
130 },
131 Floor {
132 key: "protection.gitlab.merge_access_level",
133 minimum: "at least 30",
134 heading: "Trunk is written through pull requests only",
135 accepts: |p| p.gitlab.merge_access_level >= 30,
136 },
137];
138
139pub fn check(config: &Config) -> Result<(), RkError> {
144 for floor in FLOORS {
145 if !(floor.accepts)(&config.protection) {
146 return Err(invalid(format!(
147 "{}: floor is {}; see rk method invariants ({})",
148 floor.key, floor.minimum, floor.heading
149 )));
150 }
151 }
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::{FLOORS, check};
158 use crate::config::Config;
159
160 #[test]
161 fn every_floor_names_a_real_invariant_heading() {
162 let chapter = crate::embedded::METHOD
163 .get_file("01-invariants.md")
164 .expect("the invariants ship")
165 .contents_utf8()
166 .expect("prose is utf8");
167 for floor in FLOORS {
168 assert!(
169 chapter
170 .lines()
171 .any(|line| line.strip_prefix("## ") == Some(floor.heading)),
172 "{}: {}",
173 floor.key,
174 floor.heading
175 );
176 }
177 check(&Config::default()).expect("defaults meet every floor");
178 }
179
180 #[test]
181 fn a_value_below_a_floor_refuses_naming_the_invariants() {
182 let mut config = Config::default();
183 config.protection.allowed_merge_methods.push("merge".into());
184 let error = check(&config)
185 .expect_err("merge violates squash only")
186 .to_string();
187 for expected in [
188 "protection.allowed_merge_methods",
189 "floor",
190 "squash",
191 "rk method invariants",
192 ] {
193 assert!(error.contains(expected), "{error}");
194 }
195 }
196
197 #[test]
198 fn a_stricter_value_passes_the_floor() {
199 let mut config = Config::default();
200 config.protection.required_approving_review_count = 3;
201 config.protection.dismiss_stale_reviews_on_push = true;
202 config.protection.require_code_owner_review = true;
203 config.protection.require_last_push_approval = true;
204 config.protection.gitlab.merge_access_level = 40;
205 config.protection.tag_pattern = "refs/tags/*".into();
206 config
207 .protection
208 .owned_trunk_rules
209 .push("required_signatures".into());
210 check(&config).expect("a target can be stricter");
211 }
212}