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