claude_code_toolkit/providers/
github.rs

1//! GitHub provider implementation following Repository Pattern
2
3use 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
12/// GitHub CLI management utility
13pub struct GitHubManager;
14
15impl GitHubManager {
16  pub fn new() -> Self {
17    Self
18  }
19
20  /// Check if GitHub CLI is available
21  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  /// Check if GitHub CLI is authenticated
31  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  /// List available organizations
45  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
71/// GitHub secret provider implementation
72pub 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; // Skip targets not for this provider
170      }
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![]) // Return empty vec instead of error for better UX
248    }
249  }
250
251  async fn is_configured(&self) -> Result<bool> {
252    // Check if gh CLI is available and authenticated
253    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
269/// GitHub provider creator for Factory Pattern
270pub 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![] // GitHub provider uses gh CLI, no additional config required
284  }
285
286  fn optional_config(&self) -> Vec<&str> {
287    vec!["api_endpoint", "timeout", "retry_count"]
288  }
289}