Skip to content

Commit

Permalink
Add support for Azure DevOps pull request events (#115)
Browse files Browse the repository at this point in the history
* Add the test cases for Azure DevOps pull request support

* Add support for resource versioning and stub handling of PR events

* Add support for Azure DevOps pull request events

* Update the documentation to include Azure DevOps under the VS section

* Move test event check so it applies to all events
  • Loading branch information
DewaldDeJager authored Jun 15, 2022
1 parent 6ca60d7 commit d8dcc61
Show file tree
Hide file tree
Showing 3 changed files with 724 additions and 152 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ If the (commit) message includes `[skip ci]` or `[ci skip]` no build will be tri
* handled on the path: `/h/bitbucket-server/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN`
* [Slack](https://slack.com) (both outgoing webhooks & slash commands)
* handled on the path: `/h/slack/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN`
* [Visual Studio Team Services](https://www.visualstudio.com/products/visual-studio-team-services-vs)
* [Visual Studio Team Services](https://www.visualstudio.com/products/visual-studio-team-services-vs) & [Azure DevOps](https://dev.azure.com)
* handled on the path: `/h/visualstudio/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN`
* [GitLab](https://gitlab.com)
* handled on the path: `/h/gitlab/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN`
Expand Down Expand Up @@ -134,7 +134,7 @@ That's all! The next time you __push code__
a build will be triggered (if you have Trigger mapping defined for the event(s) on Bitrise).


### Visual Studio Online / Visual Studio Team Services - setup & usage:
### Visual Studio Online / Visual Studio Team Services / Azure DevOps - setup & usage:

All you have to do is register your `bitrise-webhooks` URL for
a [visualstudio.com](https://visualstudio.com) *project* as a `Service Hooks` integration.
Expand All @@ -144,12 +144,12 @@ on [visualstudio.com 's documentations site](https://www.visualstudio.com/en-us/

A short step-by-step guide:

1. Open your *project* on [visualstudio.com](https://visualstudio.com)
2. Go to the *Admin/Control panel* of the *project*
1. Open your *project* on [visualstudio.com](https://visualstudio.com) or [dev.azure.com](https://dev.azure.com)
2. Go to the *Admin/Control panel* or *Project settings* of the *project*
3. Select `Service Hooks`
4. Create a service integration
* In the Service list select the `Web Hooks` option
* Select the `Code pushed` event as the *Trigger*
* Select the `Code pushed`, `Pull request created` or `Pull request updated` event as the *Trigger*
* In the `Filters` section select the `Repository` you want to integrate
* You can leave the other filters on default
* Click `Next`
Expand Down
204 changes: 174 additions & 30 deletions service/hook/visualstudioteamservices/visualstudioteamservices.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
Expand All @@ -17,42 +18,85 @@ const (

// ProviderID ...
ProviderID = "visualstudio"

// Push event name
Push string = "git.push"
// PullRequestCreate event name
PullRequestCreate = "git.pullrequest.created"
// PullRequestUpdate event name
PullRequestUpdate = "git.pullrequest.updated"
)

// --------------------------
// --- Webhook Data Model ---

// CommitsModel ...
type CommitsModel struct {
// CommitModel ...
type CommitModel struct {
CommitID string `json:"commitId"`
Comment string `json:"comment"`
}

// AuthorModel ...
type AuthorModel struct {
DisplayName string `json:"displayName"`
}

// RefUpdatesModel ...
type RefUpdatesModel struct {
Name string `json:"name"`
OldObjectID string `json:"oldObjectId"`
NewObjectID string `json:"newObjectId"`
}

// ResourceModel ...
type ResourceModel struct {
Commits []CommitsModel `json:"commits"`
// PushResourceModel ...
type PushResourceModel struct {
Commits []CommitModel `json:"commits"`
RefUpdates []RefUpdatesModel `json:"refUpdates"`
}

// PullRequestResourceModel ...
type PullRequestResourceModel struct {
SourceReferenceName string `json:"sourceRefName"`
TargetReferenceName string `json:"targetRefName"`
MergeStatus string `json:"mergeStatus"`
LastSourceCommit CommitModel `json:"lastMergeSourceCommit"`
CreatedBy AuthorModel `json:"createdBy"`
Status string `json:"status"`
PullRequestID int `json:"pullRequestId"`
}

// EventMessage ...
type EventMessage struct {
Text string `json:"text"`
}

// EventModel ...
type EventModel struct {
SubscriptionID string `json:"subscriptionId"`
EventType string `json:"eventType"`
PublisherID string `json:"publisherId"`
}

// PushEventModel ...
type PushEventModel struct {
SubscriptionID string `json:"subscriptionId"`
EventType string `json:"eventType"`
PublisherID string `json:"publisherId"`
Resource ResourceModel `json:"resource"`
DetailedMessage EventMessage `json:"detailedMessage"`
SubscriptionID string `json:"subscriptionId"`
EventType string `json:"eventType"`
PublisherID string `json:"publisherId"`
Resource PushResourceModel `json:"resource"`
ResourceVersion string `json:"resourceVersion"`
DetailedMessage EventMessage `json:"detailedMessage"`
Message EventMessage `json:"message"`
}

// PullRequestEventModel ...
type PullRequestEventModel struct {
SubscriptionID string `json:"subscriptionId"`
EventType string `json:"eventType"`
PublisherID string `json:"publisherId"`
Resource PullRequestResourceModel `json:"resource"`
ResourceVersion string `json:"resourceVersion"`
DetailedMessage EventMessage `json:"detailedMessage"`
Message EventMessage `json:"message"`
}

// ---------------------------------------
Expand All @@ -61,6 +105,7 @@ type PushEventModel struct {
// HookProvider ...
type HookProvider struct{}

// detectContentType ...
func detectContentType(header http.Header) (string, error) {
contentType := header.Get("Content-Type")
if contentType == "" {
Expand All @@ -72,27 +117,12 @@ func detectContentType(header http.Header) (string, error) {

// transformPushEvent ...
func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultModel {
if pushEvent.PublisherID != "tfs" {
if pushEvent.ResourceVersion != "1.0" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Not a Team Foundation Server notification, can't start a build"),
Error: fmt.Errorf("Unsupported resource version"),
}
}

if pushEvent.EventType != "git.push" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Not a push event, can't start a build"),
}
}

if pushEvent.SubscriptionID == "00000000-0000-0000-0000-000000000000" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Initial (test) event detected, skipping"),
ShouldSkip: true,
}
}

// VSO sends separate events for separate event (branches, tags, etc.)

if len(pushEvent.Resource.RefUpdates) != 1 {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Can't detect branch information (resource.refUpdates is empty), can't start a build"),
Expand Down Expand Up @@ -201,6 +231,79 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode

}

// transformPullRequestEvent ...
func transformPullRequestEvent(pullRequestEvent PullRequestEventModel) hookCommon.TransformResultModel {
if pullRequestEvent.ResourceVersion != "1.0" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Unsupported resource version"),
}
}

pullRequest := pullRequestEvent.Resource
if pullRequest.Status == "completed" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Pull request already completed"),
ShouldSkip: true,
}
}

if pullRequest.MergeStatus != "succeeded" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Pull request is not mergeable"),
}
}

if pullRequest.SourceReferenceName == "" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Missing source reference name"),
}
}

if !strings.HasPrefix(pullRequest.SourceReferenceName, "refs/heads/") {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Invalid source reference name"),
}
}

if pullRequest.TargetReferenceName == "" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Missing target reference name"),
}
}

if !strings.HasPrefix(pullRequest.TargetReferenceName, "refs/heads/") {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Invalid target reference name"),
}
}

