xbp 10.40.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! User-facing GitHub issues management (`xbp github`).

mod client;
mod interactive;

use crate::cli::commands::{GithubCmd, GithubSubCommand};
use crate::commands::cloudflare_config::require_interactive_terminal;
use crate::config::resolve_github_oauth2_key;
use crate::utils::{
    find_xbp_config_upwards, git_remote_url_from_metadata, parse_github_repo_from_remote_url,
};
use std::env;
use std::path::PathBuf;

pub use client::{
    create_comment, create_issue, ensure_label, get_issue, list_comments, list_issues, list_labels,
    update_issue, CreateIssueInput, GithubComment, GithubIssue, ListIssuesFilter, UpdateIssueInput,
};

pub async fn run_github(cmd: GithubCmd) -> Result<(), String> {
    let token = resolve_github_token()?;
    let (owner, repo) = resolve_repo(cmd.owner.as_deref(), cmd.repo.as_deref())?;
    match cmd.command {
        None => interactive::run_hub(&token, &owner, &repo).await,
        Some(GithubSubCommand::List(args)) => client::run_list(&token, &owner, &repo, args).await,
        Some(GithubSubCommand::Show(args)) => client::run_show(&token, &owner, &repo, args).await,
        Some(GithubSubCommand::Create(args)) => {
            client::run_create(&token, &owner, &repo, args).await
        }
        Some(GithubSubCommand::Edit(args)) => client::run_edit(&token, &owner, &repo, args).await,
        Some(GithubSubCommand::Status(args)) => {
            client::run_status(&token, &owner, &repo, args).await
        }
        Some(GithubSubCommand::Labels(args)) => {
            client::run_labels(&token, &owner, &repo, args).await
        }
        Some(GithubSubCommand::Assign(args)) => {
            client::run_assign(&token, &owner, &repo, args).await
        }
        Some(GithubSubCommand::Comment(args)) => {
            client::run_comment(&token, &owner, &repo, args).await
        }
        Some(GithubSubCommand::Comments(args)) => {
            client::run_comments(&token, &owner, &repo, args).await
        }
    }
}

pub(crate) fn resolve_github_token() -> Result<String, String> {
    resolve_github_oauth2_key().ok_or_else(|| {
        "No GitHub token found. Configure one with `xbp config github set-key` (or set `github_oauth2_key` in global config).".to_string()
    })
}

pub(crate) fn resolve_repo(
    owner_override: Option<&str>,
    repo_override: Option<&str>,
) -> Result<(String, String), String> {
    if let (Some(owner), Some(repo)) = (owner_override, repo_override) {
        let owner = owner.trim();
        let repo = repo.trim();
        if !owner.is_empty() && !repo.is_empty() {
            return Ok((owner.to_string(), repo.to_string()));
        }
    }
    let project_root = detect_project_root();
    let remote = git_remote_url_from_metadata(&project_root, "origin")?.ok_or_else(|| {
        format!(
            "Could not resolve git remote `origin` under {}. Pass `--owner` and `--repo`.",
            project_root.display()
        )
    })?;
    parse_github_repo_from_remote_url(&remote).ok_or_else(|| {
        format!("Remote is not a GitHub URL (`{remote}`). Pass `--owner` and `--repo`.")
    })
}

fn detect_project_root() -> PathBuf {
    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    // Prefer the enclosing git work tree so monorepo package `.xbp` configs still
    // resolve `origin` from the repository root (not the package subfolder).
    for dir in cwd.ancestors() {
        if dir.join(".git").exists() {
            return dir.to_path_buf();
        }
    }
    if let Some(found) = find_xbp_config_upwards(&cwd) {
        return found.project_root;
    }
    cwd
}

pub(crate) fn require_interactive(context: &str) -> Result<(), String> {
    require_interactive_terminal(context)
}