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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user