Commit e0ae4017ac

e0ae4017ac0c9e2bd4bfa459e13b29c32feb1d87

parent: 9002a0bf3f

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 06:30 UTC

feed: a labelled event names the labels

Ref #265

Layout: unified · split

internal/control/feedline.go +19 −5
@@ -21,7 +21,10 @@ type FeedLine struct {
21 WhenT time.Time // parsed from When, for ago/whenT rendering 21 WhenT time.Time // parsed from When, for ago/whenT rendering
22 State string // a build run's combined status; empty for anything else 22 State string // a build run's combined status; empty for anything else
23 Jobs []string // job names folded into a build run 23 Jobs []string // job names folded into a build run
24 sha string // the commit a build event fired on, for fold-matching 24 // Extra is trailing detail shown after the ref: the label list on a
25 // labelled event, empty for everything else.
26 Extra string
27 sha string // the commit a build event fired on, for fold-matching
25} 28}
26 29
27// FeedLines turns stored events into readable lines. An unknown kind 30// FeedLines turns stored events into readable lines. An unknown kind
@@ -41,10 +44,11 @@ func FeedLines(events []store.FeedEvent) []FeedLine {
41 statuses := make([][]string, 0, len(events)) 44 statuses := make([][]string, 0, len(events))
42 for _, e := range events { 45 for _, e := range events {
43 var d struct { 46 var d struct {
44 Number int64 `json:"number"` 47 Number int64 `json:"number"`
45 Job string `json:"job"` 48 Job string `json:"job"`
46 Tag string `json:"tag"` 49 Tag string `json:"tag"`
47 SHA string `json:"sha"` 50 SHA string `json:"sha"`
51 Labels []string `json:"labels"`
48 } 52 }
49 json.Unmarshal([]byte(e.Data), &d) 53 json.Unmarshal([]byte(e.Data), &d)
50 kind, rest, _ := strings.Cut(e.Kind, ".") 54 kind, rest, _ := strings.Cut(e.Kind, ".")
@@ -72,9 +76,15 @@ func FeedLines(events []store.FeedEvent) []FeedLine {
72 case "issue": 76 case "issue":
73 l.Verb, l.Ref = issueVerb(rest), fmt.Sprintf("#%d", d.Number) 77 l.Verb, l.Ref = issueVerb(rest), fmt.Sprintf("#%d", d.Number)
74 l.URL = fmt.Sprintf("/%s/issues/%d", e.RepoPath, d.Number) 78 l.URL = fmt.Sprintf("/%s/issues/%d", e.RepoPath, d.Number)
79 if rest == "labeled" {
80 l.Extra = strings.Join(d.Labels, ", ")
81 }
75 case "mr": 82 case "mr":
76 l.Verb, l.Ref = mrVerb(rest), fmt.Sprintf("!%d", d.Number) 83 l.Verb, l.Ref = mrVerb(rest), fmt.Sprintf("!%d", d.Number)
77 l.URL = fmt.Sprintf("/%s/mrs/%d", e.RepoPath, d.Number) 84 l.URL = fmt.Sprintf("/%s/mrs/%d", e.RepoPath, d.Number)
85 if rest == "labeled" {
86 l.Extra = strings.Join(d.Labels, ", ")
87 }
78 case "build": 88 case "build":
79 l.Verb, l.Ref = "build "+rest, d.Job 89 l.Verb, l.Ref = "build "+rest, d.Job
80 l.URL = fmt.Sprintf("/%s/builds/%d", e.RepoPath, d.Number) 90 l.URL = fmt.Sprintf("/%s/builds/%d", e.RepoPath, d.Number)
@@ -148,6 +158,8 @@ func issueVerb(s string) string {
148 return "reopened issue" 158 return "reopened issue"
149 case "commented": 159 case "commented":
150 return "commented on" 160 return "commented on"
161 case "labeled":
162 return "labelled"
151 } 163 }
152 return "issue " + s 164 return "issue " + s
153} 165}
@@ -162,6 +174,8 @@ func mrVerb(s string) string {
162 return "commented on" 174 return "commented on"
163 case "closed": 175 case "closed":
164 return "closed merge request" 176 return "closed merge request"
177 case "labeled":
178 return "labelled"
165 } 179 }
166 return "merge request " + s 180 return "merge request " + s
167} 181}
internal/control/feedline_test.go +29
@@ -158,6 +158,35 @@ func TestFeedLinesParsesWhenT(t *testing.T) {
158 } 158 }
159} 159}
160 160
161// A labelled issue names which labels were added, not just "issue
162// labeled": the sentence is otherwise silent about what changed.
163func TestFeedLinesNamesTheLabelsOnALabelledIssue(t *testing.T) {
164 events := []store.FeedEvent{
165 {RepoPath: "krz/gitbay", Actor: "cmc", Kind: "issue.labeled",
166 Data: `{"number":262,"labels":["ops","security"]}`},
167 }
168 lines := FeedLines(events)
169 if len(lines) != 1 {
170 t.Fatalf("FeedLines returned %d lines, want 1", len(lines))
171 }
172 l := lines[0]
173 if l.Verb != "labelled" || l.Ref != "#262" || l.Extra != "ops, security" {
174 t.Errorf("got %+v", l)
175 }
176}
177
178// Same for a labelled merge request.
179func TestFeedLinesNamesTheLabelsOnALabelledMR(t *testing.T) {
180 events := []store.FeedEvent{
181 {RepoPath: "krz/gitbay", Actor: "cmc", Kind: "mr.labeled",
182 Data: `{"number":471,"labels":["review"]}`},
183 }
184 lines := FeedLines(events)
185 if len(lines) != 1 || lines[0].Verb != "labelled" || lines[0].Ref != "!471" || lines[0].Extra != "review" {
186 t.Errorf("got %+v", lines)
187 }
188}
189
161// A scheduled job firing daily on an unchanged tip is a separate event 190// A scheduled job firing daily on an unchanged tip is a separate event
162// each tick, not another job of one run (#240): a repeated job name starts 191// each tick, not another job of one run (#240): a repeated job name starts
163// a new line, so three days read as three lines rather than "ran 3 jobs on" 192// a new line, so three days read as three lines rather than "ran 3 jobs on"
internal/web/templates/dashboard.html +1 −1
@@ -46,7 +46,7 @@
46 46
47<aside class="feedcol" aria-label="Recent activity"> 47<aside class="feedcol" aria-label="Recent activity">
48 <h2 class="colhead">Recent activity</h2> 48 <h2 class="colhead">Recent activity</h2>
49 {{range .Feed}}<p class="feedline"><a href="/{{.Actor}}">{{.Actor}}</a> {{.Verb}} <a href="{{.URL}}"{{if .Jobs}} title="{{join .Jobs ", "}}"{{end}}>{{.Ref}}</a><br><span class="none">{{.Repo}} · <span title="{{whenT .WhenT}}">{{ago .WhenT}}</span></span></p> 49 {{range .Feed}}<p class="feedline"><a href="/{{.Actor}}">{{.Actor}}</a> {{.Verb}} <a href="{{.URL}}"{{if .Jobs}} title="{{join .Jobs ", "}}"{{end}}>{{.Ref}}</a>{{if .Extra}} {{.Extra}}{{end}}<br><span class="none">{{.Repo}} · <span title="{{whenT .WhenT}}">{{ago .WhenT}}</span></span></p>
50 {{else}}<p class="none">No activity yet</p>{{end}} 50 {{else}}<p class="none">No activity yet</p>{{end}}
51</aside> 51</aside>
52 52
internal/web/templates/owner.html +1 −1
@@ -36,7 +36,7 @@
36 that goes back. */}} 36 that goes back. */}}
37{{if .Log}}<h3 class="logsub">recent</h3> 37{{if .Log}}<h3 class="logsub">recent</h3>
38<div class="ownerlog"> 38<div class="ownerlog">
39{{range .Log}}<p class="feedline"><a href="/{{.Actor}}">{{.Actor}}</a> {{.Verb}} <a href="{{.URL}}"{{if .Jobs}} title="{{join .Jobs ", "}}"{{end}}>{{.Ref}}</a><br><span class="none">{{.Repo}} · <span title="{{whenT .WhenT}}">{{ago .WhenT}}</span></span></p> 39{{range .Log}}<p class="feedline"><a href="/{{.Actor}}">{{.Actor}}</a> {{.Verb}} <a href="{{.URL}}"{{if .Jobs}} title="{{join .Jobs ", "}}"{{end}}>{{.Ref}}</a>{{if .Extra}} {{.Extra}}{{end}}<br><span class="none">{{.Repo}} · <span title="{{whenT .WhenT}}">{{ago .WhenT}}</span></span></p>
40{{end}} 40{{end}}
41</div> 41</div>
42<p class="meta"><a href="{{.Feed}}">activity feed</a></p> 42<p class="meta"><a href="{{.Feed}}">activity feed</a></p>