Skip to main content

ephemeris_forge_github/config/rulesets/
rules.rs

1mod list;
2
3use facet::Facet;
4
5pub use self::list::Rules;
6pub(crate) use self::list::{RuleDecoder, RuleRef};
7
8/// A way to merge a pull request.
9#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
10#[repr(u8)]
11pub enum MergeMethod {
12    /// A merge commit.
13    #[facet(rename = "merge")]
14    Merge,
15    /// Squash into one commit.
16    #[facet(rename = "squash")]
17    Squash,
18    /// Rebase the commits.
19    #[facet(rename = "rebase")]
20    Rebase,
21}
22
23impl MergeMethod {
24    /// Every merge method.
25    pub const ALL: [Self; 3] = [Self::Merge, Self::Squash, Self::Rebase];
26}
27
28/// A required status check.
29#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
30#[facet(deny_unknown_fields)]
31pub struct RequiredCheck {
32    /// The status check's context name.
33    pub context: String,
34    /// The id of the app the check must come from, when restricted.
35    #[facet(skip_serializing_if = Option::is_none)]
36    pub integration_id: Option<i64>,
37}
38
39/// The kind of actor that may dismiss pull request reviews.
40#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
41#[repr(u8)]
42pub enum DismissalActorType {
43    /// A user.
44    #[facet(rename = "User")]
45    User,
46    /// A team.
47    #[facet(rename = "Team")]
48    Team,
49    /// An installed GitHub App.
50    #[facet(rename = "IntegrationInstallation")]
51    IntegrationInstallation,
52    /// A repository role.
53    #[facet(rename = "RepositoryRole")]
54    RepositoryRole,
55}
56
57/// An actor that may dismiss pull request reviews.
58#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
59#[facet(deny_unknown_fields)]
60pub struct DismissalActor {
61    /// The actor's id.
62    pub id: i64,
63    /// The kind of actor.
64    #[facet(rename = "type")]
65    pub kind: DismissalActorType,
66}
67
68/// Who may dismiss pull request reviews.
69#[derive(Clone, Debug, Eq, Facet, PartialEq)]
70#[facet(deny_unknown_fields)]
71pub struct DismissalRestriction {
72    /// Whether only the allowed actors may dismiss reviews.
73    pub enabled: bool,
74    /// The actors that may dismiss reviews.
75    pub allowed_actors: Vec<DismissalActor>,
76}
77
78/// The pull request rule of a ruleset.
79#[derive(Clone, Debug, Eq, Facet, PartialEq)]
80#[facet(deny_unknown_fields)]
81#[expect(
82    clippy::struct_excessive_bools,
83    reason = "these mirror independent GitHub pull request rule parameters"
84)]
85pub struct PullRequestRule {
86    /// Whether a new push dismisses existing approvals.
87    pub dismiss_stale_reviews_on_push: bool,
88    /// Whether a code owner must approve.
89    pub require_code_owner_review: bool,
90    /// Whether someone other than the last pusher must approve.
91    pub require_last_push_approval: bool,
92    /// The number of approving reviews required before merging.
93    pub required_approving_review_count: u8,
94    /// Whether all review threads must be resolved.
95    pub required_review_thread_resolution: bool,
96    /// The merge methods the pull request may use.
97    pub allowed_merge_methods: Vec<MergeMethod>,
98    /// Who may dismiss reviews, when restricted.
99    #[facet(skip_serializing_if = Option::is_none)]
100    pub dismissal_restriction: Option<DismissalRestriction>,
101}
102
103/// The required status checks rule of a ruleset.
104#[derive(Clone, Debug, Eq, Facet, PartialEq)]
105#[facet(deny_unknown_fields)]
106pub struct StatusChecksRule {
107    /// The checks that must pass.
108    pub required_status_checks: Vec<RequiredCheck>,
109    /// Whether the branch must be up to date before the checks can pass.
110    pub strict_required_status_checks_policy: bool,
111    /// Whether the checks are skipped when the ref is created.
112    pub do_not_enforce_on_create: bool,
113}
114
115/// The update rule of a ruleset.
116#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
117#[facet(deny_unknown_fields)]
118pub struct UpdateRule {
119    /// Whether users with write access may fetch and merge into the ref.
120    pub update_allows_fetch_and_merge: bool,
121}
122
123/// The required deployments rule of a ruleset.
124#[derive(Clone, Debug, Eq, Facet, PartialEq)]
125#[facet(deny_unknown_fields)]
126pub struct DeploymentsRule {
127    /// The environments that must have a successful deployment.
128    pub required_deployment_environments: Vec<String>,
129}
130
131/// How a pattern rule matches its text.
132#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
133#[repr(u8)]
134pub enum PatternOperator {
135    /// The text starts with the pattern.
136    #[facet(rename = "starts_with")]
137    StartsWith,
138    /// The text ends with the pattern.
139    #[facet(rename = "ends_with")]
140    EndsWith,
141    /// The text contains the pattern.
142    #[facet(rename = "contains")]
143    Contains,
144    /// The text matches the regular expression.
145    #[facet(rename = "regex")]
146    Regex,
147}
148
149/// A rule that requires commit or ref metadata to match a pattern.
150#[derive(Clone, Debug, Eq, Facet, PartialEq)]
151#[facet(deny_unknown_fields)]
152pub struct PatternRule {
153    /// Whether the text must not match.
154    pub negate: bool,
155    /// How the pattern is matched.
156    pub operator: PatternOperator,
157    /// The text or regular expression to match.
158    pub pattern: String,
159    /// A label for the rule.
160    #[facet(skip_serializing_if = Option::is_none)]
161    pub name: Option<String>,
162}
163
164/// A workflow that must pass.
165#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
166#[facet(deny_unknown_fields)]
167pub struct WorkflowRef {
168    /// The workflow file path.
169    pub path: String,
170    /// The ref of the workflow file.
171    #[facet(rename = "ref", skip_serializing_if = Option::is_none)]
172    pub reference: Option<String>,
173    /// The id of the repository that holds the workflow.
174    pub repository_id: i64,
175    /// The commit of the workflow file.
176    #[facet(skip_serializing_if = Option::is_none)]
177    pub sha: Option<String>,
178}
179
180/// The required workflows rule of a ruleset.
181#[derive(Clone, Debug, Eq, Facet, PartialEq)]
182#[facet(deny_unknown_fields)]
183pub struct WorkflowsRule {
184    /// The workflows that must pass.
185    pub workflows: Vec<WorkflowRef>,
186    /// Whether the workflows are skipped when the ref is created.
187    pub do_not_enforce_on_create: bool,
188}
189
190/// The alert severity at which code scanning blocks a change.
191#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
192#[repr(u8)]
193pub enum AlertsThreshold {
194    /// No alert blocks.
195    #[facet(rename = "none")]
196    None,
197    /// Errors block.
198    #[facet(rename = "errors")]
199    Errors,
200    /// Errors and warnings block.
201    #[facet(rename = "errors_and_warnings")]
202    ErrorsAndWarnings,
203    /// Every alert blocks.
204    #[facet(rename = "all")]
205    All,
206}
207
208/// The security alert severity at which code scanning blocks a change.
209#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
210#[repr(u8)]
211pub enum SecurityAlertsThreshold {
212    /// No security alert blocks.
213    #[facet(rename = "none")]
214    None,
215    /// Critical alerts block.
216    #[facet(rename = "critical")]
217    Critical,
218    /// High and critical alerts block.
219    #[facet(rename = "high_or_higher")]
220    HighOrHigher,
221    /// Medium, high and critical alerts block.
222    #[facet(rename = "medium_or_higher")]
223    MediumOrHigher,
224    /// Every security alert blocks.
225    #[facet(rename = "all")]
226    All,
227}
228
229/// A code scanning tool whose results must pass.
230#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
231#[facet(deny_unknown_fields)]
232pub struct CodeScanningTool {
233    /// The tool's name.
234    pub tool: String,
235    /// The alert severity that blocks the change.
236    pub alerts_threshold: AlertsThreshold,
237    /// The security alert severity that blocks the change.
238    pub security_alerts_threshold: SecurityAlertsThreshold,
239}
240
241/// The code scanning rule of a ruleset.
242#[derive(Clone, Debug, Eq, Facet, PartialEq)]
243#[facet(deny_unknown_fields)]
244pub struct CodeScanningRule {
245    /// The tools whose results must pass.
246    pub code_scanning_tools: Vec<CodeScanningTool>,
247}
248
249/// Which entries must pass their checks in a merge queue.
250#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
251#[repr(u8)]
252pub enum GroupingStrategy {
253    /// Every entry in the group must pass.
254    #[facet(rename = "ALLGREEN")]
255    AllGreen,
256    /// Only the head entry must pass.
257    #[facet(rename = "HEADGREEN")]
258    HeadGreen,
259}
260
261/// How a merge queue merges a group.
262#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
263#[repr(u8)]
264pub enum QueueMergeMethod {
265    /// A merge commit.
266    #[facet(rename = "MERGE")]
267    Merge,
268    /// Squash into one commit.
269    #[facet(rename = "SQUASH")]
270    Squash,
271    /// Rebase the commits.
272    #[facet(rename = "REBASE")]
273    Rebase,
274}
275
276/// The merge queue rule of a ruleset.
277#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
278#[facet(deny_unknown_fields)]
279pub struct MergeQueueRule {
280    /// Minutes to wait for a required check before failing the entry.
281    pub check_response_timeout_minutes: u32,
282    /// Which entries must pass their checks.
283    pub grouping_strategy: GroupingStrategy,
284    /// The most entries that build checks at once.
285    pub max_entries_to_build: u32,
286    /// The most entries that merge in one group.
287    pub max_entries_to_merge: u32,
288    /// How the queue merges a group.
289    pub merge_method: QueueMergeMethod,
290    /// The fewest entries that merge in one group.
291    pub min_entries_to_merge: u32,
292    /// Minutes to wait for the minimum group size.
293    pub min_entries_to_merge_wait_minutes: u32,
294}
295
296impl Rules {
297    /// Put every list in its comparison order and drop a dismissal
298    /// restriction that is off and names no actors.
299    pub(super) fn normalize(&mut self) {
300        if let Some(pull_request) = &mut self.pull_request {
301            pull_request.allowed_merge_methods.sort_unstable();
302            pull_request.allowed_merge_methods.dedup();
303            if let Some(restriction) = &mut pull_request.dismissal_restriction {
304                restriction.allowed_actors.sort_unstable();
305                restriction.allowed_actors.dedup();
306            }
307            if pull_request
308                .dismissal_restriction
309                .as_ref()
310                .is_some_and(|restriction| {
311                    !restriction.enabled && restriction.allowed_actors.is_empty()
312                })
313            {
314                pull_request.dismissal_restriction = None;
315            }
316        }
317        if let Some(checks) = &mut self.required_status_checks {
318            checks.required_status_checks.sort_unstable();
319            checks.required_status_checks.dedup();
320        }
321        if let Some(deployments) = &mut self.required_deployments {
322            deployments.required_deployment_environments.sort_unstable();
323            deployments.required_deployment_environments.dedup();
324        }
325        if let Some(workflows) = &mut self.workflows {
326            workflows.workflows.sort_unstable();
327            workflows.workflows.dedup();
328        }
329        if let Some(scanning) = &mut self.code_scanning {
330            scanning.code_scanning_tools.sort_unstable();
331            scanning.code_scanning_tools.dedup();
332        }
333    }
334}