From 65c98ebd8137fabf4e321f4d4515cfe1e8549334 Mon Sep 17 00:00:00 2001 From: X9 Dev Date: Tue, 28 Jul 2026 18:11:15 +0200 Subject: [PATCH] fix(runreport): resolve "failed" label id by name instead of hardcoding 1 The label id was hardcoded (failedLabel = 1), so deleting/recreating the label in x9/xetup-runs (new id) would make issue creation drop the label or fail. Look the id up by name at runtime; on any lookup failure (network, auth, decode, or label absent) file the issue unlabeled rather than not at all. Verified end-to-end: label resolved and attached via lookup. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 2 +- internal/runreport/runreport.go | 63 ++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 376f426..b29c4f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,7 +164,7 @@ xetup.exe start ### Run failure reporting (runreport) - On any ERROR step, files an issue in the private `x9/xetup-runs` repo; successful runs stay silent -- Title `FAILED - - `, label `failed`, body = step table + tail-capped Deploy.log (60 KB) +- Title `FAILED - - `, label `failed` (id resolved by name at runtime), body = step table + tail-capped Deploy.log (60 KB) - Filed by the `xetup-bot` Forgejo account (collaborator on xetup-runs only) via a `write:issue` token - Token + version injected at build via `-ldflags -X internal/buildinfo.{RunsToken,Version}`; empty token -> no-op - Best-effort/non-blocking (15s HTTP timeout, 3 retries); the email report is the fallback diff --git a/internal/runreport/runreport.go b/internal/runreport/runreport.go index 5672405..b36337e 100644 --- a/internal/runreport/runreport.go +++ b/internal/runreport/runreport.go @@ -19,9 +19,13 @@ import ( ) const ( - // Forgejo API + web base for the runs tracker. - issuesAPI = "https://git.xetup.x9.cz/api/v1/repos/x9/xetup-runs/issues" - failedLabel = 1 // id of the "failed" label in x9/xetup-runs + // Forgejo API endpoints for the runs tracker. + issuesAPI = "https://git.xetup.x9.cz/api/v1/repos/x9/xetup-runs/issues" + labelsAPI = "https://git.xetup.x9.cz/api/v1/repos/x9/xetup-runs/labels?limit=50" + + // labelName is looked up by name at runtime (not a hardcoded id) so the + // label surviving a delete/recreate with a new id does not break filing. + labelName = "failed" // Cap the inline log so a huge Deploy.log does not bloat the issue; keep // the tail (the end is where the failure and its context live). @@ -52,17 +56,20 @@ func Report(results []report.StepResult, logPath string) string { title := fmt.Sprintf("FAILED %s - %s - %s", hostname, buildinfo.Version, now) body := buildBody(results, hostname, now, logPath) - payload, err := json.Marshal(map[string]any{ - "title": title, - "body": body, - "labels": []int{failedLabel}, - }) + client := &http.Client{Timeout: 15 * time.Second} + + // Look up the label id by name; on any failure file the issue unlabeled + // rather than not at all. + fields := map[string]any{"title": title, "body": body} + if ids := lookupLabelIDs(client); len(ids) > 0 { + fields["labels"] = ids + } + payload, err := json.Marshal(fields) if err != nil { fmt.Fprintf(os.Stderr, "[WARN] runreport: marshal failed: %v\n", err) return "" } - client := &http.Client{Timeout: 15 * time.Second} delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} for attempt, delay := range delays { if delay > 0 { @@ -107,6 +114,44 @@ func postIssue(client *http.Client, payload []byte) (string, error) { return out.HTMLURL, nil } +// lookupLabelIDs resolves labelName to its id in the runs repo. Returns nil on +// any error (network, auth, decode, or label absent) so the caller files the +// issue without a label instead of failing. +func lookupLabelIDs(client *http.Client) []int { + req, err := http.NewRequest(http.MethodGet, labelsAPI, nil) + if err != nil { + return nil + } + req.Header.Set("Authorization", "token "+buildinfo.RunsToken) + + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "[WARN] runreport: label lookup failed: %v\n", err) + return nil + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "[WARN] runreport: label lookup status %s\n", resp.Status) + return nil + } + + var labels []struct { + ID int `json:"id"` + Name string `json:"name"` + } + if err := json.NewDecoder(resp.Body).Decode(&labels); err != nil { + fmt.Fprintf(os.Stderr, "[WARN] runreport: label decode failed: %v\n", err) + return nil + } + for _, l := range labels { + if l.Name == labelName { + return []int{l.ID} + } + } + fmt.Fprintf(os.Stderr, "[WARN] runreport: label %q not found; filing unlabeled\n", labelName) + return nil +} + // buildBody renders the issue markdown: a summary line, a per-step table, and // the (tail-capped) Deploy.log in a collapsed block. func buildBody(results []report.StepResult, hostname, now, logPath string) string {