forge: ensure core/meta repo + mirror meta commits to forge

startup sweep adds ensure_repo('meta', core_token) after the orgs
so the first push isn't a 404. meta::git_commit now calls
forge::push_meta after every successful commit — token-in-URL
`git push http://core:$token@localhost:3000/core/meta.git` —
gated on the core token file existing (no-op when forge isn't
seeded). push failures log warn, don't bubble up.

no tea needed on the host; git is already on the hive-c0re service
PATH via /run/current-system/sw.
This commit is contained in:
müde 2026-05-17 01:52:00 +02:00
parent 68020a15c9
commit 600ed509f4
2 changed files with 95 additions and 1 deletions

View file

@ -231,6 +231,86 @@ async fn ensure_core_user_and_token() -> Result<String> {
Ok(raw.trim().to_owned()) Ok(raw.trim().to_owned())
} }
/// POST `/api/v1/user/repos` to create a repo in the authenticated
/// user's own namespace. `token` belongs to the user we want the
/// repo owned by (we use `core`'s token for `core/meta`). Idempotent:
/// HTTP 409 ("repository already exists") is treated as success.
pub async fn ensure_repo(name: &str, token: &str) -> Result<()> {
let body = format!(r#"{{"name":"{name}","auto_init":false,"private":true,"default_branch":"main"}}"#);
let url = format!("{FORGE_HTTP}/api/v1/user/repos");
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
.context("invoke curl POST /api/v1/user/repos")?;
let code = String::from_utf8_lossy(&out.stdout).trim().to_owned();
match code.as_str() {
"201" => {
tracing::info!(%name, "forge: created repo");
Ok(())
}
"409" | "422" => {
tracing::debug!(%name, "forge: repo already exists");
Ok(())
}
other => anyhow::bail!(
"POST /api/v1/user/repos name={name} returned HTTP {other}"
),
}
}
/// Read the persisted core token, or None when the forge isn't
/// seeded yet. Cheap — just a file read.
pub fn core_token() -> Option<String> {
std::fs::read_to_string(CORE_TOKEN_PATH)
.ok()
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
}
/// Push `dir` (the meta repo) to `core/meta` on the local forge.
/// Best-effort: returns Err which callers log + ignore. No-op when
/// the core token isn't present (forge not enabled).
pub async fn push_meta(dir: &Path) -> Result<()> {
let Some(token) = core_token() else {
return Ok(());
};
// 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 out = Command::new("git")
.current_dir(dir)
.args(["push", "--force", &url, "HEAD:main"])
.output()
.await
.context("invoke git push core/meta")?;
if !out.status.success() {
anyhow::bail!(
"git push core/meta failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
tracing::info!("forge: pushed meta to core/meta");
Ok(())
}
/// POST `/api/v1/orgs` to create an org named `name`. Idempotent: /// POST `/api/v1/orgs` to create an org named `name`. Idempotent:
/// HTTP 422 ("user already exists") is treated as success. /// HTTP 422 ("user already exists") is treated as success.
async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { async fn ensure_org(name: &str, admin_token: &str) -> Result<()> {
@ -297,6 +377,12 @@ pub async fn ensure_all() {
tracing::warn!(%org, error = ?e, "forge: ensure_org failed"); tracing::warn!(%org, error = ?e, "forge: ensure_org failed");
} }
} }
// Meta repo lives at core/meta — pushed from git_commit in
// meta.rs on every deploy/lock-update. Make sure it exists
// before the first push hits a 404.
if let Err(e) = ensure_repo("meta", token).await {
tracing::warn!(error = ?e, "forge: ensure_repo core/meta failed");
}
} }
let Ok(containers) = crate::lifecycle::list().await else { let Ok(containers) = crate::lifecycle::list().await else {
tracing::warn!("forge: nixos-container list failed; skipping user sweep"); tracing::warn!("forge: nixos-container list failed; skipping user sweep");

View file

@ -342,7 +342,15 @@ async fn git_commit(dir: &Path, message: &str) -> Result<()> {
message, message,
], ],
) )
.await .await?;
// Best-effort mirror to the bundled forge. No-op when the forge
// isn't seeded (no core token on disk); push failures log a warn
// but don't bubble up — a missing mirror shouldn't fail an
// otherwise successful deploy.
if let Err(e) = crate::forge::push_meta(dir).await {
tracing::warn!(error = ?e, "forge: meta push after commit failed (non-fatal)");
}
Ok(())
} }
async fn nix(dir: &Path, args: &[&str]) -> Result<()> { async fn nix(dir: &Path, args: &[&str]) -> Result<()> {