fix(#1012): guard ensure_user_email with marker; add login_name to PATCH body

sync_agent called ensure_user_email on every tick, which sent
PATCH /api/v1/admin/users/{name} without the required login_name
field. Forgejo's EditUserOption handler was resetting use_custom_avatar
to false on each call, clobbering the avatar uploaded by the
forge-avatar-sync one-shot service.

Two-part fix:
- add login_name to the PATCH body (Forgejo EditUserOption requires it)
- write EMAIL_ALIGNED_MARKER_PREFIX{name} on first success; skip the
  PATCH on all subsequent sync_agent ticks

Marker follows the same one-shot pattern as CORE_AVATAR_MARKER and
CONFIG_ORG_AVATAR_MARKER. Delete the marker to force re-alignment.
This commit is contained in:
atlas 2026-06-03 10:23:58 +02:00 committed by mara
commit 0850128090

View file

@ -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