fix(forge): set mirror_interval so pull-mirrors sync periodically, not on-access

Forgejo's default mirror behaviour syncs on every git access, which
re-introduces external DNS latency at clone time. The hive-ci runner
shares the host netns, so a host-resolver blip turns an otherwise
local clone into a hard failure (data.forgejo.org DNS lookup that
has nothing to do with the repo being cloned).

Fix:
- New MIRROR_INTERVAL const (8h0m0s) used in ensure_mirror_repo.
- On creation: pass interval=MIRROR_INTERVAL in the migrate API body.
- On existing mirror: PATCH mirror_interval on every startup so repos
  seeded before this change converge without manual intervention.

A stale mirror (up to 8 hours old) is fine for CI; a broken clone
from a transient DNS blip is not. The actions/checkout mirror is
refreshed periodically rather than triggered by runner pulls.
This commit is contained in:
atlas 2026-07-03 19:32:05 +02:00
commit 9496fb060b

View file

@ -977,20 +977,41 @@ async fn ensure_mirrors(admin_token: &str) {
}
}
/// Periodic sync interval for pull-mirrors. Forgejo syncs mirrors
/// on-access by default, which re-introduces external DNS latency on
/// every `git clone` (the hive-ci runner shares the host netns and is
/// therefore affected by host resolver blips). A fixed periodic interval
/// isolates CI from transient DNS failures — a stale mirror is
/// acceptable; a broken clone because of a momentary DNS blip is not.
const MIRROR_INTERVAL: &str = "8h0m0s";
/// Create `owner/repo` as a pull-mirror of `upstream` via the migrate API.
/// Idempotent: a cheap existence check skips an already-seeded mirror (it
/// persists in the non-ephemeral forge state across reboots); a 409 (a race
/// between that check and the POST) is also treated as success.
/// Idempotent: if the repo already exists this function patches its
/// `mirror_interval` to ensure it matches (covers mirrors that were
/// created before the interval was introduced). A 409 on the migrate
/// POST (a race between the GET check and the POST) is also success.
async fn ensure_mirror_repo(
upstream: &str,
owner: &str,
repo: &str,
admin_token: &str,
) -> Result<()> {
let get_url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}");
let (status, _) = forge_http(reqwest::Method::GET, &get_url, admin_token, "").await?;
let repo_url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}");
let (status, _) = forge_http(reqwest::Method::GET, &repo_url, admin_token, "").await?;
if status.is_success() {
tracing::debug!(%owner, %repo, "forge: pull-mirror already present");
// Mirror already present. Patch interval so mirrors seeded before
// this field was introduced (or with a different value) converge.
let patch_body = serde_json::json!({ "mirror_interval": MIRROR_INTERVAL }).to_string();
let (patch_status, patch_text) =
forge_http(reqwest::Method::PATCH, &repo_url, admin_token, &patch_body).await?;
if patch_status.is_success() {
tracing::debug!(%owner, %repo, interval = MIRROR_INTERVAL, "forge: pull-mirror interval updated");
} else {
tracing::warn!(
%owner, %repo, status = %patch_status, body = %patch_text,
"forge: failed to set mirror_interval on existing pull-mirror"
);
}
return Ok(());
}
// serde_json::json! → the upstream URL is escaped safely (no string
@ -1000,6 +1021,9 @@ async fn ensure_mirror_repo(
"repo_owner": owner,
"repo_name": repo,
"mirror": true,
// Periodic refresh instead of on-access sync — keeps CI isolated
// from external DNS failures at clone time.
"interval": MIRROR_INTERVAL,
"service": "git",
"private": false,
})
@ -1008,7 +1032,7 @@ async fn ensure_mirror_repo(
let (status, text) = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?;
match status.as_u16() {
201 => {
tracing::info!(%owner, %repo, %upstream, "forge: created pull-mirror");
tracing::info!(%owner, %repo, %upstream, interval = MIRROR_INTERVAL, "forge: created pull-mirror");
Ok(())
}
// 409 = a race created it between our GET check and here (the GET