Skip to main content

autoschematic_connector_github/
resource.rs

1use std::collections::HashMap;
2
3use autoschematic_core::{
4    connector::{Resource, ResourceAddress},
5    error_util::invalid_addr,
6    macros::FieldTypes,
7    util::RON,
8};
9use autoschematic_macros::FieldTypes;
10use documented::{Documented, DocumentedFields};
11use serde::{Deserialize, Serialize};
12
13use super::addr::GitHubResourceAddress;
14
15#[derive(Debug, Serialize, Deserialize, PartialEq, Documented, DocumentedFields, FieldTypes)]
16#[serde(default, deny_unknown_fields)]
17/// A GitHub repository with its configuration settings
18pub struct GitHubRepository {
19    /// A short description of the repository
20    pub description: Option<String>,
21    /// A URL with more information about the repository
22    pub homepage: Option<String>,
23    /// An array of topics to help categorize the repository
24    pub topics: Vec<String>,
25    /// Whether the repository is private. If false, the repository is public
26    pub private: bool,
27    /// Whether issues are enabled for the repository
28    pub has_issues: bool,
29    /// Whether projects are enabled for the repository
30    pub has_projects: bool,
31    /// Whether the wiki is enabled for the repository
32    pub has_wiki: bool,
33    /// Whether to allow squash merges for pull requests
34    pub allow_squash_merge: bool,
35    /// Whether to allow merge commits for pull requests
36    pub allow_merge_commit: bool,
37    /// Whether to allow rebase merges for pull requests
38    pub allow_rebase_merge: bool,
39    /// Whether to allow auto-merge on pull requests
40    pub allow_auto_merge: bool,
41    /// Whether to delete head branches when pull requests are merged
42    pub delete_branch_on_merge: bool,
43    /// The default branch for the repository (e.g., "main" or "master")
44    pub default_branch: String,
45    /// Whether the repository is archived and read-only
46    pub archived: bool,
47    /// Whether the repository is disabled
48    pub disabled: bool,
49    /// Map of collaborators (users or teams) and their permission roles
50    pub collaborators: HashMap<CollaboratorPrincipal, Role>,
51}
52
53impl Default for GitHubRepository {
54    fn default() -> Self {
55        Self {
56            description: Default::default(),
57            homepage: Default::default(),
58            topics: Default::default(),
59            private: true,
60            has_issues: true,
61            has_projects: true,
62            has_wiki: true,
63            allow_squash_merge: true,
64            allow_merge_commit: true,
65            allow_rebase_merge: true,
66            allow_auto_merge: false,
67            delete_branch_on_merge: false,
68            default_branch: "main".into(),
69            archived: false,
70            disabled: false,
71            collaborators: Default::default(),
72        }
73    }
74}
75
76#[derive(Debug, Serialize, Deserialize, PartialEq, Documented, DocumentedFields, FieldTypes)]
77#[serde(deny_unknown_fields)]
78/// Required status checks that must pass before merging a pull request
79pub struct RequiredStatusChecks {
80    /// Whether to require branches to be up to date before merging
81    pub strict: bool,
82    /// The list of status checks that must pass before branches can be merged
83    pub contexts: Vec<String>,
84}
85
86#[derive(Debug, Serialize, Deserialize, PartialEq, Documented, DocumentedFields, FieldTypes)]
87#[serde(deny_unknown_fields)]
88/// Pull request review enforcement settings for branch protection
89pub struct PullRequestReviewEnforcement {
90    /// The number of approving reviews required before a pull request can be merged
91    pub required_approving_review_count: u32,
92    /// Whether to dismiss approving reviews when new commits are pushed
93    pub dismiss_stale_reviews: bool,
94    /// Whether to require review from code owners
95    pub require_code_owner_reviews: bool,
96    /// Whether to require approval of the most recent reviewable push
97    pub require_last_push_approval: bool,
98}
99
100#[derive(Debug, Serialize, Deserialize, PartialEq, Documented, DocumentedFields, FieldTypes)]
101#[serde(deny_unknown_fields)]
102/// Restrictions on who can push to a protected branch
103pub struct BranchRestrictions {
104    /// Users allowed to push to the branch
105    pub users: Vec<String>,
106    /// Teams allowed to push to the branch
107    pub teams: Vec<String>,
108    /// GitHub Apps allowed to push to the branch
109    pub apps: Vec<String>,
110}
111
112#[derive(Debug, Serialize, Deserialize, PartialEq, Documented, DocumentedFields, FieldTypes)]
113#[serde(deny_unknown_fields)]
114/// Branch protection rules that control how a branch can be modified
115pub struct BranchProtection {
116    /// Status checks that must pass before merging
117    pub required_status_checks: Option<RequiredStatusChecks>,
118    /// Whether to enforce all configured restrictions for administrators
119    pub enforce_admins: bool,
120    /// Pull request review requirements
121    pub required_pull_request_reviews: Option<PullRequestReviewEnforcement>,
122    /// Restrictions on who can push to the branch
123    pub restrictions: Option<BranchRestrictions>,
124    /// Whether to require a linear commit history (no merge commits)
125    pub required_linear_history: bool,
126    /// Whether to allow force pushes to the branch
127    pub allow_force_pushes: bool,
128    /// Whether to allow branch deletions
129    pub allow_deletions: bool,
130    /// Whether to block creation of matching branches
131    pub block_creations: bool,
132    /// Whether to require all conversations on code to be resolved before merging
133    pub required_conversation_resolution: bool,
134    /// Whether to lock the branch, making it read-only
135    pub lock_branch: bool,
136    /// Whether to allow users with push access to sync from upstream forks
137    pub allow_fork_syncing: bool,
138}
139
140#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Documented, DocumentedFields)]
141/// A principal that can be granted collaborator access to a repository
142pub enum CollaboratorPrincipal {
143    /// A GitHub user account by username
144    User(String),
145    /// A GitHub team by slug/name
146    Team(String),
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Documented)]
150/// Permission level for repository collaborators
151pub enum Role {
152    /// Read-only access to the repository
153    Read,
154    /// Triage access: read plus ability to manage issues and pull requests
155    Triage,
156    /// Write access: triage plus ability to push to the repository
157    Write,
158    /// Maintain access: write plus ability to manage the repository without sensitive actions
159    Maintain,
160    /// Administrator access: full control of the repository
161    Admin,
162    /// A custom repository role defined in the organization
163    Custom(String),
164}
165
166impl Role {
167    pub fn to_string(&self) -> String {
168        match self {
169            Role::Read => "read",
170            Role::Triage => "triage",
171            Role::Write => "write",
172            Role::Maintain => "maintain",
173            Role::Admin => "admin",
174            Role::Custom(s) => s,
175        }
176        .into()
177    }
178    pub fn from_str(s: &str) -> Self {
179        match s {
180            "read" => Role::Read,
181            "triage" => Role::Triage,
182            "write" => Role::Write,
183            "maintain" => Role::Maintain,
184            "admin" => Role::Admin,
185            s => Role::Custom(s.into()),
186        }
187    }
188}
189
190// #[derive(Debug, Serialize, Deserialize, PartialEq)]
191// #[serde(deny_unknown_fields)]
192// pub struct CollaboratorSet {
193//     pub users: HashMap<String, Role>,
194//     #[serde(skip_serializing_if = "HashMap::is_empty")]
195//     pub teams: HashMap<String, Role>,
196// }
197
198pub enum GitHubResource {
199    Repository(GitHubRepository),
200    BranchProtection(BranchProtection),
201}
202
203impl Resource for GitHubResource {
204    fn to_bytes(&self) -> Result<Vec<u8>, anyhow::Error> {
205        let pretty_config = autoschematic_core::util::PrettyConfig::default().struct_names(true);
206        match self {
207            GitHubResource::Repository(repo) => Ok(RON.to_string_pretty(&repo, pretty_config)?.into()),
208            GitHubResource::BranchProtection(protection) => Ok(RON.to_string_pretty(&protection, pretty_config)?.into()),
209        }
210    }
211
212    fn from_bytes(addr: &impl ResourceAddress, s: &[u8]) -> Result<Self, anyhow::Error>
213    where
214        Self: Sized,
215    {
216        let addr = GitHubResourceAddress::from_path(&addr.to_path_buf())?;
217        let s = std::str::from_utf8(s)?;
218
219        match addr {
220            GitHubResourceAddress::Repository { .. } => Ok(GitHubResource::Repository(RON.from_str(s)?)),
221            GitHubResourceAddress::BranchProtection { .. } => Ok(GitHubResource::BranchProtection(RON.from_str(s)?)),
222            _ => Err(invalid_addr(&addr)),
223        }
224    }
225}