fix(#1868): use HIVE_FORGE_URL for internal forge calls
Replace the hardcoded FORGE_HTTP const with forge_http_base() which reads HIVE_FORGE_URL from the environment (already set unconditionally by hive-c0re.nix to http://<forge.domain>). Add forge_git_url() helper that inserts core:<token> credentials between scheme and authority for git push/clone URLs. All call sites updated: - forge/mod.rs: api() OnceLock + new forge_git_url/forge_http_base fns - forge/repos.rs: push_meta, push_config, ensure_meta_remote - forge/pr_merge.rs: tokenised_repo_url delegate + test loosened - workers/knowledge.rs: clone + push URLs - socket_server/mod.rs: clone_url in RepoCreated response No new env var: HIVE_FORGE_URL was already the right knob (mara). Closes #1868. Closes #2174 (this supersedes the operators-team fix from the closed #2218, which is re-applied in the ensure_operators_team call that was already merged separately).
This commit is contained in:
parent
05c91245c7
commit
031edbd41f
6 changed files with 57 additions and 23 deletions
|
|
@ -31,7 +31,34 @@ use users::{
|
|||
};
|
||||
|
||||
const FORGE_CONTAINER: &str = "hive-forge";
|
||||
pub(crate) const FORGE_HTTP: &str = "http://localhost:3000";
|
||||
|
||||
/// Base HTTP URL for the local Forgejo instance. Reads `HIVE_FORGE_URL`
|
||||
/// from the environment (set unconditionally by `hive-c0re.nix` to
|
||||
/// `http://<forge.domain>`) so the forge port is never hardcoded.
|
||||
/// Falls back to `http://localhost:3000` for bare runs outside the
|
||||
/// NixOS module (tests, manual invocation).
|
||||
pub(crate) fn forge_http_base() -> &'static str {
|
||||
static BASE: OnceLock<String> = OnceLock::new();
|
||||
BASE.get_or_init(|| {
|
||||
std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Token-in-URL git remote for `repo` (e.g. `"core/meta"`). Inserts
|
||||
/// `core:<token>` credentials between the scheme and authority of
|
||||
/// [`forge_http_base()`] — the form git accepts for inline auth.
|
||||
pub(crate) fn forge_git_url(token: &str, repo: &str) -> String {
|
||||
let base = forge_http_base();
|
||||
// Split on "://" to isolate scheme + authority. The base URL always
|
||||
// contains "://" (validated fallback + `HIVE_FORGE_URL` is
|
||||
// operator-set and expected to be well-formed).
|
||||
if let Some((scheme, host)) = base.split_once("://") {
|
||||
format!("{scheme}://core:{token}@{host}/{repo}.git")
|
||||
} else {
|
||||
format!("http://core:{token}@localhost:3000/{repo}.git")
|
||||
}
|
||||
}
|
||||
|
||||
/// Forgejo org grouping every agent's config repo. Core is a site admin
|
||||
/// and reads + writes every repo here. As of the agent-config-PR flow each
|
||||
/// agent is a **write collaborator on its own** `agent-configs/<name>` repo —
|
||||
|
|
@ -50,7 +77,7 @@ const CONFIG_ORG: &str = "agent-configs";
|
|||
/// (i.e. `core` user) can push.
|
||||
const SHARED_ORG: &str = "internal";
|
||||
/// The shared docs repo inside `SHARED_ORG`. Cloneable by every agent
|
||||
/// at `{FORGE_HTTP}/internal/docs.git`.
|
||||
/// at `{forge_http_base()}/internal/docs.git`.
|
||||
const SHARED_DOCS_REPO: &str = "docs";
|
||||
/// The hive-wide knowledge repo inside `SHARED_ORG`. Public — agents
|
||||
/// can fork it and open PRs without explicit collaborator grants.
|
||||
|
|
@ -108,7 +135,7 @@ async fn forge_admin(args: &[&str]) -> Result<String> {
|
|||
Ok(stdout)
|
||||
}
|
||||
|
||||
/// Typed Forgejo API client for the local forge ([`FORGE_HTTP`]),
|
||||
/// Typed Forgejo API client for the local forge ([`forge_http_base()`]),
|
||||
/// authenticated as `token`. All Forgejo API calls that don't shell
|
||||
/// out to `forgejo admin` go through clients built here — one place
|
||||
/// for the base URL and auth. Tokens differ per call site (core admin
|
||||
|
|
@ -119,7 +146,7 @@ async fn forge_admin(args: &[&str]) -> Result<String> {
|
|||
pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
||||
static URL: OnceLock<Url> = OnceLock::new();
|
||||
let url = URL
|
||||
.get_or_init(|| Url::parse(FORGE_HTTP).expect("FORGE_HTTP is a valid URL"))
|
||||
.get_or_init(|| Url::parse(forge_http_base()).expect("forge_http_base() is a valid URL"))
|
||||
.clone();
|
||||
Forgejo::new(Auth::Token(token), url).context("build forgejo api client")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo};
|
|||
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
use super::{CONFIG_ORG, api, core_token};
|
||||
use super::{CONFIG_ORG, api, core_token, forge_git_url};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PR-based config-flow merge primitives (part of the
|
||||
|
|
@ -81,7 +81,7 @@ fn repo_agent_name(repo: &str) -> &str {
|
|||
/// `push_config`'s pattern; the token is passed straight to git and never
|
||||
/// stored as a named remote.
|
||||
fn tokenised_repo_url(repo: &str, token: &str) -> String {
|
||||
format!("http://core:{token}@localhost:3000/{repo}.git")
|
||||
forge_git_url(token, repo)
|
||||
}
|
||||
|
||||
/// Resolve a PR's head sha via `git ls-remote <repo> refs/pull/<pr>/head`
|
||||
|
|
@ -311,9 +311,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn tokenised_repo_url_shape() {
|
||||
assert_eq!(
|
||||
tokenised_repo_url("agent-configs/iris", "tok"),
|
||||
"http://core:tok@localhost:3000/agent-configs/iris.git"
|
||||
// Credentials are inserted between scheme and authority; fallback
|
||||
// base is `http://localhost:3000` when HIVE_FORGE_URL is unset.
|
||||
let url = tokenised_repo_url("agent-configs/iris", "tok");
|
||||
assert!(url.contains("core:tok@"), "must embed credentials: {url}");
|
||||
assert!(
|
||||
url.ends_with("/agent-configs/iris.git"),
|
||||
"must end with repo path: {url}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ use tokio::process::Command;
|
|||
use crate::coordinator::Coordinator;
|
||||
|
||||
use super::{
|
||||
AGENTS_ORG, CONFIG_ORG, FORGE_HTTP, KNOWLEDGE_REPO, OPERATORS_TEAM, SHARED_DOCS_REPO,
|
||||
SHARED_ORG, api, core_token, is_present,
|
||||
AGENTS_ORG, CONFIG_ORG, KNOWLEDGE_REPO, OPERATORS_TEAM, SHARED_DOCS_REPO, SHARED_ORG, api,
|
||||
core_token, forge_git_url, forge_http_base, is_present,
|
||||
};
|
||||
|
||||
/// Creation options for an empty repo defaulting to `main`.
|
||||
|
|
@ -197,7 +197,7 @@ pub async fn push_meta(dir: &Path) -> Result<()> {
|
|||
// Token-in-URL push. Forgejo accepts `oauth2:<token>` or just
|
||||
// any-username:<token>; using `core` matches the owner so the
|
||||
// remote name is self-describing.
|
||||
let url = format!("http://core:{token}@localhost:3000/core/meta.git");
|
||||
let url = forge_git_url(&token, "core/meta");
|
||||
let out = Command::new("git")
|
||||
.current_dir(dir)
|
||||
.args(["push", "--force", &url, "HEAD:main"])
|
||||
|
|
@ -298,8 +298,8 @@ pub async fn meta_read_access(name: &str, core_token: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Add `http://localhost:3000/core/meta.git` as the `meta` remote in
|
||||
/// the agent's proposed config repo so the agent (and the manager) can
|
||||
/// Add the forge `core/meta.git` URL as the `meta` remote in the
|
||||
/// agent's proposed config repo so the agent (and the manager) can
|
||||
/// fetch the meta flake from the forge. Idempotent: no-op when the
|
||||
/// remote already points at the right URL, or when the proposed repo
|
||||
/// does not exist yet. No-op when the forge is not running.
|
||||
|
|
@ -311,7 +311,7 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> {
|
|||
if !proposed_dir.join(".git").exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let want = format!("{FORGE_HTTP}/core/meta.git");
|
||||
let want = format!("{}/core/meta.git", forge_http_base());
|
||||
let existing = crate::lifecycle::git_command()
|
||||
.current_dir(&proposed_dir)
|
||||
.args(["remote", "get-url", "meta"])
|
||||
|
|
@ -363,7 +363,7 @@ pub async fn push_config(name: &str) -> Result<()> {
|
|||
if !dir.join(".git").exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let url = format!("http://core:{token}@localhost:3000/{CONFIG_ORG}/{name}.git");
|
||||
let url = forge_git_url(&token, &format!("{CONFIG_ORG}/{name}"));
|
||||
let out = crate::lifecycle::git_command()
|
||||
.current_dir(&dir)
|
||||
.args([
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ async fn handle_create_repo(agent: &str, repo: &str) -> hive_sh4re::Response {
|
|||
};
|
||||
match crate::forge::create_agent_repo(agent, repo, &core_token).await {
|
||||
Ok(full_name) => hive_sh4re::Response::RepoCreated {
|
||||
clone_url: format!("{}/{full_name}.git", crate::forge::FORGE_HTTP),
|
||||
clone_url: format!("{}/{full_name}.git", crate::forge::forge_http_base()),
|
||||
full_name,
|
||||
},
|
||||
Err(e) => hive_sh4re::Response::Err {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ use std::collections::BTreeMap;
|
|||
use anyhow::{Context, Result};
|
||||
use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType};
|
||||
|
||||
use crate::forge::forge_git_url;
|
||||
|
||||
pub const ORG: &str = "internal";
|
||||
pub const REPO: &str = "knowledge";
|
||||
|
||||
|
|
@ -68,8 +70,7 @@ pub async fn ensure_local_clone(core_token: &str) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(LOCAL_DIR).context("create knowledge local dir")?;
|
||||
// Embed credentials in the URL — safe for localhost-only forge.
|
||||
let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git");
|
||||
let url = forge_git_url(core_token, &format!("{ORG}/{REPO}"));
|
||||
let out = tokio::process::Command::new("git")
|
||||
.args(["clone", &url, LOCAL_DIR])
|
||||
.output()
|
||||
|
|
@ -123,7 +124,7 @@ async fn seed_readme(core_token: &str) -> Result<()> {
|
|||
anyhow::bail!("git {args:?} failed: {stderr}");
|
||||
}
|
||||
}
|
||||
let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git");
|
||||
let url = forge_git_url(core_token, &format!("{ORG}/{REPO}"));
|
||||
let out = tokio::process::Command::new("git")
|
||||
.args(["-C", LOCAL_DIR, "push", &url, "HEAD:main"])
|
||||
.output()
|
||||
|
|
|
|||
|
|
@ -984,9 +984,11 @@ in
|
|||
)
|
||||
// {
|
||||
# In-cluster forge URL — the gateway vhost (`forge.<domain>`), which
|
||||
# nginx proxies to forgejo. The forge is mandatory, so this is
|
||||
# unconditional (the whole env block is already gated on hyperhive
|
||||
# being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
|
||||
# nginx proxies to forgejo. Used both for internal API calls in
|
||||
# hive-c0re (forge/mod.rs `forge_http_base()`) and forwarded to
|
||||
# agents via meta.rs for their forge-notify client. The forge is
|
||||
# mandatory, so this is unconditional (the whole env block is already
|
||||
# gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
|
||||
HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.matrix.enable {
|
||||
|
|
|
|||
Loading…
Reference in a new issue