#!/usr/bin/env python3 """Create a label set on a Gitea repo and apply labels to issues. Auth follows the project convention: credentials are pulled from the macOS keychain via `git credential fill` (HTTPS), then sent as Basic auth. Usage: ./manage_labels.py # create labels, then apply the mapping below ./manage_labels.py --dry # print actions without writing """ import sys import json import base64 import subprocess import urllib.request import urllib.error HOST = "gitea.dadeschools.net" ORG = "Contractor" REPO = "Timesheet" # Mirror of the eAgenda label set (i18n omitted as irrelevant to Timesheet). LABELS = [ {"name": "chore", "color": "5319e7", "description": ""}, {"name": "critical", "color": "b60205", "description": ""}, {"name": "epic", "color": "8250df", "description": ""}, {"name": "important", "color": "fbca04", "description": ""}, {"name": "nice-to-have", "color": "0e8a16", "description": ""}, {"name": "status:in-progress", "color": "fefe2e", "description": "Issue is being worked on"}, ] # issue number -> label names to apply MAPPING = { 23: ["chore"], 22: ["chore"], 21: ["chore"], 20: ["chore", "status:in-progress"], 19: ["chore"], 18: ["chore"], 17: ["important"], 14: ["nice-to-have"], 13: ["nice-to-have"], 12: ["important"], 11: ["important"], 8: ["nice-to-have"], 7: ["nice-to-have"], 3: ["important"], 2: ["important"], 1: ["nice-to-have"], } API = f"https://{HOST}/api/v1/repos/{ORG}/{REPO}" def get_auth_header(): 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(): if line.startswith("username="): user = line.split("=", 1)[1] if line.startswith("password="): password = line.split("=", 1)[1] if not user or not password: print("Could not get credentials from git credential fill", file=sys.stderr) sys.exit(1) token = base64.b64encode(f"{user}:{password}".encode()).decode() return f"Basic {token}" def api(method, path, auth, payload=None): url = f"{API}{path}" data = json.dumps(payload).encode() if payload is not None else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Authorization", auth) req.add_header("Content-Type", "application/json") try: with urllib.request.urlopen(req) as r: body = r.read().decode() return json.loads(body) if body else None except urllib.error.HTTPError as e: print(f" HTTP {e.code} on {method} {path}: {e.read().decode()}", file=sys.stderr) return None def main(): dry = "--dry" in sys.argv auth = get_auth_header() # 1. Existing labels -> name:id existing = api("GET", "/labels?limit=100", auth) or [] by_name = {l["name"]: l["id"] for l in existing} # 2. Create missing labels for spec in LABELS: if spec["name"] in by_name: print(f"label exists: {spec['name']}") continue if dry: print(f"[dry] create label: {spec['name']} #{spec['color']}") continue created = api("POST", "/labels", auth, spec) if created: by_name[created["name"]] = created["id"] print(f"created label: {created['name']} (id {created['id']})") # 3. Apply mapping for issue, names in sorted(MAPPING.items(), reverse=True): ids = [by_name[n] for n in names if n in by_name] missing = [n for n in names if n not in by_name] if missing: print(f" #{issue}: skipping unknown labels {missing}", file=sys.stderr) if dry: print(f"[dry] #{issue} <- {names}") continue # PUT replaces the issue's labels with exactly this set (idempotent). res = api("PUT", f"/issues/{issue}/labels", auth, {"labels": ids}) if res is not None: applied = [l["name"] for l in res] print(f"#{issue} labeled: {applied}") if __name__ == "__main__": main()