ephemeris_forge_github/config/
rulesets.rs1mod rules;
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use ephemeris_forge::{Mode, non_empty_name};
7use facet::Facet;
8
9pub use self::rules::{
10 AlertsThreshold, CodeScanningRule, CodeScanningTool, DeploymentsRule, DismissalActor,
11 DismissalActorType, DismissalRestriction, GroupingStrategy, MergeMethod, MergeQueueRule,
12 PatternOperator, PatternRule, PullRequestRule, QueueMergeMethod, RequiredCheck, Rules,
13 SecurityAlertsThreshold, StatusChecksRule, UpdateRule, WorkflowRef, WorkflowsRule,
14};
15pub(crate) use self::rules::{RuleDecoder, RuleRef};
16use super::ConfigError;
17
18non_empty_name! {
19 pub struct RulesetName;
21 pub error EmptyRulesetName("ruleset name cannot be empty");
23}
24
25const ORGANIZATION_ADMIN_ID: i64 = 1;
27
28#[derive(Clone, Debug, Eq, Facet, PartialEq)]
33#[facet(proxy = String)]
34#[repr(u8)]
35pub enum Target {
36 Branch,
38 Tag,
40 Unknown(String),
42}
43
44#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
46#[error("unknown ruleset target {name:?}")]
47pub struct UnknownTarget {
48 pub name: String,
50}
51
52impl Target {
53 #[must_use]
56 pub fn observed(raw: &str) -> Self {
57 Self::try_from(raw.to_owned()).unwrap_or_else(|UnknownTarget { name }| Self::Unknown(name))
58 }
59}
60
61impl TryFrom<String> for Target {
62 type Error = UnknownTarget;
63
64 fn try_from(name: String) -> Result<Self, Self::Error> {
65 match name.as_str() {
66 "branch" => Ok(Self::Branch),
67 "tag" => Ok(Self::Tag),
68 _ => Err(UnknownTarget { name }),
69 }
70 }
71}
72
73impl From<&Target> for String {
74 fn from(value: &Target) -> Self {
75 match value {
76 Target::Branch => "branch".to_owned(),
77 Target::Tag => "tag".to_owned(),
78 Target::Unknown(name) => name.clone(),
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
85#[repr(u8)]
86pub enum Enforcement {
87 #[facet(rename = "active")]
89 Active,
90 #[facet(rename = "evaluate")]
92 Evaluate,
93 #[facet(rename = "disabled")]
95 Disabled,
96}
97
98#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
100#[repr(u8)]
101pub enum ActorType {
102 #[facet(rename = "Integration")]
104 Integration,
105 #[facet(rename = "OrganizationAdmin")]
107 OrganizationAdmin,
108 #[facet(rename = "RepositoryRole")]
110 RepositoryRole,
111 #[facet(rename = "Team")]
113 Team,
114 #[facet(rename = "User")]
116 User,
117 #[facet(rename = "DeployKey")]
119 DeployKey,
120}
121
122impl ActorType {
123 #[must_use]
125 pub const fn as_str(self) -> &'static str {
126 match self {
127 Self::Integration => "Integration",
128 Self::OrganizationAdmin => "OrganizationAdmin",
129 Self::RepositoryRole => "RepositoryRole",
130 Self::Team => "Team",
131 Self::User => "User",
132 Self::DeployKey => "DeployKey",
133 }
134 }
135
136 #[must_use]
138 pub const fn needs_id(self) -> bool {
139 matches!(self, Self::RepositoryRole | Self::Team | Self::User)
140 }
141
142 #[must_use]
144 pub const fn forbids_id(self) -> bool {
145 matches!(self, Self::DeployKey)
146 }
147}
148
149impl fmt::Display for ActorType {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str(self.as_str())
152 }
153}
154
155#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
157#[repr(u8)]
158pub enum BypassMode {
159 #[facet(rename = "always")]
161 Always,
162 #[facet(rename = "pull_request")]
164 PullRequest,
165 #[facet(rename = "exempt")]
167 Exempt,
168}
169
170#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
172#[facet(deny_unknown_fields)]
173pub struct BypassActor {
174 pub actor_type: ActorType,
176 pub actor_id: Option<i64>,
179 pub bypass_mode: BypassMode,
181}
182
183impl BypassActor {
184 #[must_use]
190 pub const fn wire_id(&self) -> Option<i64> {
191 match (self.actor_type, self.actor_id) {
192 (ActorType::OrganizationAdmin, None) => Some(ORGANIZATION_ADMIN_ID),
193 (_, id) => id,
194 }
195 }
196
197 const fn is_own_app(&self) -> bool {
198 matches!(self.actor_type, ActorType::Integration) && self.actor_id.is_none()
199 }
200
201 const fn normalized(mut self) -> Self {
202 if matches!(self.actor_type, ActorType::OrganizationAdmin) {
203 self.actor_id = None;
204 }
205 self
206 }
207}
208
209#[derive(Clone, Debug, Eq, Facet, PartialEq)]
211#[facet(deny_unknown_fields)]
212pub struct Ruleset {
213 pub target: Target,
215 pub enforcement: Enforcement,
217 pub include: Vec<String>,
219 pub exclude: Vec<String>,
221 pub bypass_actors: Vec<BypassActor>,
223 pub rules: Rules,
225}
226
227impl Ruleset {
228 pub(crate) fn bypassed_by_own_app(&self) -> bool {
230 self.bypass_actors.iter().any(BypassActor::is_own_app)
231 }
232
233 #[must_use]
235 pub(crate) fn with_own_app(mut self, app_id: i64) -> Self {
236 for actor in self
237 .bypass_actors
238 .iter_mut()
239 .filter(|actor| actor.is_own_app())
240 {
241 actor.actor_id = Some(app_id);
242 }
243 self
244 }
245
246 #[must_use]
248 pub fn normalized(mut self) -> Self {
249 self.include.sort_unstable();
250 self.include.dedup();
251 self.exclude.sort_unstable();
252 self.exclude.dedup();
253 self.bypass_actors = self
254 .bypass_actors
255 .into_iter()
256 .map(BypassActor::normalized)
257 .collect();
258 self.bypass_actors.sort_unstable();
259 self.bypass_actors.dedup();
260 self.rules.normalize();
261 self
262 }
263}
264
265#[derive(Clone, Debug, Eq, Facet, PartialEq)]
267#[facet(deny_unknown_fields)]
268pub struct Rulesets {
269 pub mode: Mode,
271 pub rulesets: BTreeMap<RulesetName, Ruleset>,
273}
274
275impl Ruleset {
276 fn validate(&self, name: &RulesetName) -> Result<(), ConfigError> {
277 if let Some(actor) = self
278 .bypass_actors
279 .iter()
280 .find(|actor| actor.actor_type.needs_id() && actor.actor_id.is_none())
281 {
282 return Err(ConfigError::MissingActorId {
283 ruleset: name.clone(),
284 actor_type: actor.actor_type,
285 });
286 }
287 if let Some(actor) = self
288 .bypass_actors
289 .iter()
290 .find(|actor| actor.actor_type.forbids_id() && actor.actor_id.is_some())
291 {
292 return Err(ConfigError::UnexpectedActorId {
293 ruleset: name.clone(),
294 actor_type: actor.actor_type,
295 });
296 }
297 if self
298 .rules
299 .pull_request
300 .as_ref()
301 .is_some_and(|rule| rule.allowed_merge_methods.is_empty())
302 {
303 return Err(ConfigError::NoMergeMethods {
304 ruleset: name.clone(),
305 });
306 }
307 Ok(())
308 }
309}
310
311impl Rulesets {
312 pub fn validate(&self) -> Result<(), ConfigError> {
317 self.rulesets
318 .iter()
319 .try_for_each(|(name, ruleset)| ruleset.validate(name))
320 }
321}