From ed63310b71471286e413333394e8c8f22c2c6d22 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Sun, 21 Jun 2026 16:59:48 -0400 Subject: [PATCH] feat: parameterize create_pr.py and add PRGS remote - argparse: --remote {dadeschools,prgs}, --title/--head/--base/--body, --body-file (or '-' for stdin), and --host/--org/--repo overrides. - REMOTES table: dadeschools (gitea.dadeschools.net/Contractor) and prgs (gitea.prgs.cc/Scaled-Tech-Consulting). - Print 'PR #N: ' on success; surface API error body on failure. - Fix credential parsing to split('=', 1) so tokens containing '=' work. Co-Authored-By: Claude Opus 4.8 (1M context) --- create_pr.py | 142 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 105 insertions(+), 37 deletions(-) diff --git a/create_pr.py b/create_pr.py index 40d0e30..c07f2f5 100755 --- a/create_pr.py +++ b/create_pr.py @@ -1,44 +1,112 @@ +#!/usr/bin/env python3 +"""Create a Gitea pull request. + +Parameterized over title/body/head/base and the target Gitea instance. +Two instances are known out of the box: + + dadeschools -> gitea.dadeschools.net / Contractor / Timesheet + prgs -> gitea.prgs.cc / Scaled-Tech-Consulting / Timesheet + +Auth is pulled from the macOS keychain via `git credential fill` for the +chosen host -- no tokens on the command line. + +Examples: + create_pr.py --remote dadeschools \\ + --title "Open generated PDF after creation (#7)" \\ + --head feat/7-open-pdf-2234ddf5 --base feat/4-5-24-validations \\ + --body "Closes #7" + + create_pr.py --remote prgs --title "Fix X" --head fix/x --body-file body.md + + # override any field of a known remote, or point at an arbitrary repo: + create_pr.py --host gitea.example.com --org Foo --repo Bar \\ + --title "..." --head topic +""" import sys import json -import urllib.request -import subprocess import base64 +import argparse +import subprocess +import urllib.request +import urllib.error -host = "gitea.dadeschools.net" -org = "Contractor" -repo = "Timesheet" - -p = subprocess.Popen(["git", "credential", "fill"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) -out, _ = p.communicate(f"protocol=https\nhost=gitea.dadeschools.net\n\n") - -user = "" -password = "" -for line in out.splitlines(): - if line.startswith("username="): - user = line.split("=")[1] - if line.startswith("password="): - password = line.split("=")[1] - -if not user or not password: - print("Could not get credentials") - sys.exit(1) - -url = f"https://{host}/api/v1/repos/{org}/{repo}/pulls" -data = { - "title": "feat: Support PTO, Sick, Holiday, and Unpaid days", - "body": "Closes #6", - "head": "feat/6-absence-categories", - "base": "main" +REMOTES = { + "dadeschools": {"host": "gitea.dadeschools.net", "org": "Contractor", + "repo": "Timesheet"}, + "prgs": {"host": "gitea.prgs.cc", "org": "Scaled-Tech-Consulting", + "repo": "Timesheet"}, } -req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers={ - "Content-Type": "application/json", -}) -auth_b64 = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("utf-8") -req.add_header("Authorization", f"Basic {auth_b64}") -try: - with urllib.request.urlopen(req) as response: - print(response.read().decode()) -except urllib.error.HTTPError as e: - print("Error:", e.read().decode()) +def get_credentials(host): + """Return (user, password) for `host` via `git credential fill`.""" + p = subprocess.Popen( + ["git", "credential", "fill"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, + ) + out, _ = p.communicate(f"protocol=https\nhost={host}\n\n") + user = password = "" + for line in out.splitlines(): + # split(maxsplit=1): tokens/passwords can themselves contain '='. + if line.startswith("username="): + user = line.split("=", 1)[1] + elif line.startswith("password="): + password = line.split("=", 1)[1] + return user, password + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Create a Gitea pull request.") + parser.add_argument("--remote", choices=sorted(REMOTES), default="dadeschools", + help="Known Gitea instance (default: dadeschools).") + parser.add_argument("--host", help="Override the Gitea host.") + parser.add_argument("--org", help="Override the owner/org.") + parser.add_argument("--repo", help="Override the repository.") + parser.add_argument("--title", required=True, help="PR title.") + parser.add_argument("--head", required=True, help="Source branch.") + parser.add_argument("--base", default="main", help="Target branch (default: main).") + parser.add_argument("--body", default="", help="PR body text.") + parser.add_argument("--body-file", help="Read PR body from this file ('-' for stdin).") + args = parser.parse_args(argv) + + profile = REMOTES[args.remote] + host = args.host or profile["host"] + org = args.org or profile["org"] + repo = args.repo or profile["repo"] + + body = args.body + if args.body_file: + if args.body_file == "-": + body = sys.stdin.read() + else: + with open(args.body_file, "r", encoding="utf-8") as fh: + body = fh.read() + + user, password = get_credentials(host) + if not user or not password: + print(f"Could not get credentials for {host} " + f"(no keychain entry? try a manual `git credential fill`).", + file=sys.stderr) + return 1 + + url = f"https://{host}/api/v1/repos/{org}/{repo}/pulls" + payload = {"title": args.title, "body": body, "head": args.head, "base": args.base} + req = urllib.request.Request( + url, data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + auth_b64 = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("utf-8") + req.add_header("Authorization", f"Basic {auth_b64}") + + try: + with urllib.request.urlopen(req) as response: + data = json.load(response) + print(f"PR #{data.get('number')}: {data.get('html_url')}") + return 0 + except urllib.error.HTTPError as e: + print(f"Error {e.code}: {e.read().decode()}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main())