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