feat(#2072): auto-seed Forgejo pull-mirrors (DEFAULT_ACTIONS_URL=self for CI)
General-purpose mirror mechanism for the internal forge, per mara's call on #2074 (real Forgejo pull-mirrors, nix-configured — not a pushed clone). - services.hyperhive.forge.mirrors: list of { upstream, dest } pull-mirrors, any repo. Each is created as a real Forgejo pull-mirror (re-syncs from upstream), dest = <owner>/<repo> in its own org. - When forge.ci.enable is set: an actions/checkout mirror is auto-appended + forgejo DEFAULT_ACTIONS_URL is pointed at this instance, so CI 'uses: actions/checkout@vN' resolves on loopback — immune to a host-resolver blip that previously reded every checkout (the seed/re-sync needs external DNS, but that's off the CI critical path). - forgejo-seed-mirrors.service: host-side oneshot (the core admin token never enters a container), modelled on hive-ci-prefetch — waits <=60s for the core token, then idempotently ensures each dest org + creates the pull-mirror via the migrate API. partOf the forge container so it re-ensures on restart. - assertions: dest must be <owner>/<repo>; mirror orgs can't shadow the c0re-managed namespaces (config/shared/agents/core) so the seed never races hive-c0re's own provisioning. Supersedes #2074 (the raw-clone stopgap) as the durable #2072 fix.
This commit is contained in:
parent
b0d099274e
commit
4a3581a3d2
1 changed files with 129 additions and 0 deletions
|
|
@ -46,6 +46,82 @@ let
|
||||||
dest = "actions/checkout";
|
dest = "actions/checkout";
|
||||||
};
|
};
|
||||||
effectiveMirrors = cfg.mirrors ++ lib.optional ciEnabled actionCheckoutMirror;
|
effectiveMirrors = cfg.mirrors ++ lib.optional ciEnabled actionCheckoutMirror;
|
||||||
|
|
||||||
|
# Host-side core admin token hive-c0re mints after provisioning the forge
|
||||||
|
# admin (same file hive-ci-prefetch reads). Root-only; never enters a
|
||||||
|
# container — so the mirror seed runs host-side, exactly like
|
||||||
|
# hive-ci-prefetch, rather than minting a second token in-container.
|
||||||
|
coreTokenPath = "/var/lib/hyperhive/forge-core-token";
|
||||||
|
|
||||||
|
# Idempotently create each `effectiveMirrors` entry as a real Forgejo
|
||||||
|
# pull-mirror via the migrate API. Host-side: only talks to the forge on
|
||||||
|
# loopback (forgejo itself does the upstream clone, so the upstream-DNS
|
||||||
|
# dependency lives in the container + is off the CI critical path).
|
||||||
|
# Modelled on hive-ci-prefetch's wait-for-core-token loop.
|
||||||
|
seedMirrorsScript = pkgs.writeShellScript "forgejo-seed-mirrors" ''
|
||||||
|
set -uo pipefail
|
||||||
|
FORGE_URL="http://127.0.0.1:${toString cfg.httpPort}"
|
||||||
|
|
||||||
|
CORE_TOKEN=""
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if [ -f "${coreTokenPath}" ]; then CORE_TOKEN=$(cat "${coreTokenPath}"); break; fi
|
||||||
|
echo "forgejo-seed-mirrors: waiting for core token ($i/60)..." >&2
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ -z "''${CORE_TOKEN:-}" ]; then
|
||||||
|
echo "forgejo-seed-mirrors: core token absent after 60s — cannot seed mirrors" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
AUTH="Authorization: token $CORE_TOKEN"
|
||||||
|
rc=0
|
||||||
|
|
||||||
|
seed_one() {
|
||||||
|
upstream="$1"; owner="$2"; repo="$3"
|
||||||
|
# Ensure the dest org (idempotent: 201 created / 422 already exists).
|
||||||
|
ohttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "$AUTH" -H 'Content-Type: application/json' \
|
||||||
|
"$FORGE_URL/api/v1/orgs" -d "{\"username\":\"$owner\"}" || echo 000)
|
||||||
|
case "$ohttp" in
|
||||||
|
201 | 422) ;;
|
||||||
|
*) echo "forgejo-seed-mirrors: ensure org '$owner' returned HTTP $ohttp" >&2 ;;
|
||||||
|
esac
|
||||||
|
# Skip if the repo already exists (the mirror persists across reboots
|
||||||
|
# in the non-ephemeral forge state, so this no-ops on every reboot
|
||||||
|
# after the first).
|
||||||
|
rhttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "$AUTH" "$FORGE_URL/api/v1/repos/$owner/$repo" || echo 000)
|
||||||
|
if [ "$rhttp" = 200 ]; then
|
||||||
|
echo "forgejo-seed-mirrors: $owner/$repo already present — skipping" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Create the pull-mirror. service=git → generic git clone of
|
||||||
|
# clone_addr (no upstream API token needed); mirror=true → forgejo
|
||||||
|
# keeps it re-syncing on its mirror interval.
|
||||||
|
resp=$(${pkgs.curl}/bin/curl -s -w $'\n%{http_code}' -X POST \
|
||||||
|
-H "$AUTH" -H 'Content-Type: application/json' \
|
||||||
|
"$FORGE_URL/api/v1/repos/migrate" \
|
||||||
|
-d "{\"clone_addr\":\"$upstream\",\"repo_owner\":\"$owner\",\"repo_name\":\"$repo\",\"mirror\":true,\"service\":\"git\",\"private\":false}" \
|
||||||
|
|| printf '\n000')
|
||||||
|
mhttp=$(printf '%s' "$resp" | tail -n1)
|
||||||
|
case "$mhttp" in
|
||||||
|
2*) echo "forgejo-seed-mirrors: created pull-mirror $owner/$repo from $upstream" >&2 ;;
|
||||||
|
*)
|
||||||
|
echo "forgejo-seed-mirrors: migrate $owner/$repo failed HTTP $mhttp: $(printf '%s' "$resp" | sed '$d')" >&2
|
||||||
|
rc=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
${lib.concatMapStringsSep "\n" (
|
||||||
|
m:
|
||||||
|
let
|
||||||
|
parts = lib.splitString "/" m.dest;
|
||||||
|
in
|
||||||
|
"seed_one ${lib.escapeShellArg m.upstream} ${lib.escapeShellArg (builtins.elemAt parts 0)} ${lib.escapeShellArg (builtins.elemAt parts 1)}"
|
||||||
|
) effectiveMirrors}
|
||||||
|
|
||||||
|
exit $rc
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
||||||
|
|
@ -262,6 +338,37 @@ in
|
||||||
hostname like "forge.example.com" or "git.internal".
|
hostname like "forge.example.com" or "git.internal".
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# Each mirror dest must be exactly `<owner>/<repo>` — the seed
|
||||||
|
# splits on the single slash to create the org + repo.
|
||||||
|
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
|
||||||
|
message = ''
|
||||||
|
Every services.hyperhive.forge.mirrors[].dest must be exactly
|
||||||
|
"<owner>/<repo>" (one slash). Got: ${
|
||||||
|
lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# Keep mirror orgs out of the hive-c0re-managed namespaces
|
||||||
|
# (config/shared/agents/core) so the seed never races / collides
|
||||||
|
# with hive-c0re's own startup provisioning of those orgs.
|
||||||
|
assertion = lib.all (
|
||||||
|
m:
|
||||||
|
!(lib.elem (builtins.elemAt (lib.splitString "/" m.dest) 0) [
|
||||||
|
"config"
|
||||||
|
"shared"
|
||||||
|
"agents"
|
||||||
|
"core"
|
||||||
|
])
|
||||||
|
) effectiveMirrors;
|
||||||
|
message = ''
|
||||||
|
services.hyperhive.forge.mirrors[].dest must not place a mirror
|
||||||
|
in a hive-c0re-managed org (config / shared / agents / core) —
|
||||||
|
those are provisioned by hive-c0re and a mirror there would
|
||||||
|
collide. Use a dedicated org (e.g. "actions/checkout").
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
containers.hive-forge = {
|
containers.hive-forge = {
|
||||||
|
|
@ -474,5 +581,27 @@ in
|
||||||
cfg.sshPort
|
cfg.sshPort
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Seed the configured pull-mirrors once the forge + core token are up.
|
||||||
|
# Host-side (the core admin token never enters a container) and modelled
|
||||||
|
# on hive-ci-prefetch. partOf the forge container so it re-runs (and
|
||||||
|
# re-ensures, idempotently) on every forge (re)start. Only present when
|
||||||
|
# there's something to seed.
|
||||||
|
systemd.services.forgejo-seed-mirrors = lib.mkIf (effectiveMirrors != [ ]) {
|
||||||
|
description = "Seed Forgejo pull-mirrors (host-side)";
|
||||||
|
after = [
|
||||||
|
"hive-c0re.service"
|
||||||
|
"container@hive-forge.service"
|
||||||
|
];
|
||||||
|
wants = [ "container@hive-forge.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
partOf = [ "container@hive-forge.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = seedMirrorsScript;
|
||||||
|
SyslogIdentifier = "forgejo-seed-mirrors";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue