Files
KDE-x86_64-v4-Fedora/ci/sync-copr-packages.py
funkemunky 5ee9499dcb
Validate Fedora x86_64-v3 Copr SRPMs (Gitea) / prepare (push) Successful in 3s
Validate Fedora x86_64-v3 Copr SRPMs (Gitea) / validate-srpms (push) Failing after 1s
Fixing sync
2026-04-27 12:25:28 -04:00

256 lines
8.2 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import subprocess
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Register or update Copr SCM package definitions that use this "
"repository's .copr/Makefile and optionally submit builds."
)
)
parser.add_argument("--project", required=True, help="Copr project, e.g. user/project")
parser.add_argument("--clone-url", required=True, help="Git clone URL Copr should use for this repository")
parser.add_argument("--commit", default="HEAD", help="Git ref Copr should build from")
parser.add_argument("--chroot", default="fedora-43-x86_64", help="Copr chroot to configure and build for")
parser.add_argument("--packages-file", default="packages.txt", help="Package list to register")
parser.add_argument("--specs-dir", default="SPECS", help="Directory containing repo-local package snapshots")
parser.add_argument(
"--package-input",
default="",
help="Optional comma, space, or newline separated package list. Overrides --packages-file when set.",
)
parser.add_argument(
"--macro-package-name",
default="copr-rpm-macros-x86-64-v3",
help="Name of the buildroot macro package in Copr",
)
parser.add_argument(
"--macro-package-spec",
default="packaging/copr-rpm-macros-x86-64-v3.spec",
help="Local spec reference for the buildroot macro package",
)
parser.add_argument("--webhook-rebuild", choices=("on", "off"), default="off")
parser.add_argument("--max-builds", type=int, default=0)
parser.add_argument("--timeout", type=int, default=18000)
parser.add_argument("--skip-chroot-update", action="store_true")
parser.add_argument("--submit-macro-build", action="store_true")
parser.add_argument("--submit-package-builds", action="store_true")
parser.add_argument("--nowait-package-builds", action="store_true")
parser.add_argument(
"--copr-debug",
action="store_true",
help="Run copr-cli commands with --debug and include full output on failures.",
)
return parser.parse_args()
def run(command: list[str], *, quiet: bool = False) -> subprocess.CompletedProcess[str]:
kwargs: dict[str, object] = {
"check": True,
"text": True,
}
if quiet:
kwargs["stdout"] = subprocess.DEVNULL
kwargs["stderr"] = subprocess.DEVNULL
return subprocess.run(command, **kwargs)
def run_copr(args: argparse.Namespace, command: list[str]) -> subprocess.CompletedProcess[str]:
copr_command = command[:]
if args.copr_debug and len(copr_command) >= 1 and copr_command[0] == "copr":
copr_command.insert(1, "--debug")
try:
return subprocess.run(
copr_command,
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
stdout = (exc.stdout or "").strip()
stderr = (exc.stderr or "").strip()
details = []
if stdout:
details.append(f"stdout:\n{stdout}")
if stderr:
details.append(f"stderr:\n{stderr}")
output_block = "\n\n".join(details) if details else "(no output captured)"
raise RuntimeError(
"Copr command failed:\n"
f"{' '.join(copr_command)}\n\n"
f"{output_block}\n\n"
"If this includes 'Response is not in JSON format', the server likely "
"returned an HTML error page (auth issue, wrong project path/casing, or "
"temporary Copr outage). Re-run with --copr-debug for request details."
) from exc
def package_exists(project: str, package_name: str) -> bool:
result = subprocess.run(
["copr", "get-package", project, "--name", package_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
text=True,
)
return result.returncode == 0
def load_packages(packages_file: Path, package_input: str) -> list[str]:
if package_input.strip():
packages = [entry for entry in re.split(r"[\s,]+", package_input.strip()) if entry]
else:
packages = [
line.strip()
for line in packages_file.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
deduped: list[str] = []
seen: set[str] = set()
for package in packages:
if package not in seen:
deduped.append(package)
seen.add(package)
return deduped
def resolve_spec_ref(package_name: str, specs_dir: Path) -> str:
package_dir = specs_dir / package_name
package_named_spec = package_dir / f"{package_name}.spec"
if package_named_spec.is_file():
return package_named_spec.as_posix()
spec_files = sorted(package_dir.glob("*.spec"))
if len(spec_files) == 1:
return spec_files[0].as_posix()
if len(spec_files) > 1:
raise RuntimeError(
f"multiple spec files found for {package_name} under {package_dir}; "
"set an explicit spec path instead"
)
raise RuntimeError(f"no spec file found for {package_name} under {package_dir}")
def upsert_scm_package(
*,
args: argparse.Namespace,
project: str,
package_name: str,
spec_ref: str,
clone_url: str,
commit: str,
webhook_rebuild: str,
max_builds: int,
timeout: int,
) -> None:
action = "edit-package-scm" if package_exists(project, package_name) else "add-package-scm"
command = [
"copr",
action,
project,
"--name",
package_name,
"--clone-url",
clone_url,
"--method",
"make_srpm",
"--spec",
spec_ref,
]
if commit != "HEAD":
command.extend(["--commit", commit])
if webhook_rebuild != "off":
command.extend(["--webhook-rebuild", webhook_rebuild])
if max_builds != 0:
command.extend(["--max-builds", str(max_builds)])
if timeout != 18000:
command.extend(["--timeout", str(timeout)])
run_copr(args, command)
def submit_build(args: argparse.Namespace, project: str, package_name: str, chroot: str, *, nowait: bool) -> None:
command = [
"copr",
"build-package",
project,
"--name",
package_name,
"--chroot",
chroot,
]
if nowait:
command.append("--nowait")
run_copr(args, command)
def main() -> int:
args = parse_args()
packages = load_packages(Path(args.packages_file), args.package_input)
specs_dir = Path(args.specs_dir)
# Preflight to fail fast on auth/project typos before batch operations.
run_copr(args, ["copr", "get", args.project])
upsert_scm_package(
args=args,
project=args.project,
package_name=args.macro_package_name,
spec_ref=args.macro_package_spec,
clone_url=args.clone_url,
commit=args.commit,
webhook_rebuild=args.webhook_rebuild,
max_builds=args.max_builds,
timeout=args.timeout,
)
for package_name in packages:
upsert_scm_package(
args=args,
project=args.project,
package_name=package_name,
spec_ref=resolve_spec_ref(package_name, specs_dir),
clone_url=args.clone_url,
commit=args.commit,
webhook_rebuild=args.webhook_rebuild,
max_builds=args.max_builds,
timeout=args.timeout,
)
if not args.skip_chroot_update:
run_copr(
args,
[
"copr",
"edit-chroot",
f"{args.project}/{args.chroot}",
"--packages",
args.macro_package_name,
]
)
if args.submit_macro_build:
submit_build(args, args.project, args.macro_package_name, args.chroot, nowait=False)
if args.submit_package_builds:
for package_name in packages:
submit_build(
args,
args.project,
package_name,
args.chroot,
nowait=args.nowait_package_builds,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())