fix: align forge user email to git user.email so commits link to profiles

agent users were created with {name}@hive.local but git commits use
{name}@hyperhive (set by meta::render_flake). forgejo matches by email,
so no profile link appeared on any commit.

- extract agent_email() helper returning {name}@hyperhive
- use it in ensure_user_exists (new users)
- add ensure_user_email() that runs gitea admin user edit to patch
  existing users; called from ensure_all on every startup sweep

Closes #64
This commit is contained in:
damocles 2026-05-20 16:24:01 +02:00 committed by Mara
parent 5c27ab9d13
commit a024ca65c0

View file

@ -131,6 +131,13 @@ fn extract_token(output: &str) -> Option<String> {
.map(str::to_owned) .map(str::to_owned)
} }
/// Canonical email address for a hive agent's Forgejo account.
/// Must match the `user.email` set by `meta::render_flake` so commits
/// by the agent link back to their Forgejo profile page.
fn agent_email(name: &str) -> String {
format!("{name}@hyperhive")
}
/// Ensure a forgejo user named `name` exists. Idempotent: forgejo /// Ensure a forgejo user named `name` exists. Idempotent: forgejo
/// returns a "user already exists" error which we treat as success. /// returns a "user already exists" error which we treat as success.
/// `admin` adds `--admin` (site admin) — used for the bootstrap /// `admin` adds `--admin` (site admin) — used for the bootstrap
@ -143,7 +150,7 @@ async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> {
name, name,
"--email", "--email",
]; ];
let email = format!("{name}@hive.local"); let email = agent_email(name);
args.push(&email); args.push(&email);
args.extend(["--random-password", "--must-change-password=false"]); args.extend(["--random-password", "--must-change-password=false"]);
if admin { if admin {
@ -171,6 +178,18 @@ async fn ensure_user_exists(name: &str, admin: bool) -> 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`) link to profiles.
/// Best-effort: failures are warned, not propagated.
async fn ensure_user_email(name: &str) {
let email = agent_email(name);
match forge_admin(&["user", "edit", "--username", name, "--email", &email]).await {
Ok(_) => tracing::debug!(%name, %email, "forge: user email aligned"),
Err(e) => tracing::warn!(%name, error = %e, "forge: could not align user email"),
}
}
/// Mint a fresh access token for `name` and persist it to /// Mint a fresh access token for `name` and persist it to
/// `<state>/forge-token` (0600). Token name is suffixed with a /// `<state>/forge-token` (0600). Token name is suffixed with a
/// monotonic clock so re-issuing doesn't collide with an existing /// monotonic clock so re-issuing doesn't collide with an existing
@ -496,6 +515,10 @@ pub async fn ensure_all() {
if let Err(e) = ensure_user_for(&name).await { if let Err(e) = ensure_user_for(&name).await {
tracing::warn!(%name, error = ?e, "forge: ensure_user failed"); tracing::warn!(%name, error = ?e, "forge: ensure_user failed");
} }
// Align email to match the git user.email set by meta::render_flake
// so commits link to the agent's Forgejo profile. Best-effort;
// also patches up agents created before this fix (old @hive.local).
ensure_user_email(&name).await;
// Mirror the agent's applied config repo into agent-configs. // Mirror the agent's applied config repo into agent-configs.
// ensure_config_repo is idempotent; push_config catches any // ensure_config_repo is idempotent; push_config catches any
// drift since the last run — e.g. the startup migration just // drift since the last run — e.g. the startup migration just