From 9b1789a1641dad7c607370055bb39e8b884513c3 Mon Sep 17 00:00:00 2001 From: Archicratia Date: Mon, 2 Mar 2026 19:36:09 +0100 Subject: [PATCH] ci: fix auto-label (no array fallback, retries, post-verify) --- .gitea/workflows/auto-label-issues.yml | 84 +++++++++++++++++--------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/.gitea/workflows/auto-label-issues.yml b/.gitea/workflows/auto-label-issues.yml index 2a53ef6..d5e3272 100644 --- a/.gitea/workflows/auto-label-issues.yml +++ b/.gitea/workflows/auto-label-issues.yml @@ -4,22 +4,37 @@ 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: - FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }} + # 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, urllib.request, urllib.error + 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") - forge = os.environ["FORGE_BASE"].rstrip("/") - token = os.environ["FORGE_TOKEN"] owner, repo = os.environ["REPO_FULL"].split("/", 1) event_path = os.environ["EVENT_PATH"] @@ -46,12 +61,9 @@ jobs: 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) + 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: @@ -76,42 +88,56 @@ jobs: "Authorization": f"token {token}", "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "archicratie-auto-label/1.0", + "User-Agent": "archicratie-auto-label/1.1", } - def jreq(method, url, payload=None): + 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") - req = urllib.request.Request(url, data=data, headers=headers, method=method) - try: - with urllib.request.urlopen(req, timeout=20) 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 + 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") or [] + 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 = [name_to_id[x] for x in desired] + 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") or [] - current_ids = {x.get("id") for x in current if x.get("id") is not None} + 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)) - # set labels = union (n'enlève rien) + # Replace labels = union (n'enlève rien) url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels" - try: - jreq("PUT", url, {"labels": final_ids}) - except Exception: - jreq("PUT", url, final_ids) + + # 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 \ No newline at end of file -- 2.49.1