claude_code_toolkit/providers/
github.rs1use super::{ BaseProvider, ProviderCreator };
4use crate::error::{ ClaudeCodeError, Result };
5use crate::traits::{ Secret, SecretProvider, SyncResult, Target };
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::process::Stdio;
9use tokio::process::Command;
10use tracing::{ debug, error, info, warn };
11
12pub struct GitHubManager;
14
15impl GitHubManager {
16 pub fn new() -> Self {
17 Self
18 }
19
20 pub async fn check_gh_cli(&self) -> Result<bool> {
22 match
23 Command::new("gh").arg("--version").stdout(Stdio::null()).stderr(Stdio::null()).status().await
24 {
25 Ok(status) => Ok(status.success()),
26 Err(_) => Ok(false),
27 }
28 }
29
30 pub async fn check_authentication(&self) -> Result<bool> {
32 match
33 Command::new("gh")
34 .args(["auth", "status"])
35 .stdout(Stdio::null())
36 .stderr(Stdio::null())
37 .status().await
38 {
39 Ok(status) => Ok(status.success()),
40 Err(_) => Ok(false),
41 }
42 }
43
44 pub async fn list_organizations(&self) -> Result<Vec<String>> {
46 let output = Command::new("gh")
47 .args(["api", "user/orgs", "--jq", ".[].login"])
48 .output().await
49 .map_err(|e| { ClaudeCodeError::Process(format!("Failed to list organizations: {}", e)) })?;
50
51 if !output.status.success() {
52 return Err(ClaudeCodeError::Process("Failed to fetch organizations from GitHub".to_string()));
53 }
54
55 let organizations = String::from_utf8_lossy(&output.stdout)
56 .lines()
57 .map(|line| line.trim().to_string())
58 .filter(|line| !line.is_empty())
59 .collect();
60
61 Ok(organizations)
62 }
63}
64
65impl Default for GitHubManager {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71pub struct GitHubProvider {
73 #[allow(dead_code)]
74 base: BaseProvider,
75}
76
77impl GitHubProvider {
78 pub fn new(config: HashMap<String, String>) -> Result<Self> {
79 Ok(Self {
80 base: BaseProvider::new("github", config),
81 })
82 }
83
84 async fn execute_gh_command(&self, args: &[&str]) -> Result<std::process::Output> {
85 let output = Command::new("gh")
86 .args(args)
87 .output().await
88 .map_err(|e| { ClaudeCodeError::Process(format!("Failed to execute gh command: {}", e)) })?;
89
90 Ok(output)
91 }
92
93 async fn update_secret(&self, target: &Target, secret: &Secret) -> Result<()> {
94 let args = match target.target_type.as_str() {
95 "organization" =>
96 vec!["secret", "set", &secret.name, "--org", &target.name, "--body", &secret.value],
97 "repository" =>
98 vec!["secret", "set", &secret.name, "--repo", &target.name, "--body", &secret.value],
99 _ => {
100 return Err(
101 ClaudeCodeError::Generic(format!("Unsupported target type: {}", target.target_type))
102 );
103 }
104 };
105
106 info!("Updating secret {} for {} {}", secret.name, target.target_type, target.name);
107
108 let output = self.execute_gh_command(&args).await?;
109
110 if output.status.success() {
111 info!(
112 "Successfully updated secret {} for {} {}",
113 secret.name,
114 target.target_type,
115 target.name
116 );
117 Ok(())
118 } else {
119 let error_msg = String::from_utf8_lossy(&output.stderr);
120 error!("Failed to update secret for {} {}: {}", target.target_type, target.name, error_msg);
121 Err(ClaudeCodeError::Process(format!("Failed to update secret: {}", error_msg)))
122 }
123 }
124
125 async fn check_target_access(&self, target: &Target) -> Result<bool> {
126 let (api_path, args) = match target.target_type.as_str() {
127 "organization" => {
128 let path = format!("orgs/{}", target.name);
129 (path, vec!["api"])
130 }
131 "repository" => {
132 let path = format!("repos/{}", target.name);
133 (path, vec!["api"])
134 }
135 _ => {
136 return Ok(false);
137 }
138 };
139
140 let mut full_args = args;
141 full_args.push(&api_path);
142
143 let output = self.execute_gh_command(&full_args).await?;
144 Ok(output.status.success())
145 }
146}
147
148#[async_trait]
149impl SecretProvider for GitHubProvider {
150 fn provider_name(&self) -> &str {
151 "github"
152 }
153
154 async fn sync_secrets(&self, secrets: &[Secret], targets: &[Target]) -> Result<SyncResult> {
155 let mut succeeded = 0;
156 let mut failed = 0;
157 let mut errors = Vec::new();
158
159 debug!("GitHub provider: processing {} targets, {} secrets", targets.len(), secrets.len());
160
161 for target in targets {
162 debug!(
163 "Checking target: provider='{}', self.provider_name()='{}'",
164 target.provider,
165 self.provider_name()
166 );
167 if target.provider != self.provider_name() {
168 debug!("Skipping target {} (provider mismatch)", target.name);
169 continue; }
171
172 debug!("Processing target: {} {}", target.target_type, target.name);
173 for secret in secrets {
174 debug!("Updating secret {} for target {}", secret.name, target.name);
175 match self.update_secret(target, secret).await {
176 Ok(()) => {
177 debug!("Successfully updated secret {} for {}", secret.name, target.name);
178 succeeded += 1;
179 }
180 Err(e) => {
181 error!("Failed to update secret {} for {}: {}", secret.name, target.name, e);
182 failed += 1;
183 errors.push(format!("{}:{} - {}", target.target_type, target.name, e));
184 }
185 }
186 }
187 }
188
189 Ok(SyncResult {
190 succeeded,
191 failed,
192 errors,
193 })
194 }
195
196 async fn validate_access(&self, targets: &[Target]) -> Result<HashMap<String, bool>> {
197 let mut results = HashMap::new();
198
199 for target in targets {
200 if target.provider != self.provider_name() {
201 continue;
202 }
203
204 let key = format!("{}:{}", target.target_type, target.name);
205 let has_access = self.check_target_access(target).await.unwrap_or(false);
206 results.insert(key, has_access);
207 }
208
209 Ok(results)
210 }
211
212 async fn list_targets(&self, target_type: &str) -> Result<Vec<String>> {
213 let args = match target_type {
214 "organization" => vec!["api", "user/orgs", "--jq", ".[].login"],
215 "repository" =>
216 vec![
217 "repo",
218 "list",
219 "--limit",
220 "100",
221 "--json",
222 "nameWithOwner",
223 "--jq",
224 ".[].nameWithOwner"
225 ],
226 _ => {
227 return Err(ClaudeCodeError::Generic(format!("Unsupported target type: {}", target_type)));
228 }
229 };
230
231 let output = self.execute_gh_command(&args).await?;
232
233 if output.status.success() {
234 let stdout = String::from_utf8_lossy(&output.stdout);
235 let targets: Vec<String> = stdout
236 .lines()
237 .map(|s| s.trim())
238 .filter(|s| !s.is_empty())
239 .map(|s| s.to_string())
240 .collect();
241
242 debug!("Found {} {} targets", targets.len(), target_type);
243 Ok(targets)
244 } else {
245 let error_msg = String::from_utf8_lossy(&output.stderr);
246 warn!("Failed to list {}: {}", target_type, error_msg);
247 Ok(vec![]) }
249 }
250
251 async fn is_configured(&self) -> Result<bool> {
252 let version_output = self.execute_gh_command(&["--version"]).await?;
254 if !version_output.status.success() {
255 return Ok(false);
256 }
257
258 let auth_output = Command::new("gh")
259 .args(["auth", "status"])
260 .stdout(Stdio::null())
261 .stderr(Stdio::null())
262 .status().await
263 .map_err(|e| ClaudeCodeError::Process(e.to_string()))?;
264
265 Ok(auth_output.success())
266 }
267}
268
269pub struct GitHubProviderCreator;
271
272impl ProviderCreator for GitHubProviderCreator {
273 fn create(&self, config: &HashMap<String, String>) -> Result<Box<dyn SecretProvider>> {
274 let provider = GitHubProvider::new(config.clone())?;
275 Ok(Box::new(provider))
276 }
277
278 fn provider_type(&self) -> &str {
279 "github"
280 }
281
282 fn required_config(&self) -> Vec<&str> {
283 vec![] }
285
286 fn optional_config(&self) -> Vec<&str> {
287 vec!["api_endpoint", "timeout", "retry_count"]
288 }
289}