Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Filter workflow events by mentions and assignment

727df38613897e6ffd56e1df05a2f674174bf547 · Keenan Nemetz · 8d ago · parent eb07ff56

Filter workflow events by mentions and assignment

Add mentioned, replied-to, assigned-to, and author filters to issue, comment, and change-request triggers. Move the role vocabulary into core/role so author.role is validated without importing the access store, and pin mentions, reply authors, and assignee changes in the event payload.

server/events.go +123−43

@@ -56,38 +56,44 @@ type eventPayload struct {
56 56 }
57 57
58 58 type payloadIssue struct {
59 ID string `json:"id"`
60 Title string `json:"title"`
61 State string `json:"state"`
62 Author string `json:"author"`
63 Labels []string `json:"labels,omitempty"`
64 Assignees []string `json:"assignees,omitempty"`
65 AssigneesAdded []string `json:"assignees_added,omitempty"`
66 AssigneesRemoved []string `json:"assignees_removed,omitempty"`
67 Mentions []string `json:"mentions,omitempty"`
68 Body string `json:"body,omitempty"`
69 BodyTruncated bool `json:"body_truncated,omitempty"`
70 URL string `json:"url"`
71 }
72
73 type payloadRequest struct {
59 74 ID string `json:"id"`
60 75 Title string `json:"title"`
61 76 State string `json:"state"`
62 77 Author string `json:"author"`
63 Labels []string `json:"labels,omitempty"`
64 Assignees []string `json:"assignees,omitempty"`
78 Base string `json:"base"`
79 Head string `json:"head"`
80 HeadRef string `json:"head_ref,omitempty"`
81 Mentions []string `json:"mentions,omitempty"`
65 82 Body string `json:"body,omitempty"`
66 83 BodyTruncated bool `json:"body_truncated,omitempty"`
67 84 URL string `json:"url"`
68 85 }
69 86
70 type payloadRequest struct {
71 ID string `json:"id"`
72 Title string `json:"title"`
73 State string `json:"state"`
74 Author string `json:"author"`
75 Base string `json:"base"`
76 Head string `json:"head"`
77 HeadRef string `json:"head_ref,omitempty"`
78 Body string `json:"body,omitempty"`
79 BodyTruncated bool `json:"body_truncated,omitempty"`
80 URL string `json:"url"`
81 }
82
83 87 type payloadComment struct {
84 ID string `json:"id"`
85 Author string `json:"author"`
86 Body string `json:"body,omitempty"`
87 BodyTruncated bool `json:"body_truncated,omitempty"`
88 ReplyTo string `json:"reply_to,omitempty"`
89 Deleted bool `json:"deleted,omitempty"`
90 URL string `json:"url"`
88 ID string `json:"id"`
89 Author string `json:"author"`
90 Body string `json:"body,omitempty"`
91 BodyTruncated bool `json:"body_truncated,omitempty"`
92 Mentions []string `json:"mentions,omitempty"`
93 ReplyTo string `json:"reply_to,omitempty"`
94 ReplyToAuthor string `json:"reply_to_author,omitempty"`
95 Deleted bool `json:"deleted,omitempty"`
96 URL string `json:"url"`
91 97 }
92 98
93 99 // watchMetadata applies metadata events at startup and then on a timer. A browser
@@ -271,34 +277,44 @@ func metadataPathIDs(path string) (entityID, commentID, kind string, ok bool) {
271 277 func (s *Server) processMetadataPath(m *repo.Meta, old string, c git.Commit, acl *access.ACL, entityID, commentID, kind string) {
272 278 switch kind {
273 279 case "issue":
274 s.issueEvent(m, old, c, entityID)
280 s.issueEvent(m, old, c, acl, entityID)
275 281 case "issue_comment":
276 s.commentEvent(m, old, c, entityID, commentID, false)
282 s.commentEvent(m, old, c, acl, entityID, commentID, false)
277 283 case "request_comment":
278 s.commentEvent(m, old, c, entityID, commentID, true)
284 s.commentEvent(m, old, c, acl, entityID, commentID, true)
279 285 case "request", "approval":
280 286 s.changeRequestEvent(m, old, c, acl, entityID, kind == "request")
281 287 }
282 288 }
283 289
284 func (s *Server) issueEvent(m *repo.Meta, old string, c git.Commit, issueID string) {
290 func (s *Server) issueEvent(m *repo.Meta, old string, c git.Commit, acl *access.ACL, issueID string) {
285 291 oldIssue, _, _ := metadata.LoadIssueAtCommit(m.Git(), old, issueID)
286 292 newIssue, exists, err := metadata.LoadIssueAtCommit(m.Git(), c.SHA, issueID)
287 293 if err != nil || !exists {
288 294 return
289 295 }
290 var action string
296 var oldAssignees []string
297 if oldIssue != nil {
298 oldAssignees = oldIssue.Assignees
299 }
300 added, removed := nameDiff(oldAssignees, newIssue.Assignees)
301 actions := make([]string, 0, 2)
291 302 switch {
292 303 case oldIssue == nil:
293 action = "opened"
304 actions = append(actions, "opened")
294 305 case oldIssue.State != newIssue.State:
295 306 if newIssue.State == "closed" {
296 action = "closed"
307 actions = append(actions, "closed")
297 308 } else {
298 action = "reopened"
309 actions = append(actions, "reopened")
299 310 }
300 311 default:
301 action = "edited"
312 actions = append(actions, "edited")
313 }
314 // An assignment is also an edit, so an existing edited listener keeps seeing
315 // the change. A workflow that wants only assignments listens for assigned.
316 if oldIssue != nil && len(added) > 0 {
317 actions = append(actions, "assigned")
302 318 }
303 319 ref, branch, commit, err := s.defaultBranchCommit(m)
304 320 if err != nil {
@@ -307,19 +323,46 @@ func (s *Server) issueEvent(m *repo.Meta, old string, c git.Commit, issueID stri
307 323 }
308 324 body, clipped := clipBody(newIssue.Description)
309 325 payload := eventPayload{
310 Event: "issue", Action: action, Repository: m.Name, Ref: ref, Branch: branch,
326 Event: "issue", Repository: m.Name, Ref: ref, Branch: branch,
311 327 Commit: commit, Sender: c.Author,
312 328 Issue: &payloadIssue{
313 329 ID: newIssue.ID, Title: newIssue.Title, State: newIssue.State, Author: newIssue.Author,
314 330 Labels: newIssue.Labels, Assignees: newIssue.Assignees,
315 Body: body, BodyTruncated: clipped,
331 AssigneesAdded: added, AssigneesRemoved: removed,
332 Mentions: metadata.Mentions(newIssue.Description),
333 Body: body, BodyTruncated: clipped,
316 334 URL: "/r/" + m.Name + "/issues/" + newIssue.ID,
317 335 },
318 336 }
319 s.queuePayload(m, "issue", action, ref, branch, "", commit, payload, c.SHA+":issue:"+action+":"+issueID)
337 for _, action := range actions {
338 s.queuePayload(m, "issue", action, ref, branch, "", commit, acl, payload,
339 c.SHA+":issue:"+action+":"+issueID)
340 }
341 }
342
343 // nameDiff reports which names were added to and removed from a set. Both lists
344 // keep the order the metadata writer stores, so the event is deterministic.
345 func nameDiff(before, after []string) (added, removed []string) {
346 had := make(map[string]bool, len(before))
347 for _, name := range before {
348 had[name] = true
349 }
350 have := make(map[string]bool, len(after))
351 for _, name := range after {
352 have[name] = true
353 if !had[name] {
354 added = append(added, name)
355 }
356 }
357 for _, name := range before {
358 if !have[name] {
359 removed = append(removed, name)
360 }
361 }
362 return added, removed
320 363 }
321 364
322 func (s *Server) commentEvent(m *repo.Meta, old string, c git.Commit, entityID, commentID string, request bool) {
365 func (s *Server) commentEvent(m *repo.Meta, old string, c git.Commit, acl *access.ACL, entityID, commentID string, request bool) {
323 366 var server string
324 367 if request {
325 368 server = "request"
@@ -355,24 +398,37 @@ func (s *Server) commentEvent(m *repo.Meta, old string, c git.Commit, entityID,
355 398 url = "/r/" + m.Name + "/requests/" + req.ID
356 399 body, clipped := clipBody(req.Description)
357 400 change = &payloadRequest{ID: req.ID, Title: req.Title, State: req.State, Author: req.Author,
358 Base: req.Base, Head: req.Head, HeadRef: req.HeadRef, Body: body,
401 Base: req.Base, Head: req.Head, HeadRef: req.HeadRef,
402 Mentions: metadata.Mentions(req.Description), Body: body,
359 403 BodyTruncated: clipped, URL: url}
360 404 }
361 405 } else if iss, ok, _ := metadata.LoadIssueAtCommit(m.Git(), c.SHA, entityID); ok && iss != nil {
362 406 url = "/r/" + m.Name + "/issues/" + iss.ID
363 407 body, clipped := clipBody(iss.Description)
364 408 issue = &payloadIssue{ID: iss.ID, Title: iss.Title, State: iss.State, Author: iss.Author,
365 Labels: iss.Labels, Assignees: iss.Assignees, Body: body, BodyTruncated: clipped, URL: url}
409 Labels: iss.Labels, Assignees: iss.Assignees, Mentions: metadata.Mentions(iss.Description),
410 Body: body, BodyTruncated: clipped, URL: url}
411 }
412 replyToAuthor := ""
413 if shown.ReplyTo != "" {
414 parent := findComment(m, c.SHA, entityID, shown.ReplyTo, request)
415 if parent == nil {
416 parent = findComment(m, old, entityID, shown.ReplyTo, request)
417 }
418 if parent != nil {
419 replyToAuthor = parent.Author
420 }
366 421 }
367 422 body, clipped := clipBody(shown.Body)
368 423 payload := eventPayload{
369 424 Event: "comment", Action: action, Repository: m.Name, Ref: ref, Branch: branch,
370 425 Commit: commit, Sender: c.Author, CommentOn: server, Issue: issue, Change: change,
371 426 Comment: &payloadComment{ID: shown.ID, Author: shown.Author, Body: body,
372 BodyTruncated: clipped, ReplyTo: shown.ReplyTo, Deleted: shown.Deleted,
427 BodyTruncated: clipped, Mentions: metadata.Mentions(shown.Body),
428 ReplyTo: shown.ReplyTo, ReplyToAuthor: replyToAuthor, Deleted: shown.Deleted,
373 429 URL: url + "#c-" + shown.ID},
374 430 }
375 s.queuePayload(m, "comment", action, ref, branch, "", commit, payload,
431 s.queuePayload(m, "comment", action, ref, branch, "", commit, acl, payload,
376 432 c.SHA+":comment:"+action+":"+entityID+":"+commentID)
377 433 }
378 434
@@ -428,7 +484,8 @@ func (s *Server) changeRequestEvent(m *repo.Meta, old string, c git.Commit, acl
428 484 ChangedFiles: changed,
429 485 Change: &payloadRequest{ID: newReq.ID, Title: newReq.Title, State: newReq.State,
430 486 Author: newReq.Author, Base: newReq.Base, Head: newReq.Head, HeadRef: newReq.HeadRef,
431 Body: body, BodyTruncated: clipped, URL: "/r/" + m.Name + "/requests/" + newReq.ID},
487 Mentions: metadata.Mentions(newReq.Description),
488 Body: body, BodyTruncated: clipped, URL: "/r/" + m.Name + "/requests/" + newReq.ID},
432 489 }
433 490 actions := []string{}
434 491 switch {
@@ -461,7 +518,7 @@ func (s *Server) changeRequestEvent(m *repo.Meta, old string, c git.Commit, acl
461 518 }
462 519 }
463 520 for _, action := range actions {
464 s.queuePayload(m, "change_request", action, baseRef, newReq.Base, "", base, payload,
521 s.queuePayload(m, "change_request", action, baseRef, newReq.Base, "", base, acl, payload,
465 522 c.SHA+":change_request:"+action+":"+requestID)
466 523 }
467 524 }
@@ -482,7 +539,7 @@ func (s *Server) defaultBranchCommit(m *repo.Meta) (ref, branch, commit string,
482 539 return ref, branch, commit, nil
483 540 }
484 541
485 func (s *Server) queuePayload(m *repo.Meta, trigger, action, ref, branch, tag, commit string, payload eventPayload, occurrence string) {
542 func (s *Server) queuePayload(m *repo.Meta, trigger, action, ref, branch, tag, commit string, acl *access.ACL, payload eventPayload, occurrence string) {
486 543 payload.Event = trigger
487 544 payload.Action = action
488 545 body, err := json.Marshal(payload)
@@ -495,15 +552,38 @@ func (s *Server) queuePayload(m *repo.Meta, trigger, action, ref, branch, tag, c
495 552 "bytes", len(body), "limit", ci.MaxEventPayloadBytes)
496 553 return
497 554 }
555 event := eventContext(trigger, action, payload, acl)
498 556 n := s.queue(m, m.Name, queueSpec{
499 557 ref: ref, branch: branch, tag: tag, commit: commit,
500 trigger: trigger, action: action, payload: string(body), occurrence: occurrence,
558 trigger: trigger, action: action, event: event,
559 payload: string(body), occurrence: occurrence,
501 560 })
502 561 if n > 0 {
503 562 s.Log.Info("ci: queued an event run", "repo", m.Name, "trigger", trigger, "action", action, "runs", n)
504 563 }
505 564 }
506 565
566 // eventContext takes the filter inputs from the pinned payload. The comment's own
567 // body is the subject of a comment event; the issue or change body is the subject
568 // of an issue or change event.
569 func eventContext(trigger, action string, payload eventPayload, acl *access.ACL) ci.EventContext {
570 event := ci.EventContext{Kind: trigger, Action: action, Sender: payload.Sender}
571 if acl != nil && payload.Sender != "" {
572 event.SenderRole = acl.RoleFor(payload.Sender)
573 }
574 switch {
575 case payload.Comment != nil:
576 event.Mentions = payload.Comment.Mentions
577 event.ReplyToAuthor = payload.Comment.ReplyToAuthor
578 case payload.Issue != nil:
579 event.Mentions = payload.Issue.Mentions
580 event.AssigneesAdded = payload.Issue.AssigneesAdded
581 case payload.Change != nil:
582 event.Mentions = payload.Change.Mentions
583 }
584 return event
585 }
586
507 587 func clipBody(body string) (string, bool) {
508 588 if len(body) <= maxPayloadBody {
509 589 return body, false