Watch
0
0
Fork
You've already forked hyperhive
0

hive-forge repo-create: say the account can't create repos instead of a bare 500

This commit is contained in:
atlas 2026-11-12 21:11:41 +01:00
commit 6ec12e7d84
4 changed files with 83 additions and 19 deletions

View file

@ -102,7 +102,7 @@ Deadline: hive-forge stops after HIVE_FORGE_DEADLINE seconds (default 900), prin
* `comment-show` — Print the body (or full JSON) of a single comment by id * `comment-show` — Print the body (or full JSON) of a single comment by id
* `comment-edit` — Edit an existing comment by id * `comment-edit` — Edit an existing comment by id
* `clone` — Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with credentials autoinjected. Pairs with `pr create --agit` * `clone` — Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with credentials autoinjected. Pairs with `pr create --agit`
* `repo-create` — Create a forge repo under the current user (or `--org`). Prints the repo URL * `repo-create` — Create a forge repo under the current user (or `--org`). Prints the repo URL. Agent accounts can't create repos (limit 0)
* `repo-add-collaborator` — Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`) with a permission level. Companion to `repo-create` * `repo-add-collaborator` — Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`) with a permission level. Companion to `repo-create`
* `repo-labels` — List every label defined on the repo (name + description), optionally filtered by a name substring * `repo-labels` — List every label defined on the repo (name + description), optionally filtered by a name substring
* `repo-search` — Search the forge for repositories by keyword, topic, or description * `repo-search` — Search the forge for repositories by keyword, topic, or description
@ -1008,7 +1008,7 @@ Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with credentials autoinjecte
## `hive-forge repo-create` ## `hive-forge repo-create`
Create a forge repo under the current user (or `--org`). Prints the repo URL Create a forge repo under the current user (or `--org`). Prints the repo URL. Agent accounts can't create repos (limit 0)
**Usage:** `hive-forge repo-create [OPTIONS] <NAME>` **Usage:** `hive-forge repo-create [OPTIONS] <NAME>`

View file

@ -224,16 +224,18 @@ plain comment show under `last comment`, not `reviews`.
### Repo management ### Repo management
Agents **can't create repos directly via forge token** — Forgejo Agent accounts can't create repos: swarm-controller sets their
disables push-to-create and the agent token doesn't have the Create repo-creation limit to 0 (see `docs/2-handbook/agents/security.md`), so
scope. an operator merge stays the only path to main. Ask the operator to
create the repo (for example `agents/<name>`) and add you as a
collaborator. Forgejo also disables push-to-create.
The CLI verbs below (`repo-create` / `repo-add-collaborator`) use the `repo-create` is for accounts that can create repos (operators,
agent's own forge token, so a repo lands under the agent's user account non-agent users). On an agent account it fails with a message naming
or an org the agent belongs to. the limit and the operator path.
**`repo-create <name>`** — create a repo under the authenticated user **`repo-create <name>`** — create a repo under the authenticated user
and print its URL. Key flags: and print its URL. Not available to agent accounts. Key flags:
- `--org <ORG>` — create under an org namespace instead - `--org <ORG>` — create under an org namespace instead
- `--private` — private repo (default: public) - `--private` — private repo (default: public)

View file

