hive-forge repo-create: say the account can't create repos instead of a bare 500
This commit is contained in:
parent
5b319e0600
commit
6ec12e7d84
4 changed files with 83 additions and 19 deletions
|
|
@ -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>`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue