fix(runreport): resolve "failed" label id by name instead of hardcoding 1
All checks were successful
release / build-and-release (push) Successful in 35s
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:
parent
ef56410139
commit
65c98ebd81
2 changed files with 55 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,9 +19,13 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// Forgejo API + web base for the runs tracker.
|
||||
// Forgejo API endpoints 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
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue