Skip to main content

ephemeris_forge_github/config/
environments.rs

1use std::collections::BTreeMap;
2
3use ephemeris_forge::{Mode, non_empty_name};
4use facet::Facet;
5
6use super::ConfigError;
7
8const MAX_WAIT_TIMER: u32 = 43_200;
9const MAX_REVIEWERS: usize = 6;
10
11non_empty_name! {
12    /// The name of a deployment environment, which is never empty.
13    pub struct EnvironmentName;
14    /// An environment name was the empty string.
15    pub error EmptyEnvironmentName("environment name cannot be empty");
16}
17
18/// The kind of account that reviews deployments.
19#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
20#[repr(u8)]
21pub enum EnvironmentReviewerType {
22    /// A user.
23    #[facet(rename = "User")]
24    User,
25    /// An organization team.
26    #[facet(rename = "Team")]
27    Team,
28}
29
30/// A user or team that must approve deployments.
31#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
32#[facet(deny_unknown_fields)]
33pub struct EnvironmentReviewer {
34    /// Whether the id names a user or a team.
35    #[facet(rename = "type")]
36    pub kind: EnvironmentReviewerType,
37    /// The user or team id.
38    pub id: u64,
39}
40
41/// Which branches may deploy to an environment.
42#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
43#[facet(deny_unknown_fields)]
44pub struct DeploymentBranchPolicy {
45    /// Only branches with protection rules may deploy.
46    pub protected_branches: bool,
47    /// Only branches and tags that match a branch policy may deploy.
48    pub custom_branch_policies: bool,
49}
50
51/// Whether a branch policy matches branches or tags.
52#[derive(Clone, Copy, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
53#[repr(u8)]
54pub enum BranchPolicyType {
55    /// The pattern matches branch names.
56    #[facet(rename = "branch")]
57    Branch,
58    /// The pattern matches tag names.
59    #[facet(rename = "tag")]
60    Tag,
61}
62
63/// A name pattern that may deploy to an environment.
64#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
65#[facet(deny_unknown_fields)]
66pub struct BranchPolicy {
67    /// The name pattern. A `*` does not match a `/`.
68    pub name: String,
69    /// Whether the pattern matches branches or tags.
70    #[facet(rename = "type")]
71    pub kind: BranchPolicyType,
72}
73
74/// A deployment environment.
75#[derive(Clone, Debug, Eq, Facet, PartialEq)]
76#[facet(deny_unknown_fields)]
77pub struct Environment {
78    /// Minutes to wait before a deployment starts.
79    pub wait_timer: u32,
80    /// Whether the person who started a deployment cannot approve it.
81    pub prevent_self_review: bool,
82    /// The users and teams that approve deployments. Compared as a set.
83    pub reviewers: Vec<EnvironmentReviewer>,
84    /// Which branches may deploy. Null allows every branch.
85    pub deployment_branch_policy: Option<DeploymentBranchPolicy>,
86    /// The name patterns that may deploy. Compared as a set.
87    pub branch_policies: Vec<BranchPolicy>,
88}
89
90/// Desired deployment environments of a repository.
91#[derive(Clone, Debug, Eq, Facet, PartialEq)]
92#[facet(deny_unknown_fields)]
93pub struct Environments {
94    /// Whether environments that are not declared are kept or deleted.
95    pub mode: Mode,
96    /// The environments keyed by name.
97    pub environments: BTreeMap<EnvironmentName, Environment>,
98}
99
100impl Environment {
101    fn validate(&self, name: &EnvironmentName) -> Result<(), ConfigError> {
102        if self.wait_timer > MAX_WAIT_TIMER {
103            return Err(ConfigError::EnvironmentWaitTimer {
104                environment: name.clone(),
105                minutes: self.wait_timer,
106            });
107        }
108        if self.reviewers.len() > MAX_REVIEWERS {
109            return Err(ConfigError::TooManyEnvironmentReviewers {
110                environment: name.clone(),
111                count: self.reviewers.len(),
112            });
113        }
114        let policy = self.deployment_branch_policy;
115        if policy.is_some_and(|policy| policy.protected_branches && policy.custom_branch_policies) {
116            return Err(ConfigError::ConflictingDeploymentBranchPolicy {
117                environment: name.clone(),
118            });
119        }
120        if !self.branch_policies.is_empty()
121            && !policy.is_some_and(|policy| policy.custom_branch_policies)
122        {
123            return Err(ConfigError::BranchPoliciesWithoutCustom {
124                environment: name.clone(),
125            });
126        }
127        Ok(())
128    }
129}
130
131impl Environments {
132    /// Check what decoding cannot.
133    ///
134    /// # Errors
135    /// Returns an error when an environment has a value GitHub refuses.
136    pub fn validate(&self) -> Result<(), ConfigError> {
137        self.environments
138            .iter()
139            .try_for_each(|(name, environment)| environment.validate(name))
140    }
141}