d3659534ef
- create_issue.py: argparse single-issue creator mirroring create_pr.py
(--remote {dadeschools,prgs}, --title/--body/--body-file, host overrides),
replacing the one-shot hardcoded ROADMAP backfill batch.
- manage_labels.py: add the label set + mapping tool (executable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
4.1 KiB
Python
Executable File
129 lines
4.1 KiB
Python
Executable File
#!/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()
|