#!/usr/bin/env python3 """Commit the given paths and push to the default branch. Pushing to main triggers `publish`. Needs the repository secrets TANGLED_CI_SSH_KEY and TANGLED_PUSH_URL. If main moved while the job ran, the commit is rebased onto it and the push retried. commit_and_push.py -m 'actions: sync' modpacks """ import argparse import os import shlex import subprocess import sys import tempfile from pathlib import Path PUSH_ATTEMPTS = 3 def git(*args: str, env: dict[str, str] | None = None, check: bool = True) -> subprocess.CompletedProcess: return subprocess.run(["git", *args], env=env, check=check) def commit_and_push(message: str, paths: list[str]) -> int: for name in ("TANGLED_CI_SSH_KEY", "TANGLED_PUSH_URL"): if not os.environ.get(name): print(f"{name} is not set; configure this Tangled repository secret", file=sys.stderr) return 1 git("config", "user.name", "tangled-ci[bot]") git("config", "user.email", "tangled-ci@noreply.invalid") git("add", "--", *paths) if git("diff", "--cached", "--quiet", check=False).returncode == 0: print("nothing to commit") return 0 git("commit", "-m", message) branch = os.environ.get("TANGLED_REPO_DEFAULT_BRANCH", "main") url = os.environ["TANGLED_PUSH_URL"] with tempfile.TemporaryDirectory(prefix="tangled-ci-") as tmp: key = Path(tmp) / "id_ed25519" key.touch(mode=0o600) key.write_text(os.environ["TANGLED_CI_SSH_KEY"].rstrip("\n") + "\n") ssh_opts = "-o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" env = { **os.environ, "GIT_SSH_COMMAND": f"ssh -i {shlex.quote(str(key))} {ssh_opts}", } for attempt in range(1, PUSH_ATTEMPTS + 1): if git("push", url, f"HEAD:refs/heads/{branch}", env=env, check=False).returncode == 0: return 0 if attempt == PUSH_ATTEMPTS: break print(f"push rejected (attempt {attempt}/{PUSH_ATTEMPTS}); rebasing onto {branch}", file=sys.stderr) git("pull", "--rebase", url, branch, env=env) print("push failed", file=sys.stderr) return 1 def main() -> int: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-m", "--message", default=os.environ.get("TANGLED_COMMIT_MESSAGE")) parser.add_argument("paths", nargs="+") args = parser.parse_args() if not args.message: parser.error("a commit message is required (-m or TANGLED_COMMIT_MESSAGE)") return commit_and_push(args.message, args.paths) if __name__ == "__main__": sys.exit(main())