@ -110,7 +110,7 @@ enum Verb {
/// credentials autoinjected. Pairs with `pr create --agit`. /// credentials autoinjected. Pairs with `pr create --agit`.
Clone(verbs::clone::Args), Clone(verbs::clone::Args),
/// Create a forge repo under the current user (or `--org`). Prints /// Create a forge repo under the current user (or `--org`). Prints
/// the repo URL. /// the repo URL. Agent accounts can't create repos (limit 0).
RepoCreate(verbs::repo_create::Args), RepoCreate(verbs::repo_create::Args),
/// Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`) /// Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`)
/// with a permission level. Companion to `repo-create`. /// with a permission level. Companion to `repo-create`.

View file

@ -3,15 +3,19 @@
//! user (or under `--org`). Prints the new repo's web URL (or the full repo //! user (or under `--org`). Prints the new repo's web URL (or the full repo
//! object under `--json`). //! object under `--json`).
//! //!
//! Wraps `POST /api/v1/user/repos` (or `/orgs/{org}/repos`) so agents don't //! Wraps `POST /api/v1/user/repos` (or `/orgs/{org}/repos`). For accounts
//! reach for raw API calls when the operator greenlights a new repo — the //! that may create repos (operators, non-agent users). Agent accounts
//! instance has push-to-create disabled, so `git push` to a non-existent //! cannot: swarm-controller sets their repo-creation limit to 0, and
//! repo 403s. Pairs with `clone` (then push your content) and //! Forgejo reports that as a bare 500 on the user path, which
//! `repo-add-collaborator` (add peers to a fresh agent-namespace repo). //! `explain_create_error` turns into a message naming the operator path.
//! The instance has push-to-create disabled, so `git push` to a
//! non-existent repo 403s. Pairs with `clone` and `repo-add-collaborator`.
use anyhow::Result; use anyhow::Result;
use clap::Args as ClapArgs; use clap::Args as ClapArgs;
use forgejo_api::ForgejoError;
use forgejo_api::structs::CreateRepoOption; use forgejo_api::structs::CreateRepoOption;
use reqwest::StatusCode;
use crate::client::Client; use crate::client::Client;
use crate::verbs::print_json; use crate::verbs::print_json;
@ -40,6 +44,25 @@ pub struct Args {
auto_init: bool, auto_init: bool,
} }
/// Forgejo reports "user has reached maximum limit of repositories" as a
/// 500 the client keeps no body for, and `GET /user` does not expose
/// `max_repo_creation`, so a 500 on the user path is matched as "most
/// likely the creation limit". Every other error, and the org path,
/// passes through unchanged.
fn explain_create_error(org: Option<&str>, err: ForgejoError) -> anyhow::Error {
let at_limit = org.is_none()
&& matches!(&err, ForgejoError::UnexpectedStatusCode(s) if *s == StatusCode::INTERNAL_SERVER_ERROR);
if !at_limit {
return err.into();
}
anyhow::anyhow!(
"this account most likely cannot create repositories (the forge answered 500, which is \
how Forgejo reports a repo-creation limit of 0). On hyperhive, agent accounts are \
locked to 0 by design, so an operator merge stays the only path to main. Ask the \
operator to create the repo (for example `agents/<name>`) and add you as a collaborator."
)
}
/// # Errors /// # Errors
/// ///
/// Propagates any transport error from the Forgejo REST call (network /// Propagates any transport error from the Forgejo REST call (network
@ -62,10 +85,12 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
trust_model: None, trust_model: None,
}; };
let resp = match args.org.as_deref() { let org = args.org.as_deref();
Some(org) => client.api().create_org_repo(org, payload).send()?, let resp = match org {
None => client.api().create_current_user_repo(payload).send()?, Some(org) => client.api().create_org_repo(org, payload).send(),
}; None => client.api().create_current_user_repo(payload).send(),
}
.map_err(|e| explain_create_error(org, e))?;
if client.json_mode() { if client.json_mode() {
return print_json(&serde_json::to_value(&resp)?); return print_json(&serde_json::to_value(&resp)?);
@ -76,3 +101,40 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
} }
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
fn status(code: StatusCode) -> ForgejoError {
ForgejoError::UnexpectedStatusCode(code)
}
#[test]
fn user_path_500_names_the_limit_and_the_operator_path() {
let msg = explain_create_error(None, status(StatusCode::INTERNAL_SERVER_ERROR)).to_string();
assert!(msg.contains("cannot create repositories"), "{msg}");
assert!(msg.contains("locked to 0"), "{msg}");
assert!(msg.contains("add you as a collaborator"), "{msg}");
}
#[test]
fn other_user_path_errors_pass_through() {
for code in [
StatusCode::FORBIDDEN,
StatusCode::CONFLICT,
StatusCode::BAD_GATEWAY,
] {
let msg = explain_create_error(None, status(code)).to_string();
assert_eq!(msg, status(code).to_string());
}
}
#[test]
fn org_path_errors_pass_through() {
for code in [StatusCode::FORBIDDEN, StatusCode::INTERNAL_SERVER_ERROR] {
let msg = explain_create_error(Some("acme"), status(code)).to_string();
assert_eq!(msg, status(code).to_string());
}
}
}