if pullRequest.LastSourceCommit == (CommitModel{}) || pullRequest.LastSourceCommit.CommitID == "" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Missing last source branch commit details"),
}
}

var buildParams = bitriseapi.BuildParamsModel{
CommitHash: pullRequest.LastSourceCommit.CommitID,
CommitMessage: pullRequestEvent.Message.Text,
Branch: strings.TrimPrefix(pullRequest.SourceReferenceName, "refs/heads/"),
BranchDest: strings.TrimPrefix(pullRequest.TargetReferenceName, "refs/heads/"),
PullRequestAuthor: pullRequest.CreatedBy.DisplayName,
}

if pullRequest.PullRequestID != 0 {
buildParams.PullRequestID = &pullRequest.PullRequestID
}

return hookCommon.TransformResultModel{
TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{
{
BuildParams: buildParams,
},
},
}
}

// TransformRequest ...
func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel {
contentType, err := detectContentType(r.Header)
Expand Down Expand Up @@ -228,12 +331,53 @@ func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformRes
}
}

var pushEvent PushEventModel
if err := json.NewDecoder(r.Body).Decode(&pushEvent); err != nil {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Failed to read request body"),
}
}

var event EventModel
if err := json.Unmarshal(body, &event); err != nil {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Failed to parse request body as JSON: %s", err),
}
}

return transformPushEvent(pushEvent)
if event.PublisherID != "tfs" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Not a Team Foundation Server notification, can't start a build"),
}
}

if event.SubscriptionID == "00000000-0000-0000-0000-000000000000" {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Initial (test) event detected, skipping"),
ShouldSkip: true,
}
}

if event.EventType == Push {
var pushEvent PushEventModel
if err := json.Unmarshal(body, &pushEvent); err != nil {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Failed to parse request body as JSON: %s", err),
}
}
return transformPushEvent(pushEvent)
} else if event.EventType == PullRequestCreate || event.EventType == PullRequestUpdate {
var pullRequestEvent PullRequestEventModel
if err := json.Unmarshal(body, &pullRequestEvent); err != nil {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Failed to parse request body as JSON: %s", err),
}
}
return transformPullRequestEvent(pullRequestEvent)
} else {
return hookCommon.TransformResultModel{
Error: fmt.Errorf("Unsupported event type"),
}
}

}
Loading

0 comments on commit d8dcc61

Please sign in to comment.