ci: harden repository bootstrap transport

This commit is contained in:
2026-07-23 00:09:43 +02:00
parent a10a01c903
commit 9788bdde0c
3 changed files with 475 additions and 10 deletions

View File

@@ -3,35 +3,136 @@ from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlsplit
ROOT = Path(__file__).resolve().parents[2]
REGISTERED_TRANSPORT = "registered"
PUBLIC_HTTPS_TRANSPORT = "public-https"
GITEA_SSH_PREFIX = "git@git.add-ideas.de:"
GITEA_HTTPS_PREFIX = "https://git.add-ideas.de/"
GITEA_REPOSITORY_PATH = re.compile(r"add-ideas/[a-z0-9][a-z0-9-]*[.]git")
def parse_args() -> argparse.Namespace:
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Clone GovOPlaN repositories listed in repositories.json.")
parser.add_argument("--check", action="store_true", help="Only report missing repositories.")
parser.add_argument("--parent", type=Path, help="Override checkout parent directory.")
return parser.parse_args()
parser.add_argument(
"--repo",
action="append",
default=[],
metavar="NAME",
help="Clone only this registered repository; repeat for more than one.",
)
parser.add_argument(
"--exclude-repo",
action="append",
default=[],
metavar="NAME",
help="Exclude this registered repository; repeat for more than one.",
)
parser.add_argument(
"--transport",
choices=(REGISTERED_TRANSPORT, PUBLIC_HTTPS_TRANSPORT),
default=REGISTERED_TRANSPORT,
help=(
"Clone registered remotes unchanged, or convert public "
"git.add-ideas.de SSH remotes to anonymous HTTPS."
),
)
return parser.parse_args(argv)
def main() -> int:
args = parse_args()
manifest = json.loads((ROOT / "repositories.json").read_text())
def clone_remote(remote: str, *, transport: str) -> str:
if transport == REGISTERED_TRANSPORT:
return remote
if transport != PUBLIC_HTTPS_TRANSPORT:
raise ValueError(f"unsupported repository transport: {transport}")
if remote.startswith(GITEA_SSH_PREFIX):
repository_path = remote.removeprefix(GITEA_SSH_PREFIX)
else:
parsed = urlsplit(remote)
if (
parsed.scheme not in {"https", "ssh"}
or parsed.hostname != "git.add-ideas.de"
or parsed.port is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
"public HTTPS bootstrap only accepts registered "
"git.add-ideas.de remotes"
)
if parsed.scheme == "https" and parsed.username is not None:
raise ValueError("public HTTPS bootstrap does not accept credentials")
if parsed.scheme == "ssh" and parsed.username != "git":
raise ValueError("registered Gitea SSH remotes must use the git account")
repository_path = parsed.path.removeprefix("/")
if GITEA_REPOSITORY_PATH.fullmatch(repository_path) is None:
raise ValueError("registered Gitea repository path is malformed")
return GITEA_HTTPS_PREFIX + repository_path
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
manifest = json.loads((ROOT / "repositories.json").read_text(encoding="utf-8"))
parent = args.parent or Path(manifest["default_parent"])
missing: list[dict[str, str]] = []
missing: list[tuple[dict[str, str], Path, str]] = []
repositories = manifest["repositories"]
names = [entry["name"] for entry in repositories]
if len(names) != len(set(names)):
raise ValueError("repository registry contains duplicate names")
requested = set(args.repo)
excluded = set(args.exclude_repo)
unknown = (requested | excluded) - set(names)
if unknown:
raise ValueError(f"unknown registered repositories: {', '.join(sorted(unknown))}")
overlap = requested & excluded
if overlap:
raise ValueError(
f"repositories cannot be selected and excluded: {', '.join(sorted(overlap))}"
)
for entry in manifest["repositories"]:
for entry in repositories:
if (requested and entry["name"] not in requested) or entry["name"] in excluded:
continue
repo = parent / entry["path"]
if repo.exists():
continue
missing.append(entry)
transport = entry.get("bootstrap_transport", args.transport)
remote = clone_remote(entry["remote"], transport=transport)
missing.append((entry, repo, remote))
environment = os.environ.copy()
environment.update(
{
"GIT_ASKPASS": "/bin/false",
"GIT_TERMINAL_PROMPT": "0",
}
)
for entry, repo, remote in missing:
print(f"missing: {entry['name']} -> {repo}")
if not args.check:
subprocess.run(["git", "clone", entry["remote"], str(repo)], check=True)
subprocess.run(
[
"git",
"-c",
"credential.helper=",
"clone",
"--",
remote,
str(repo),
],
check=True,
env=environment,
)
if args.check:
return 1 if missing else 0