refactor(#2051): fold body-return into forge_http instead of a near-copy
Per review: rather than adding forge_http_full (a near-duplicate of forge_http), change forge_http itself to return (StatusCode, String). Status-only callers bind (status, _); the branch-protection verify path uses the body to log the real Forgejo rejection reason. Updates all call sites accordingly.
This commit is contained in:
parent
512e9ff09f
commit
3fedc102cc
1 changed files with 24 additions and 40 deletions
|
|
@ -139,33 +139,16 @@ fn agent_email(name: &str) -> String {
|
|||
/// 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.
|
||||
/// Returns the response status **and body**. The body lets callers log
|
||||
/// *why* Forgejo rejected a request (e.g. the validation message on a
|
||||
/// 422); status-only callers just bind `(status, _)`. Body read is
|
||||
/// best-effort — a read error yields an empty string rather than
|
||||
/// failing the whole call.
|
||||
async fn forge_http(
|
||||
method: reqwest::Method,
|
||||
url: &str,
|
||||
token: &str,
|
||||
body: &str,
|
||||
) -> Result<StatusCode> {
|
||||
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())
|
||||
}
|
||||
|
||||
/// Like [`forge_http`] but also returns the response body, so callers
|
||||
/// can log *why* Forgejo rejected a request (e.g. the validation
|
||||
/// message on a 422). Body read is best-effort — a read error yields an
|
||||
/// empty string rather than failing the whole call.
|
||||
async fn forge_http_full(
|
||||
method: reqwest::Method,
|
||||
url: &str,
|
||||
token: &str,
|
||||
body: &str,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
|
|
@ -275,14 +258,14 @@ async fn ensure_user_email(name: &str) {
|
|||
let body = format!(r#"{{"email":"{email}","login_name":"{name}","source_id":0}}"#);
|
||||
let url = format!("{FORGE_HTTP}/api/v1/admin/users/{name}");
|
||||
match forge_http(reqwest::Method::PATCH, &url, &token, &body).await {
|
||||
Ok(status) if status.is_success() => {
|
||||
Ok((status, _)) if status.is_success() => {
|
||||
if let Some(parent) = marker.parent() {
|
||||
std::fs::create_dir_all(parent).ok();
|
||||
}
|
||||
std::fs::write(&marker, "").ok();
|
||||
tracing::info!(%name, %email, "forge: user email aligned");
|
||||
}
|
||||
Ok(status) if status == reqwest::StatusCode::FORBIDDEN => {
|
||||
Ok((status, _)) if status == reqwest::StatusCode::FORBIDDEN => {
|
||||
// Core token missing admin scope — see
|
||||
// `docs/forge.md::Token scopes` migration note.
|
||||
tracing::warn!(
|
||||
|
|
@ -291,7 +274,7 @@ async fn ensure_user_email(name: &str) {
|
|||
Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes."
|
||||
);
|
||||
}
|
||||
Ok(status) => {
|
||||
Ok((status, _)) => {
|
||||
tracing::warn!(%name, %email, %status, "forge: PATCH user email returned non-success");
|
||||
}
|
||||
Err(e) => tracing::warn!(%name, error = %e, "forge: PATCH user email transport error"),
|
||||
|
|
@ -326,21 +309,21 @@ async fn ensure_repo_creation_disabled(name: &str) {
|
|||
let body = format!(r#"{{"login_name":"{name}","source_id":0,"max_repo_creation":0}}"#);
|
||||
let url = format!("{FORGE_HTTP}/api/v1/admin/users/{name}");
|
||||
match forge_http(reqwest::Method::PATCH, &url, &token, &body).await {
|
||||
Ok(status) if status.is_success() => {
|
||||
Ok((status, _)) if status.is_success() => {
|
||||
if let Some(parent) = marker.parent() {
|
||||
std::fs::create_dir_all(parent).ok();
|
||||
}
|
||||
std::fs::write(&marker, "").ok();
|
||||
tracing::info!(%name, "forge: disabled direct repo creation (max_repo_creation=0)");
|
||||
}
|
||||
Ok(status) if status == reqwest::StatusCode::FORBIDDEN => {
|
||||
Ok((status, _)) if status == reqwest::StatusCode::FORBIDDEN => {
|
||||
tracing::warn!(
|
||||
%name, %status,
|
||||
"forge: PATCH max_repo_creation forbidden — core token likely missing admin scope. \
|
||||
Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes."
|
||||
);
|
||||
}
|
||||
Ok(status) => {
|
||||
Ok((status, _)) => {
|
||||
tracing::warn!(%name, %status, "forge: PATCH max_repo_creation returned non-success");
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -468,7 +451,7 @@ async fn ensure_core_avatar(token: &str) -> Result<()> {
|
|||
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
|
||||
);
|
||||
let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar");
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("set core avatar: HTTP {status}");
|
||||
}
|
||||
|
|
@ -499,7 +482,7 @@ async fn ensure_config_org_avatar(token: &str) -> Result<()> {
|
|||
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
|
||||
);
|
||||
let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar");
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("set {CONFIG_ORG} avatar: HTTP {status}");
|
||||
}
|
||||
|
|
@ -552,7 +535,7 @@ fn classify_core_token_status(status: StatusCode) -> CoreTokenCheck {
|
|||
async fn check_core_token(token: &str) -> CoreTokenCheck {
|
||||
let url = format!("{FORGE_HTTP}/api/v1/user");
|
||||
match forge_http(reqwest::Method::GET, &url, token, "").await {
|
||||
Ok(status) => classify_core_token_status(status),
|
||||
Ok((status, _)) => classify_core_token_status(status),
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
error = %e,
|
||||
|
|
@ -614,7 +597,8 @@ fn repo_body_public(name: &str) -> String {
|
|||
/// created as private on an older deployment.
|
||||
async fn set_repo_public(owner: &str, repo: &str, token: &str) -> Result<()> {
|
||||
let url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}");
|
||||
let status = forge_http(reqwest::Method::PATCH, &url, token, r#"{"private":false}"#).await?;
|
||||
let (status, _) =
|
||||
forge_http(reqwest::Method::PATCH, &url, token, r#"{"private":false}"#).await?;
|
||||
match status.as_u16() {
|
||||
200 => {
|
||||
tracing::debug!(%owner, %repo, "forge: repo set to public");
|
||||
|
|
@ -639,7 +623,7 @@ async fn ensure_org_repo_public(org: &str, name: &str, token: &str) -> Result<()
|
|||
/// (HTTP 409 / 422) into success. `label` is `<owner>/<name>` — purely
|
||||
/// for log + error context.
|
||||
async fn create_repo(url: &str, body: &str, token: &str, label: &str) -> Result<()> {
|
||||
let status = forge_http(reqwest::Method::POST, url, token, body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, url, token, body).await?;
|
||||
match status.as_u16() {
|
||||
201 => {
|
||||
tracing::info!(%label, "forge: created repo");
|
||||
|
|
@ -915,7 +899,7 @@ 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 status = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?;
|
||||
match status.as_u16() {
|
||||
201 => {
|
||||
tracing::info!(%name, "forge: created org");
|
||||
|
|
@ -950,7 +934,7 @@ async fn ensure_operators_team(token: &str) -> Result<()> {
|
|||
let body = format!(
|
||||
r#"{{"name":"{OPERATORS_TEAM}","description":"hyperhive operators — merge gate for agent repos","permission":"write","includes_all_repositories":true,"can_create_org_repo":false}}"#
|
||||
);
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
match status.as_u16() {
|
||||
201 => {
|
||||
tracing::info!("forge: created {OPERATORS_TEAM} team in {AGENTS_ORG}");
|
||||
|
|
@ -978,7 +962,7 @@ async fn add_collaborator(
|
|||
) -> Result<()> {
|
||||
let url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}/collaborators/{user}");
|
||||
let body = format!(r#"{{"permission":"{permission}"}}"#);
|
||||
let status = forge_http(reqwest::Method::PUT, &url, token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::PUT, &url, token, &body).await?;
|
||||
match status.as_u16() {
|
||||
201 | 204 => {
|
||||
tracing::debug!(%owner, %repo, %user, %permission, "forge: collaborator set");
|
||||
|
|
@ -1000,7 +984,7 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()>
|
|||
let body = format!(
|
||||
r#"{{"branch_name":"main","enable_merge_whitelist":true,"merge_whitelist_teams":["{OPERATORS_TEAM}"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true}}"#
|
||||
);
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
match status.as_u16() {
|
||||
201 => {
|
||||
tracing::info!(%repo, "forge: applied operator branch protection");
|
||||
|
|
@ -1037,7 +1021,7 @@ async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<
|
|||
let body = format!(
|
||||
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":false}}"#
|
||||
);
|
||||
let (status, resp_body) = forge_http_full(reqwest::Method::POST, &url, token, &body).await?;
|
||||
let (status, resp_body) = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
if status.as_u16() == 201 {
|
||||
tracing::info!(%repo, "forge: applied config-repo branch protection");
|
||||
return Ok(());
|
||||
|
|
@ -1051,7 +1035,7 @@ async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<
|
|||
// verify the `main` rule actually exists, and on failure surface the
|
||||
// POST's response body so the real reason is in the journal.
|
||||
let main_url = format!("{url}/main");
|
||||
let check = forge_http(reqwest::Method::GET, &main_url, token, "").await?;
|
||||
let (check, _) = forge_http(reqwest::Method::GET, &main_url, token, "").await?;
|
||||
if check.as_u16() == 200 {
|
||||
tracing::debug!(%repo, %status, "forge: config-repo branch protection already present");
|
||||
Ok(())
|
||||
|
|
@ -1447,7 +1431,7 @@ pub async fn mark_pr_merged(repo: &str, pr: u64, sha: &str) -> Result<(), ForgeM
|
|||
.ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?;
|
||||
let url = format!("{FORGE_HTTP}/api/v1/repos/{repo}/pulls/{pr}/merge");
|
||||
let body = format!(r#"{{"Do":"manually-merged","MergeCommitID":"{sha}"}}"#);
|
||||
let status = forge_http(reqwest::Method::POST, &url, &token, &body)
|
||||
let (status, _) = forge_http(reqwest::Method::POST, &url, &token, &body)
|
||||
.await
|
||||
.context("POST pulls/<pr>/merge (manually-merged)")?;
|
||||
if status.is_success() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue