1use anyhow::Result;
2use clap::{Args, Subcommand};
3use colored::*;
4use ggen_core::{GitHubClient, RepoInfo};
5
6#[derive(Args, Debug)]
7pub struct GitHubArgs {
8 #[command(subcommand)]
9 pub command: GitHubCommand,
10}
11
12#[derive(Subcommand, Debug)]
13pub enum GitHubCommand {
14 PagesStatus(PagesStatusArgs),
16 WorkflowStatus(WorkflowStatusArgs),
18 TriggerWorkflow(TriggerWorkflowArgs),
20}
21
22#[derive(Args, Debug)]
23pub struct PagesStatusArgs {
24 #[arg(short, long)]
26 pub repo: Option<String>,
27
28 #[arg(long)]
30 pub json: bool,
31}
32
33#[derive(Args, Debug)]
34pub struct WorkflowStatusArgs {
35 #[arg(short, long, default_value = "publish-registry.yml")]
37 pub workflow: String,
38
39 #[arg(short, long)]
41 pub repo: Option<String>,
42
43 #[arg(short, long, default_value = "10")]
45 pub limit: u32,
46
47 #[arg(long)]
49 pub json: bool,
50}
51
52#[derive(Args, Debug)]
53pub struct TriggerWorkflowArgs {
54 #[arg(short, long, default_value = "publish-registry.yml")]
56 pub workflow: String,
57
58 #[arg(short, long)]
60 pub repo: Option<String>,
61
62 #[arg(short = 'b', long, default_value = "master")]
64 pub ref_name: String,
65}
66
67pub async fn run(args: &GitHubArgs) -> Result<()> {
68 match &args.command {
69 GitHubCommand::PagesStatus(cmd_args) => pages_status(cmd_args).await,
70 GitHubCommand::WorkflowStatus(cmd_args) => workflow_status(cmd_args).await,
71 GitHubCommand::TriggerWorkflow(cmd_args) => trigger_workflow(cmd_args).await,
72 }
73}
74
75async fn pages_status(args: &PagesStatusArgs) -> Result<()> {
76 let repo_str = get_repository(&args.repo)?;
77 let repo = RepoInfo::parse(&repo_str)?;
78 let client = GitHubClient::new(repo.clone())?;
79
80 if !client.is_authenticated() {
81 eprintln!("{} {}", "Warning:".yellow(), "Not authenticated. Set GITHUB_TOKEN or GH_TOKEN for full access.");
82 eprintln!();
83 }
84
85 let pages_config_result = client.get_pages_config(&repo).await;
87
88 let mut site_url = None;
89
90 match &pages_config_result {
91 Ok(config) => {
92 site_url = config.html_url.clone();
93 if args.json {
94 println!("{}", serde_json::to_string_pretty(config)?);
95 } else {
96 print_pages_status(config, &repo);
97 }
98 }
99 Err(e) => {
100 if e.to_string().contains("not configured") {
101 if args.json {
102 println!("{{\"status\": \"not_configured\"}}");
103 } else {
104 println!("{}", "GitHub Pages Configuration:".bold());
105 println!("{}", "─".repeat(60).dimmed());
106 println!("{} GitHub Pages is not configured for {}", "❌".red(), repo.as_str());
107 println!();
108 println!("{}", "To enable GitHub Pages:".yellow());
109 println!(" 1. Go to https://github.com/{}/settings/pages", repo.as_str());
110 println!(" 2. Under 'Build and deployment', set Source to 'GitHub Actions'");
111 println!(" 3. Save settings");
112 }
113 } else {
114 anyhow::bail!("Failed to get Pages configuration: {}", e);
115 }
116 }
117 }
118
119 if let Some(url) = site_url {
121 let status = client.check_site_status(&url).await?;
122
123 if !args.json {
124 println!();
125 println!("{}", "Site Accessibility:".bold());
126 println!("{}", "─".repeat(60).dimmed());
127 if status == 200 {
128 println!("{} Site is live: {}", "✅".green(), url.cyan());
129 } else if status == 404 {
130 println!("{} Site returns 404: {}", "❌".red(), url.dimmed());
131 println!(" {}", "Deployment may still be in progress...".yellow());
132 } else {
133 println!("{} Unexpected status {}: {}", "⚠️".yellow(), status, url.dimmed());
134 }
135 }
136 }
137
138 Ok(())
139}
140
141async fn workflow_status(args: &WorkflowStatusArgs) -> Result<()> {
142 let repo_str = get_repository(&args.repo)?;
143 let repo = RepoInfo::parse(&repo_str)?;
144 let client = GitHubClient::new(repo.clone())?;
145
146 let runs = client.get_workflow_runs(&repo, &args.workflow, args.limit).await?;
147
148 if args.json {
149 println!("{}", serde_json::to_string_pretty(&runs)?);
150 } else {
151 print_workflow_runs(&runs, &args.workflow, &repo);
152 }
153
154 Ok(())
155}
156
157async fn trigger_workflow(args: &TriggerWorkflowArgs) -> Result<()> {
158 let repo_str = get_repository(&args.repo)?;
159 let repo = RepoInfo::parse(&repo_str)?;
160 let client = GitHubClient::new(repo.clone())?;
161
162 if !client.is_authenticated() {
163 anyhow::bail!("GitHub token required to trigger workflows. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
164 }
165
166 println!("{} Triggering workflow {} on branch {}...",
167 "🚀".bold(),
168 args.workflow.cyan(),
169 args.ref_name.yellow()
170 );
171
172 client.trigger_workflow(&repo, &args.workflow, &args.ref_name).await?;
173
174 println!("{} Workflow triggered successfully!", "✅".green());
175 println!();
176 println!("View status at: https://github.com/{}/actions", repo.as_str());
177
178 Ok(())
179}
180
181fn print_pages_status(config: &ggen_core::PagesConfig, repo: &RepoInfo) {
182 println!("{}", "GitHub Pages Configuration:".bold());
183 println!("{}", "─".repeat(60).dimmed());
184
185 if let Some(url) = &config.url {
186 println!("URL: {}", url.cyan());
187 }
188
189 if let Some(status) = &config.status {
190 let status_icon = match status.as_str() {
191 "built" => "✅",
192 "building" => "🔄",
193 _ => "❓",
194 };
195 println!("Status: {} {}", status_icon, status);
196 }
197
198 if let Some(source) = &config.source {
199 if let Some(branch) = &source.branch {
200 println!("Branch: {}", branch.yellow());
201 }
202 if let Some(path) = &source.path {
203 println!("Path: {}", path);
204 }
205 }
206
207 if let Some(https) = config.https_enforced {
208 println!("HTTPS: {}", if https { "✅ Enforced".green() } else { "❌ Not enforced".red() });
209 }
210
211 println!();
212 println!("{}", "Repository:".bold());
213 println!("{}", "─".repeat(60).dimmed());
214 println!("https://github.com/{}", repo.as_str());
215}
216
217fn print_workflow_runs(runs: &ggen_core::WorkflowRunsResponse, workflow: &str, repo: &RepoInfo) {
218 println!("{} {} {}",
219 "Workflow Runs for".bold(),
220 workflow.cyan(),
221 format!("({})", repo.as_str()).dimmed()
222 );
223 println!("{}", "─".repeat(80).dimmed());
224
225 if runs.workflow_runs.is_empty() {
226 println!("No runs found for this workflow");
227 return;
228 }
229
230 println!("{:<8} {:<12} {:<12} {:<15} {:<25} {}",
231 "RUN".bold(),
232 "STATUS".bold(),
233 "CONCLUSION".bold(),
234 "BRANCH".bold(),
235 "CREATED".bold(),
236 "URL".bold()
237 );
238 println!("{}", "─".repeat(80).dimmed());
239
240 for run in &runs.workflow_runs {
241 let status_icon = match run.status.as_str() {
242 "completed" => "✅",
243 "in_progress" => "🔄",
244 "queued" => "⏳",
245 _ => "❓",
246 };
247
248 let conclusion_icon = match run.conclusion.as_deref() {
249 Some("success") => "✅".green(),
250 Some("failure") => "❌".red(),
251 Some("cancelled") => "⏹️".yellow(),
252 _ => "─".dimmed(),
253 };
254
255 let created = run.created_at
257 .split('T')
258 .next()
259 .and_then(|date| {
260 run.created_at.split('T').nth(1).map(|time| {
261 let time_part = time.split('.').next().unwrap_or(time);
262 format!("{} {}", date, time_part)
263 })
264 })
265 .unwrap_or_else(|| run.created_at.clone());
266
267 println!("{:<8} {:<12} {:<12} {:<15} {:<25} {}",
268 format!("#{}", run.run_number),
269 format!("{} {}", status_icon, run.status),
270 format!("{} {}", conclusion_icon, run.conclusion.as_deref().unwrap_or("-")),
271 run.head_branch.yellow(),
272 created.dimmed(),
273 run.html_url.cyan()
274 );
275 }
276
277 println!();
278 println!("Total runs: {}", runs.total_count);
279}
280
281fn get_repository(repo_arg: &Option<String>) -> Result<String> {
283 if let Some(repo) = repo_arg {
284 return Ok(repo.clone());
285 }
286
287 let output = std::process::Command::new("git")
289 .args(&["remote", "get-url", "origin"])
290 .output()?;
291
292 if !output.status.success() {
293 anyhow::bail!("Could not determine repository. Provide --repo or run from a git repository.");
294 }
295
296 let remote_url = String::from_utf8(output.stdout)?.trim().to_string();
297
298 let repo = if remote_url.contains("github.com") {
302 let parts: Vec<&str> = if remote_url.contains("https://") {
303 remote_url.trim_end_matches(".git")
304 .split("github.com/")
305 .collect()
306 } else {
307 remote_url.trim_end_matches(".git")
308 .split("github.com:")
309 .collect()
310 };
311
312 if parts.len() == 2 {
313 parts[1].to_string()
314 } else {
315 anyhow::bail!("Could not parse GitHub repository from remote URL: {}", remote_url);
316 }
317 } else {
318 anyhow::bail!("Remote URL is not a GitHub repository: {}", remote_url);
319 };
320
321 Ok(repo)
322}