G11.3: bulk DR copy — every repo as a read-only mirror

Grant's plan: clone the whole account, let it circulate, reverse direction later
when things are clean. Right plan, with one change that matters.

Sampling 40 repos found 18 carrying deploy/release/publish workflows that
trigger on push: — roughly 63 across the account. Importing those writable with
Actions enabled would arm sixty-odd production deploy triggers on Veron 1, each
needing disarming by hand.

So the bulk goes in as READ-ONLY pull mirrors. A mirror cannot run Actions at
all, so this carries zero deploy risk, and Gitea syncs them itself with no
script and no timer. What you get is a complete, current second copy of the
whole account — the disaster-recovery half — with none of the execution risk.

Converting one to writable + CI stays a deliberate per-repo act: re-import,
review its workflows, disable the deploying ones. That judgement belongs at the
moment you want CI on that repo, not in bulk sixty times by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-12 22:40:52 -04:00
parent 89723c6ebd
commit ae6ca8b9d2

View File

@@ -93,6 +93,55 @@ def _api(method: str, path: str, body: dict | None = None) -> tuple[int, dict]:
return e.code, {"message": raw[:300]}
def all_repo_names() -> list[str]:
out = subprocess.run(
["gh", "repo", "list", GITHUB_OWNER, "--limit", "300",
"--json", "name,isArchived"],
capture_output=True, text=True, check=True,
)
return sorted(r["name"] for r in json.loads(out.stdout) if not r["isArchived"])
def import_everything_as_mirrors() -> int:
"""Bulk DR copy: every repo, as a READ-ONLY pull mirror.
Mirrors are the right shape for the bulk, and the reason is safety rather
than tidiness. Sampling 40 repos found **18 carrying deploy / release /
publish workflows that trigger on `push:`** — roughly 63 across the account.
Importing those as writable repos with Actions enabled would arm sixty-odd
production deploy triggers on Veron 1, each of which would then have to be
disarmed by hand.
A pull mirror cannot run Actions at all, so the bulk import carries **zero**
deploy risk, and Gitea does the syncing itself with no script and no timer.
What you get is a complete, current, second copy of the whole account.
Converting one to a writable CI repo is then a deliberate per-repo act:
delete, re-import with `mirror=false`, review its workflows, disable the
deploying ones. That is the moment to make that judgement — not in bulk,
sixty times, by accident.
"""
names = all_repo_names()
existing = 0
done = 0
failed = []
print(f"{len(names)} active repos on GitHub. Importing missing ones as read-only mirrors.\n")
for n in names:
status, _ = _api("GET", f"/repos/{OWNER}/{n}")
if status == 200:
existing += 1
continue
if import_repo(n, mirror=True):
done += 1
else:
failed.append(n)
print(f"\n already present: {existing}")
print(f" newly mirrored: {done}")
if failed:
print(f" FAILED ({len(failed)}): {', '.join(failed[:10])}")
return 1 if failed else 0
def list_candidates() -> None:
"""Private repos whose CI cannot run on GitHub at all."""
out = subprocess.run(
@@ -164,6 +213,9 @@ def main() -> int:
list_candidates()
return 0
if args.all_as_mirrors:
return import_everything_as_mirrors()
targets = SAFE_ORDER if args.safe_batch else args.repos
if not targets:
ap.error("name a repo, or pass --safe-batch / --list-candidates")