diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index d3220428..558cc786 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -28,6 +28,12 @@ const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set"; /// Sibling marker for the `agent-configs` org avatar. Same one-shot /// semantics — delete to force the upload to re-run. const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set"; +/// Per-agent marker written once the account email has been aligned to +/// `{name}@hyperhive`. Skips the `PATCH /api/v1/admin/users/{name}` +/// call on every subsequent `sync_agent` tick — that PATCH was resetting +/// Forgejo's `use_custom_avatar` flag and clobbering the avatar uploaded +/// by `forge-avatar-sync` (#1012). Delete to force re-alignment. +const EMAIL_ALIGNED_MARKER_PREFIX: &str = "/var/lib/hyperhive/forge-email-aligned-"; // Avatar PNGs are loaded at runtime from // `$HIVE_ASSETS_DIR/branding/{hyperhive,agent-configs}.png` via the // helpers in `hive_sh4re::assets`. The `agent-configs.png` is @@ -231,27 +237,38 @@ async fn change_user_password(name: &str, password: &str) -> Result<()> { /// Idempotently align the Forgejo account email to `agent_email(name)`. /// Existing agents were created with `{name}@hive.local`; this corrects -/// that so git commits (which use `{name}@hyperhive.local`) link to profiles. +/// that so git commits (which use `{name}@hyperhive`) link to profiles. /// Best-effort: failures are warned, not propagated. /// +/// Marker-guarded: writes `EMAIL_ALIGNED_MARKER_PREFIX{name}` on first +/// success and skips the PATCH on all subsequent calls. This prevents +/// Forgejo's admin-user-edit endpoint from resetting `use_custom_avatar` +/// on every `sync_agent` tick (root cause of #1012 — wrong forge avatar +/// after container rebuild). Delete the marker to force re-alignment. +/// /// Uses the admin REST API (`PATCH /api/v1/admin/users/{name}`) rather /// than `forgejo admin user edit` because the CLI dropped the `edit` -/// subcommand somewhere between forgejo 8 and current (the bare CLI -/// surfaced as "flag provided but not defined: -username"). Body sets -/// `source_id = 0` (local auth, the default for users hive-c0re -/// creates) which forgejo's PATCH validator requires even when the -/// only thing changing is the email. +/// subcommand somewhere between forgejo 8 and current. Body includes +/// `login_name` (required by Forgejo's `EditUserOption` validator) and +/// `source_id = 0` (local auth, the default for users hive-c0re creates). async fn ensure_user_email(name: &str) { + let marker = format!("{EMAIL_ALIGNED_MARKER_PREFIX}{name}"); + if std::path::Path::new(&marker).exists() { + return; + } let Some(token) = core_token() else { tracing::debug!(%name, "forge: skipping ensure_user_email — no core token yet"); return; }; let email = agent_email(name); - let body = format!(r#"{{"email":"{email}","source_id":0}}"#); + // `login_name` is required by Forgejo's EditUserOption validator. + // Omitting it caused Forgejo to reset use_custom_avatar on each call (#1012). + 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() => { - tracing::debug!(%name, %email, "forge: user email aligned"); + std::fs::write(&marker, "").ok(); + tracing::info!(%name, %email, "forge: user email aligned"); } Ok(status) if status == reqwest::StatusCode::FORBIDDEN => { // Core token missing admin scope — see