use anyhow::{Context as _, Result, bail};
use std::process::Command;
use super::git_output;
use super::remote::detect_github_repo;
use super::tags::create_and_push_tag;
pub fn gh_api_get(endpoint: &str, token: Option<&str>) -> Result<serde_json::Value> {
let mut cmd = Command::new("gh");
cmd.args(["api", endpoint]);
if let Some(tok) = token {
cmd.env("GITHUB_TOKEN", tok);
}
let output = cmd
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.context("failed to spawn gh CLI")?;
if !output.status.success() {
let stderr_raw = String::from_utf8_lossy(&output.stderr);
let raw = format!("gh api GET {} failed: {}", endpoint, stderr_raw.trim());
bail!("{}", redact_gh_stderr(&raw, token));
}
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout).context("failed to parse gh api response")
}
fn redact_gh_stderr(stderr: &str, token: Option<&str>) -> String {
let stripped = crate::redact::redact_url_credentials(stderr);
let mut env: Vec<(String, String)> = std::env::vars().collect();
if let Some(tok) = token
&& !tok.is_empty()
{
env.push(("GITHUB_TOKEN".to_string(), tok.to_string()));
}
crate::redact::string(&stripped, &env)
}
pub fn gh_api_get_paginated(endpoint: &str, token: Option<&str>) -> Result<Vec<serde_json::Value>> {
let mut cmd = Command::new("gh");
cmd.args(["api", "--paginate", endpoint]);
if let Some(tok) = token {
cmd.env("GITHUB_TOKEN", tok);
}
let output = cmd
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.context("failed to spawn gh CLI")?;
if !output.status.success() {
let stderr_raw = String::from_utf8_lossy(&output.stderr);
let raw = format!("gh api GET {} failed: {}", endpoint, stderr_raw.trim());
bail!("{}", redact_gh_stderr(&raw, token));
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(serde_json::Value::Array(arr)) = serde_json::from_str::<serde_json::Value>(&stdout) {
return Ok(arr);
}
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&stdout) {
return Ok(vec![val]);
}
let mut all_items = Vec::new();
for chunk in stdout.split_inclusive(']') {
let trimmed = chunk.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(serde_json::Value::Array(arr)) =
serde_json::from_str::<serde_json::Value>(trimmed)
{
all_items.extend(arr);
} else if let Ok(val) = serde_json::from_str::<serde_json::Value>(trimmed) {
all_items.push(val);
} else {
tracing::warn!(
len = trimmed.len(),
chunk = ?&trimmed[..trimmed.len().min(200)],
"gh_api_get_paginated: failed to parse JSON chunk",
);
}
}
Ok(all_items)
}
fn gh_api_post(endpoint: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
use std::io::Write;
let body_str = serde_json::to_string(body)?;
let mut child = Command::new("gh")
.args(["api", "--method", "POST", endpoint, "--input", "-"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.context("failed to spawn gh CLI")?;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(body_str.as_bytes())?;
}
child.stdin.take();
let output = child.wait_with_output()?;
if !output.status.success() {
let stderr_raw = String::from_utf8_lossy(&output.stderr);
let raw = format!("gh api POST {} failed: {}", endpoint, stderr_raw.trim());
bail!("{}", crate::redact::redact_process_env(&raw));
}
let response: serde_json::Value = serde_json::from_slice(&output.stdout)
.with_context(|| format!("failed to parse GitHub API response from {}", endpoint))?;
Ok(response)
}
pub fn create_tag_via_github_api(
tag: &str,
message: &str,
dry_run: bool,
log: &crate::log::StageLogger,
strict: bool,
) -> Result<()> {
if dry_run {
log.status(&format!(
"(dry-run) would create tag via GitHub API: {} (\"{}\")",
tag, message
));
return Ok(());
}
let (owner, repo) = detect_github_repo()?;
let sha = git_output(&["rev-parse", "HEAD"])?;
let body = serde_json::json!({
"tag": tag,
"message": message,
"object": sha,
"type": "commit",
"tagger": {
"name": git_output(&["config", "user.name"]).unwrap_or_else(|_| "anodizer".to_string()),
"email": git_output(&["config", "user.email"]).unwrap_or_else(|_| "anodizer@users.noreply.github.com".to_string()),
"date": crate::sde::resolve_now().to_rfc3339(),
}
});
let tag_endpoint = format!("/repos/{owner}/{repo}/git/tags");
let response = match gh_api_post(&tag_endpoint, &body) {
Ok(resp) => resp,
Err(e) => {
if e.to_string().contains("failed to spawn gh CLI") {
if strict {
anyhow::bail!(
"gh CLI not found, cannot create tag via GitHub API (strict mode)"
);
}
log.warn("gh CLI not found, falling back to local git tag + push");
return create_and_push_tag(tag, message, dry_run, log, strict);
}
return Err(e);
}
};
let tag_sha = response["sha"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("GitHub API response missing 'sha' field"))?;
let ref_body = serde_json::json!({
"ref": format!("refs/tags/{}", tag),
"sha": tag_sha,
});
let ref_endpoint = format!("/repos/{owner}/{repo}/git/refs");
gh_api_post(&ref_endpoint, &ref_body)?;
Ok(())
}