Skip to main content

ward/github/
rulesets.rs

1use anyhow::{Context, Result};
2use serde::Deserialize;
3
4use super::Client;
5
6#[derive(Debug, Deserialize)]
7pub struct Ruleset {
8    pub id: u64,
9    pub name: String,
10}
11
12#[derive(Debug, Deserialize)]
13pub struct RulesetDetail {
14    pub id: u64,
15    pub name: String,
16    pub enforcement: String,
17    #[serde(default)]
18    pub target: String,
19    #[serde(default)]
20    pub rules: Vec<RulesetRule>,
21    #[serde(default)]
22    pub conditions: Option<serde_json::Value>,
23    #[serde(default)]
24    pub bypass_actors: Vec<serde_json::Value>,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct RulesetRule {
29    #[serde(rename = "type")]
30    pub rule_type: String,
31    #[serde(default)]
32    pub parameters: Option<serde_json::Value>,
33}
34
35impl Client {
36    /// List rulesets for a repository.
37    pub async fn list_rulesets(&self, repo: &str) -> Result<Vec<Ruleset>> {
38        let resp = self
39            .get(&format!("/repos/{}/{repo}/rulesets", self.org))
40            .await?;
41
42        if !resp.status().is_success() {
43            return Ok(Vec::new());
44        }
45
46        Ok(resp.json().await.unwrap_or_default())
47    }
48
49    /// Get details for a specific ruleset.
50    pub async fn get_ruleset(&self, repo: &str, ruleset_id: u64) -> Result<RulesetDetail> {
51        let resp = self
52            .get(&format!("/repos/{}/{repo}/rulesets/{ruleset_id}", self.org))
53            .await?;
54
55        let status = resp.status();
56        if !status.is_success() {
57            let body = resp.text().await.unwrap_or_default();
58            anyhow::bail!("Failed to get ruleset {ruleset_id} for {repo} (HTTP {status}): {body}");
59        }
60
61        resp.json()
62            .await
63            .context("Failed to parse ruleset detail response")
64    }
65
66    /// Create a new ruleset for a repository.
67    pub async fn create_ruleset(
68        &self,
69        repo: &str,
70        ruleset: &serde_json::Value,
71    ) -> Result<RulesetDetail> {
72        let resp = self
73            .post_json(&format!("/repos/{}/{repo}/rulesets", self.org), ruleset)
74            .await?;
75
76        let status = resp.status();
77        if !status.is_success() {
78            let body = resp.text().await.unwrap_or_default();
79            anyhow::bail!("Failed to create ruleset for {repo} (HTTP {status}): {body}");
80        }
81
82        resp.json()
83            .await
84            .context("Failed to parse created ruleset response")
85    }
86
87    /// Update an existing ruleset.
88    pub async fn update_ruleset(
89        &self,
90        repo: &str,
91        ruleset_id: u64,
92        ruleset: &serde_json::Value,
93    ) -> Result<()> {
94        let resp = self
95            .put_json(
96                &format!("/repos/{}/{repo}/rulesets/{ruleset_id}", self.org),
97                ruleset,
98            )
99            .await?;
100
101        let status = resp.status();
102        if !status.is_success() {
103            let body = resp.text().await.unwrap_or_default();
104            anyhow::bail!(
105                "Failed to update ruleset {ruleset_id} for {repo} (HTTP {status}): {body}"
106            );
107        }
108
109        Ok(())
110    }
111
112    /// Delete a ruleset from a repository.
113    pub async fn delete_ruleset(&self, repo: &str, ruleset_id: u64) -> Result<()> {
114        let resp = self
115            .delete(&format!("/repos/{}/{repo}/rulesets/{ruleset_id}", self.org))
116            .await?;
117
118        let status = resp.status();
119        if !status.is_success() && status.as_u16() != 204 {
120            let body = resp.text().await.unwrap_or_default();
121            anyhow::bail!(
122                "Failed to delete ruleset {ruleset_id} for {repo} (HTTP {status}): {body}"
123            );
124        }
125
126        Ok(())
127    }
128
129    /// Create a Copilot code review ruleset.
130    pub async fn create_copilot_review_ruleset(&self, repo: &str) -> Result<()> {
131        let existing = self.list_rulesets(repo).await?;
132        if existing.iter().any(|r| r.name == "Copilot Code Review") {
133            tracing::info!("Copilot review ruleset already exists for {repo}");
134            return Ok(());
135        }
136
137        let body = serde_json::json!({
138            "name": "Copilot Code Review",
139            "target": "branch",
140            "enforcement": "active",
141            "conditions": {
142                "ref_name": {
143                    "include": ["~DEFAULT_BRANCH"],
144                    "exclude": []
145                }
146            },
147            "rules": [{
148                "type": "copilot_code_review",
149                "parameters": {
150                    "review_on_push": true
151                }
152            }],
153            "bypass_actors": []
154        });
155
156        let resp = self
157            .post_json(&format!("/repos/{}/{repo}/rulesets", self.org), &body)
158            .await?;
159
160        let status = resp.status();
161        if !status.is_success() {
162            let body = resp.text().await.unwrap_or_default();
163            anyhow::bail!(
164                "Failed to create Copilot review ruleset for {repo} (HTTP {status}): {body}"
165            );
166        }
167
168        Ok(())
169    }
170}