ggen_cli_lib/cmds/
github.rs

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    /// Check GitHub Pages configuration and status
15    PagesStatus(PagesStatusArgs),
16    /// View GitHub Actions workflow runs
17    WorkflowStatus(WorkflowStatusArgs),
18    /// Trigger a workflow manually
19    TriggerWorkflow(TriggerWorkflowArgs),
20}
21
22#[derive(Args, Debug)]
23pub struct PagesStatusArgs {
24    /// Repository in owner/repo format (defaults to current repo from git remote)
25    #[arg(short, long)]
26    pub repo: Option<String>,
27
28    /// Output as JSON
29    #[arg(long)]
30    pub json: bool,
31}
32
33#[derive(Args, Debug)]
34pub struct WorkflowStatusArgs {
35    /// Workflow file name (e.g., "publish-registry.yml")
36    #[arg(short, long, default_value = "publish-registry.yml")]
37    pub workflow: String,
38
39    /// Repository in owner/repo format (defaults to current repo from git remote)
40    #[arg(short, long)]
41    pub repo: Option<String>,
42
43    /// Number of runs to show
44    #[arg(short, long, default_value = "10")]
45    pub limit: u32,
46
47    /// Output as JSON
48    #[arg(long)]
49    pub json: bool,
50}
51
52#[derive(Args, Debug)]
53pub struct TriggerWorkflowArgs {
54    /// Workflow file name (e.g., "publish-registry.yml")
55    #[arg(short, long, default_value = "publish-registry.yml")]
56    pub workflow: String,
57
58    /// Repository in owner/repo format (defaults to current repo from git remote)
59    #[arg(short, long)]
60    pub repo: Option<String>,
61
62    /// Branch, tag, or SHA to run workflow on
63    #[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    // Get Pages configuration
86    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    // Check if site is accessible
120    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        // Extract just the date and time part of the RFC3339 string
256        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
281/// Get repository from argument or git remote
282fn get_repository(repo_arg: &Option<String>) -> Result<String> {
283    if let Some(repo) = repo_arg {
284        return Ok(repo.clone());
285    }
286
287    // Try to get from git remote
288    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    // Parse GitHub URL (supports both HTTPS and SSH)
299    // HTTPS: https://github.com/owner/repo.git
300    // SSH: git@github.com:owner/repo.git
301    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}