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 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 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 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 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 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 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}