From b283768f262e7a5fecba931af73dacff81a77ac1 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 15:37:30 +0200 Subject: [PATCH] forge: replace curl shell-outs with reqwest http helper (closes #249) --- hive-c0re/Cargo.toml | 1 + hive-c0re/src/forge.rs | 85 ++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index 747d918..50afeb2 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] anyhow.workspace = true axum.workspace = true +reqwest.workspace = true clap.workspace = true hive-fr0nt.workspace = true hive-sh4re.workspace = true diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 0d07f1a..100ea9b 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -20,6 +20,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use reqwest::StatusCode; use tokio::process::Command; use crate::coordinator::Coordinator; @@ -142,6 +143,29 @@ fn agent_email(name: &str) -> String { format!("{name}@hyperhive") } +/// Thin Forgejo REST helper. Sends `method` to `url` with a JSON body +/// and `Authorization: token `, returns the HTTP status code. +/// All Forgejo API calls that don't shell out to `forgejo admin` go +/// through here — one place for auth header, content-type, error +/// propagation, and the shared reqwest Client. +async fn forge_http( + method: reqwest::Method, + url: &str, + token: &str, + body: &str, +) -> Result { + let client = reqwest::Client::new(); + let resp = client + .request(method, url) + .header("Authorization", format!("token {token}")) + .header("Content-Type", "application/json") + .body(body.to_owned()) + .send() + .await + .with_context(|| format!("forge HTTP request to {url}"))?; + Ok(resp.status()) +} + /// Ensure a forgejo user named `name` exists. Idempotent: forgejo /// returns a "user already exists" error which we treat as success. /// `admin` adds `--admin` (site admin) — used for the bootstrap @@ -271,33 +295,13 @@ fn repo_body(name: &str) -> String { /// (HTTP 409 / 422) into success. `label` is `/` — purely /// for log + error context. async fn create_repo(url: &str, body: &str, token: &str, label: &str) -> Result<()> { - let out = Command::new("curl") - .args([ - "-sS", - "-o", - "/dev/null", - "-w", - "%{http_code}", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-H", - &format!("Authorization: token {token}"), - "-d", - body, - url, - ]) - .output() - .await - .with_context(|| format!("invoke curl POST {url}"))?; - let code = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - match code.as_str() { - "201" => { + let status = forge_http(reqwest::Method::POST, url, token, body).await?; + match status.as_u16() { + 201 => { tracing::info!(%label, "forge: created repo"); Ok(()) } - "409" | "422" => { + 409 | 422 => { tracing::debug!(%label, "forge: repo already exists"); Ok(()) } @@ -501,40 +505,17 @@ pub async fn push_config(name: &str) -> Result<()> { async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { let body = format!(r#"{{"username":"{name}"}}"#); let url = format!("{FORGE_HTTP}/api/v1/orgs"); - let out = Command::new("curl") - .args([ - "-sS", - "-o", - "/dev/null", - "-w", - "%{http_code}", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-H", - &format!("Authorization: token {admin_token}"), - "-d", - &body, - &url, - ]) - .output() - .await - .context("invoke curl POST /api/v1/orgs")?; - let code = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - match code.as_str() { - "201" => { + let status = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?; + match status.as_u16() { + 201 => { tracing::info!(%name, "forge: created org"); Ok(()) } - "422" | "409" => { + 422 | 409 => { tracing::debug!(%name, "forge: org already exists"); Ok(()) } - other => anyhow::bail!( - "POST /api/v1/orgs name={name} returned HTTP {other}: {}", - String::from_utf8_lossy(&out.stderr).trim() - ), + other => anyhow::bail!("POST /api/v1/orgs name={name} returned HTTP {other}"), } }