143 lines
5.6 KiB
YAML
143 lines
5.6 KiB
YAML
name: Auto-label issues
|
|
|
|
on:
|
|
issues:
|
|
types: [opened, edited]
|
|
|
|
concurrency:
|
|
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
label:
|
|
runs-on: mac-ci
|
|
container:
|
|
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
|
|
|
steps:
|
|
- name: Apply labels from Type/State/Category
|
|
env:
|
|
# IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
|
|
FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
|
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
|
REPO_FULL: ${{ gitea.repository }}
|
|
EVENT_PATH: ${{ github.event_path }}
|
|
NODE_OPTIONS: --dns-result-order=ipv4first
|
|
run: |
|
|
python3 - <<'PY'
|
|
import json, os, re, time, urllib.request, urllib.error, socket
|
|
|
|
forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
|
|
if not forge:
|
|
raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")
|
|
|
|
token = os.environ.get("FORGE_TOKEN") or ""
|
|
if not token:
|
|
raise SystemExit("Missing secret FORGE_TOKEN")
|
|
|
|
owner, repo = os.environ["REPO_FULL"].split("/", 1)
|
|
event_path = os.environ["EVENT_PATH"]
|
|
|
|
with open(event_path, "r", encoding="utf-8") as f:
|
|
ev = json.load(f)
|
|
|
|
issue = ev.get("issue") or {}
|
|
title = issue.get("title") or ""
|
|
body = issue.get("body") or ""
|
|
number = issue.get("number") or issue.get("index")
|
|
if not number:
|
|
raise SystemExit("No issue number/index in event payload")
|
|
|
|
def pick_line(key: str) -> str:
|
|
m = re.search(rf"^\s*{re.escape(key)}\s*:\s*([^\n\r]+)", body, flags=re.M)
|
|
return m.group(1).strip() if m else ""
|
|
|
|
desired = set()
|
|
|
|
t = pick_line("Type")
|
|
s = pick_line("State")
|
|
c = pick_line("Category")
|
|
|
|
print("PARSED:", {"Type": t, "State": s, "Category": c})
|
|
|
|
# 1) explicite depuis le body
|
|
if t: desired.add(t)
|
|
if s: desired.add(s)
|
|
if c: desired.add(c)
|
|
|
|
# 2) fallback depuis le titre si Type absent
|
|
if not t:
|
|
if title.startswith("[Correction]"):
|
|
desired.add("type/correction")
|
|
elif title.startswith("[Fact-check]") or title.startswith("[Vérification]"):
|
|
desired.add("type/fact-check")
|
|
|
|
# 3) fallback State si absent
|
|
if not s:
|
|
if "type/fact-check" in desired:
|
|
desired.add("state/a-sourcer")
|
|
elif "type/correction" in desired:
|
|
desired.add("state/recevable")
|
|
|
|
if not desired:
|
|
print("No labels to apply.")
|
|
raise SystemExit(0)
|
|
|
|
api = forge + "/api/v1"
|
|
headers = {
|
|
"Authorization": f"token {token}",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "archicratie-auto-label/1.1",
|
|
}
|
|
|
|
def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
|
|
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
|
last_err = None
|
|
for i in range(retries):
|
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
b = r.read()
|
|
return json.loads(b.decode("utf-8")) if b else None
|
|
except urllib.error.HTTPError as e:
|
|
b = e.read().decode("utf-8", errors="replace")
|
|
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
|
|
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
|
|
last_err = e
|
|
# retry only on network/timeout
|
|
time.sleep(backoff * (i + 1))
|
|
raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")
|
|
|
|
# labels repo
|
|
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
|
|
name_to_id = {x.get("name"): x.get("id") for x in labels}
|
|
|
|
missing = [x for x in desired if x not in name_to_id]
|
|
if missing:
|
|
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))
|
|
|
|
wanted_ids = sorted({int(name_to_id[x]) for x in desired})
|
|
|
|
# labels actuels de l'issue
|
|
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
|
|
current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}
|
|
|
|
final_ids = sorted(current_ids.union(wanted_ids))
|
|
|
|
# Replace labels = union (n'enlève rien)
|
|
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
|
|
|
|
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
|
|
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
|
|
|
|
# vérif post-apply (anti "timeout mais appliqué")
|
|
post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
|
|
post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}
|
|
|
|
missing_ids = [i for i in wanted_ids if i not in post_ids]
|
|
if missing_ids:
|
|
raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")
|
|
|
|
print(f"OK labels #{number}: {sorted(desired)}")
|
|
PY |