fix(runreport): resolve "failed" label id by name instead of hardcoding 1
All checks were successful
release / build-and-release (push) Successful in 35s

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 <noreply@anthropic.com>
This commit is contained in:
X9 Dev 2026-07-28 18:11:15 +02:00
parent ef56410139
commit 65c98ebd81
2 changed files with 55 additions and 10 deletions

View file

@ -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 <host> - <version> - <date>`, label `failed`, body = step table + tail-capped Deploy.log (60 KB)
- Title `FAILED <host> - <version> - <date>`, 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

View file

@ -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 {