From cbb28ec1f1ef319a76d01465f4ca45ce2d45e00b Mon Sep 17 00:00:00 2001
From: David McPike <7037366+mcdafydd@users.noreply.github.com>
Date: Mon, 29 Apr 2019 00:06:48 -0500
Subject: [PATCH] Azure Devops support
- Add bluemonday to strip HTML tags from comments
- Rename all WorkItemEvent refs to WorkItemCommentedEvent
- Rename pullEvent to event to match types
- Minor comment clean-ups
- Support multiple pull request links in a work item
---
Gopkg.toml | 4 ++
server/events/event_parser.go | 80 ++++++++++++++++++-----------------
server/events_controller.go | 37 ++++++++++------
3 files changed, 69 insertions(+), 52 deletions(-)
diff --git a/Gopkg.toml b/Gopkg.toml
index f905e9f97..09d51f87a 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -100,6 +100,10 @@
branch = "master"
name = "github.com/mcdafydd/go-azuredevops"
+[[constraint]]
+ branch = "master"
+ name = "github.com/microcosm-cc/bluemonday"
+
[[constraint]]
name = "github.com/go-test/deep"
version = "1.0.1"
diff --git a/server/events/event_parser.go b/server/events/event_parser.go
index 404854a23..d610e7ff9 100644
--- a/server/events/event_parser.go
+++ b/server/events/event_parser.go
@@ -238,7 +238,7 @@ type EventParsing interface {
// event given the Bitbucket Server header.
GetBitbucketServerPullEventType(eventTypeHeader string) models.PullRequestEventType
- // ParseAzureDevopsWorkItemEvent parses Azure Devops work item comment events.
+ // ParseAzureDevopsWorkItemCommentedEvent parses Azure Devops work item comment events.
// There is no service hook event for comments made directly on a pull request, only
// for comments made on a work item. In order to see the content of a comment in
// the webhook payload, the comment must be on a work item. Since a work item
@@ -251,8 +251,8 @@ type EventParsing interface {
// baseRepo is the repo that the pull request will be merged into.
// user is the pull request author.
// pullNum is the number of the pull request that triggered the webhook.
- ParseAzureDevopsWorkItemEvent(comment *azuredevops.WorkItem) (
- baseRepo models.Repo, user models.User, pullNum int, err error)
+ // *** Add tests and handle linking multiple pull requests to a work item.
+ ParseAzureDevopsWorkItemCommentedEvent(comment *azuredevops.WorkItem) (pullRefs []PullRef, err error)
// ParseAzureDevopsPull parses the response from the Azure Devops API endpoint (not
// from a webhook) that returns a pull request.
@@ -723,12 +723,11 @@ func (e *EventParser) ParseBitbucketServerPullEvent(body []byte) (pull models.Pu
// ParseAzureDevopsPullEvent parses Azure Devops pull request events.
// See EventParsing for return value docs.
-func (e *EventParser) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) (pull models.PullRequest, pullEventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {
- /*if pullEvent == nil {
- err = errors.New("pullEvent is null")
+func (e *EventParser) ParseAzureDevopsPullEvent(event azuredevops.Event) (pull models.PullRequest, pullEventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {
+ pullResource, ok := event.Resource.(*azuredevops.GitPullRequest)
+ if !ok {
return
- }*/
- pullResource := pullEvent.Resource.(*azuredevops.GitPullRequest)
+ }
pull, baseRepo, headRepo, err = e.ParseAzureDevopsPull(pullResource)
if err != nil {
return
@@ -743,7 +742,7 @@ func (e *EventParser) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) (pu
err = errors.New("CreatedBy.UniqueName is null")
return
}
- switch pullEvent.EventType {
+ switch event.EventType {
case "git.pullrequest.created":
pullEventType = models.OpenedPullEvent
case "git.pullrequest.updated":
@@ -815,22 +814,19 @@ func (e *EventParser) ParseAzureDevopsPull(pull *azuredevops.GitPullRequest) (pu
return
}
-// ParseAzureDevopsWorkItemEvent parses Azure Devops work item comment events.
-// Azure Devops does not have a webhook that fires upon commenting on a pull request,
-// only for commenting on a work item.
-// It is possible to get a workitem.commented event without a reference linking it
-// to a repository if the user hasn't linked it to a pull request (relations list field
-// in the JSON payload). *** Test outcome of linking multiple pull requests to a work item.
-// Example comment in JSON payload:
-// "System.History": "
third comment discussion - see where this shows up in the
-// webhook. the webhook should also indicate two existing links to this work item.
-// one branch and one active pull request!
"
+// PullRef Data necessary to process a single pull request.
+type PullRef struct {
+ BaseRepo models.Repo
+ User models.User
+ PullNum int
+}
+
+// ParseAzureDevopsWorkItemCommentedEvent parses Azure Devops work item comment events.
+// Multiple pull requests can be linked to a single work item.
// See EventParsing for return value docs.
-func (e *EventParser) ParseAzureDevopsWorkItemEvent(comment *azuredevops.WorkItem) (baseRepo models.Repo, user models.User, pullNum int, err error) {
- // Check for pull request links to the work item that generated the comment
- // service event. If a pull request link exists, extract its number
- // Example link URI: vstfs:///Git/PullRequestId/d03edfe3-755b-4e8d-8f88-5aa5b51aba61%2f5d4fa372-78d4-4d85-aff6-7dcbc45d44d2%2f7
+func (e *EventParser) ParseAzureDevopsWorkItemCommentedEvent(comment *azuredevops.WorkItem) (pullRefs []PullRef, err error) {
for _, relation := range comment.Relations {
+ ref := &PullRef{}
if uri := relation.GetURL(); strings.Contains(uri, "vstfs:///Git/PullRequestId/") {
var parsed *url.URL
parsed, err = url.Parse(uri)
@@ -838,28 +834,34 @@ func (e *EventParser) ParseAzureDevopsWorkItemEvent(comment *azuredevops.WorkIte
return
}
pullNumStr := strings.Split(parsed.Path, "/")[5]
- pullNum, err = strconv.Atoi(pullNumStr)
+ ref.PullNum, err = strconv.Atoi(pullNumStr)
if err != nil {
return
}
+
+ // Retrieve the linked pull request to get baseRepo and user
+ client := new(azuredevops.Client)
+ client, err = azuredevops.NewClient(e.AzureDevopsOrg, e.AzureDevopsProject, e.AzureDevopsToken, nil)
+ if err != nil {
+ return
+ }
+ pr := new(azuredevops.GitPullRequest)
+ opts := azuredevops.PullRequestListOptions{}
+ pr, _, err = client.PullRequests.Get(ref.PullNum, &opts)
+ if err != nil {
+ return
+ }
+ createdBy := pr.GetCreatedBy()
+ ref.User = models.User{Username: createdBy.GetUniqueName()}
+ ref.BaseRepo, err = e.ParseAzureDevopsRepo(pr.GetRepository())
+ if err != nil {
+ return
+ }
+ pullRefs = append(pullRefs, *ref)
}
}
- // Retrieve the linked pull request to get baseRepo and user
- client, err := azuredevops.NewClient(e.AzureDevopsOrg, e.AzureDevopsProject, e.AzureDevopsToken, nil)
- opts := azuredevops.PullRequestListOptions{}
- pr, _, err := client.PullRequests.Get(pullNum, &opts)
- if err != nil {
- return
- }
- createdBy := pr.GetCreatedBy()
- user = models.User{Username: createdBy.GetUniqueName()}
- baseRepo, err = e.ParseAzureDevopsRepo(pr.GetRepository())
- if err != nil {
- return
- }
-
- return baseRepo, user, pullNum, err
+ return pullRefs, err
}
// ParseAzureDevopsRepo parses the response from the Azure Devops API endpoint that
diff --git a/server/events_controller.go b/server/events_controller.go
index 982618b60..5ff2d2ea9 100644
--- a/server/events_controller.go
+++ b/server/events_controller.go
@@ -21,6 +21,7 @@ import (
"github.com/google/go-github/github"
gitlab "github.com/lkysow/go-gitlab"
"github.com/mcdafydd/go-azuredevops/azuredevops"
+ "github.com/microcosm-cc/bluemonday"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/models"
@@ -231,7 +232,7 @@ func (e *EventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.
e.respond(w, logging.Debug, http.StatusBadRequest, "Error unmarshaling webhook payload %s", azuredevopsReqID)
}*/
switch event.PayloadType {
- case azuredevops.WorkItemEvent:
+ case azuredevops.WorkItemCommentedEvent:
e.Logger.Debug("handling as comment event")
e.HandleAzureDevopsCommentEvent(w, event, azuredevopsReqID)
case azuredevops.PullRequestEvent:
@@ -459,32 +460,42 @@ func (e *EventsController) HandleGitlabMergeRequestEvent(w http.ResponseWriter,
// HandleAzureDevopsCommentEvent handles comment events from Azure Devops where Atlantis
// commands can come from. It's exported to make testing easier.
+// Sometimes we may want data from the parent azuredevops.Event struct, so we handle type checking here.
func (e *EventsController) HandleAzureDevopsCommentEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {
- workItem := event.Resource.(*azuredevops.WorkItem)
- fields := *workItem.Fields
- if fields["System.State"] != "New" {
- e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since action was not created %s", azuredevopsReqID)
+ comment := new(string)
+ workItem, ok := event.Resource.(*azuredevops.WorkItem)
+ if !ok || event.PayloadType != azuredevops.WorkItemCommentedEvent {
+ e.respond(w, logging.Debug, http.StatusBadRequest, "Event.Resource is nil or received bad event type %v; Request-Id = %s", event.Resource, azuredevopsReqID)
return
}
+ *comment, ok = (*workItem.Fields)["System.History"].(string)
+ if !ok {
+ e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since comment is not a string; Request-Id = %s", azuredevopsReqID)
+ return
+ }
+ strippedComment := bluemonday.StrictPolicy().SanitizeBytes([]byte(*comment))
+
+ if len(workItem.Relations) == 0 {
+ e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since no pull request is linked to work item; Request-Id = %s", azuredevopsReqID)
+ return
+ }
+ pullRefs, err := e.Parser.ParseAzureDevopsWorkItemCommentedEvent(workItem)
- baseRepo, user, pullNum, err := e.Parser.ParseAzureDevopsWorkItemEvent(workItem)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Failed parsing event: %v %s", err, azuredevopsReqID)
return
}
- // *** Verify this for Azure Devops - comes from Github behavior ***
- // We pass in nil for maybeHeadRepo because the head repo data isn't
- // available in the AzureDevopsWorkItem event.
- comment := fields["System.History"]
- e.handleCommentEvent(w, baseRepo, nil, nil, user, pullNum, comment, models.AzureDevops)
+ for _, ref := range pullRefs {
+ e.handleCommentEvent(w, ref.BaseRepo, nil, nil, ref.User, ref.PullNum, string(strippedComment), models.AzureDevops)
+ }
}
// HandleAzureDevopsPullRequestEvent will delete any locks associated with the pull
// request if the event is a pull request closed event. It's exported to make
// testing easier.
-func (e *EventsController) HandleAzureDevopsPullRequestEvent(w http.ResponseWriter, pullEvent *azuredevops.Event, azuredevopsReqID string) {
- pull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseAzureDevopsPullEvent(*pullEvent)
+func (e *EventsController) HandleAzureDevopsPullRequestEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {
+ pull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseAzureDevopsPullEvent(*event)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s", err, azuredevopsReqID)
return