-
Notifications
You must be signed in to change notification settings - Fork 237
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR adds initial support for Azure DevOps Build and Pull Request events Co-authored-by: William Bagdon <[email protected]>
- Loading branch information
Showing
7 changed files
with
733 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package azuredevops | ||
|
||
// this package receives Azure DevOps Server webhooks | ||
// https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops-2020 | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
) | ||
|
||
// parse errors | ||
var ( | ||
ErrInvalidHTTPMethod = errors.New("invalid HTTP Method") | ||
ErrParsingPayload = errors.New("error parsing payload") | ||
) | ||
|
||
// Event defines an Azure DevOps server hook event type | ||
type Event string | ||
|
||
// Azure DevOps Server hook types | ||
const ( | ||
BuildCompleteEventType Event = "build.complete" | ||
GitPullRequestCreatedEventType Event = "git.pullrequest.created" | ||
GitPullRequestUpdatedEventType Event = "git.pullrequest.updated" | ||
GitPullRequestMergedEventType Event = "git.pullrequest.merged" | ||
) | ||
|
||
// Webhook instance contains all methods needed to process events | ||
type Webhook struct { | ||
} | ||
|
||
// New creates and returns a WebHook instance | ||
func New() (*Webhook, error) { | ||
hook := new(Webhook) | ||
return hook, nil | ||
} | ||
|
||
// Parse verifies and parses the events specified and returns the payload object or an error | ||
func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) { | ||
defer func() { | ||
_, _ = io.Copy(ioutil.Discard, r.Body) | ||
_ = r.Body.Close() | ||
}() | ||
|
||
if r.Method != http.MethodPost { | ||
return nil, ErrInvalidHTTPMethod | ||
} | ||
|
||
payload, err := ioutil.ReadAll(r.Body) | ||
if err != nil || len(payload) == 0 { | ||
return nil, ErrParsingPayload | ||
} | ||
|
||
var pl BasicEvent | ||
err = json.Unmarshal([]byte(payload), &pl) | ||
if err != nil { | ||
return nil, ErrParsingPayload | ||
} | ||
|
||
switch pl.EventType { | ||
case GitPullRequestCreatedEventType, GitPullRequestMergedEventType, GitPullRequestUpdatedEventType: | ||
var fpl GitPullRequestEvent | ||
err = json.Unmarshal([]byte(payload), &fpl) | ||
return fpl, err | ||
case BuildCompleteEventType: | ||
var fpl BuildCompleteEvent | ||
err = json.Unmarshal([]byte(payload), &fpl) | ||
return fpl, err | ||
default: | ||
return nil, fmt.Errorf("unknown event %s", pl.EventType) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package azuredevops | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"testing" | ||
|
||
"reflect" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// NOTES: | ||
// - Run "go test" to run tests | ||
// - Run "gocov test | gocov report" to report on test converage by file | ||
// - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called | ||
// | ||
// or | ||
// | ||
// -- may be a good idea to change to output path to somewherelike /tmp | ||
// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html | ||
// | ||
|
||
const ( | ||
virtualDir = "/webhooks" | ||
) | ||
|
||
var hook *Webhook | ||
|
||
func TestMain(m *testing.M) { | ||
|
||
// setup | ||
var err error | ||
hook, err = New() | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
os.Exit(m.Run()) | ||
// teardown | ||
} | ||
|
||
func newServer(handler http.HandlerFunc) *httptest.Server { | ||
mux := http.NewServeMux() | ||
mux.HandleFunc(virtualDir, handler) | ||
return httptest.NewServer(mux) | ||
} | ||
|
||
func TestWebhooks(t *testing.T) { | ||
assert := require.New(t) | ||
tests := []struct { | ||
name string | ||
event Event | ||
typ interface{} | ||
filename string | ||
headers http.Header | ||
}{ | ||
{ | ||
name: "build.complete", | ||
event: BuildCompleteEventType, | ||
typ: BuildCompleteEvent{}, | ||
filename: "../testdata/azuredevops/build.complete.json", | ||
}, | ||
{ | ||
name: "git.pullrequest.created", | ||
event: GitPullRequestCreatedEventType, | ||
typ: GitPullRequestEvent{}, | ||
filename: "../testdata/azuredevops/git.pullrequest.created.json", | ||
}, | ||
{ | ||
name: "git.pullrequest.merged", | ||
event: GitPullRequestMergedEventType, | ||
typ: GitPullRequestEvent{}, | ||
filename: "../testdata/azuredevops/git.pullrequest.merged.json", | ||
}, | ||
{ | ||
name: "git.pullrequest.updated", | ||
event: GitPullRequestUpdatedEventType, | ||
typ: GitPullRequestEvent{}, | ||
filename: "../testdata/azuredevops/git.pullrequest.updated.json", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tc := tt | ||
client := &http.Client{} | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
payload, err := os.Open(tc.filename) | ||
assert.NoError(err) | ||
defer func() { | ||
_ = payload.Close() | ||
}() | ||
|
||
var parseError error | ||
var results interface{} | ||
server := newServer(func(w http.ResponseWriter, r *http.Request) { | ||
results, parseError = hook.Parse(r, tc.event) | ||
}) | ||
defer server.Close() | ||
req, err := http.NewRequest(http.MethodPost, server.URL+virtualDir, payload) | ||
assert.NoError(err) | ||
req.Header.Set("Content-Type", "application/json") | ||
|
||
resp, err := client.Do(req) | ||
assert.NoError(err) | ||
assert.Equal(http.StatusOK, resp.StatusCode) | ||
assert.NoError(parseError) | ||
assert.Equal(reflect.TypeOf(tc.typ), reflect.TypeOf(results)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package azuredevops | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// https://docs.microsoft.com/en-us/azure/devops/service-hooks/events | ||
|
||
// azure devops does not send an event header, this BasicEvent is provided to get the EventType | ||
|
||
type BasicEvent struct { | ||
ID string `json:"id"` | ||
EventType Event `json:"eventType"` | ||
PublisherID string `json:"publisherId"` | ||
Scope string `json:"scope"` | ||
CreatedDate Date `json:"createdDate"` | ||
} | ||
|
||
// git.pullrequest.* | ||
// git.pullrequest.created | ||
// git.pullrequest.merged | ||
// git.pullrequest.updated | ||
|
||
type GitPullRequestEvent struct { | ||
ID string `json:"id"` | ||
EventType Event `json:"eventType"` | ||
PublisherID string `json:"publisherId"` | ||
Scope string `json:"scope"` | ||
Message Message `json:"message"` | ||
DetailedMessage Message `json:"detailedMessage"` | ||
Resource PullRequest `json:"resource"` | ||
ResourceVersion string `json:"resourceVersion"` | ||
ResourceContainers interface{} `json:"resourceContainers"` | ||
CreatedDate Date `json:"createdDate"` | ||
} | ||
|
||
// build.complete | ||
|
||
type BuildCompleteEvent struct { | ||
ID string `json:"id"` | ||
EventType Event `json:"eventType"` | ||
PublisherID string `json:"publisherId"` | ||
Scope string `json:"scope"` | ||
Message Message `json:"message"` | ||
DetailedMessage Message `json:"detailedMessage"` | ||
Resource Build `json:"resource"` | ||
ResourceVersion string `json:"resourceVersion"` | ||
ResourceContainers interface{} `json:"resourceContainers"` | ||
CreatedDate Date `json:"createdDate"` | ||
} | ||
|
||
// ----------------------- | ||
|
||
type Message struct { | ||
Text string `json:"text"` | ||
HTML string `json:"html"` | ||
Markdown string `json:"markdown"` | ||
} | ||
|
||
type Commit struct { | ||
CommitID string `json:"commitId"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type PullRequest struct { | ||
Repository Repository `json:"repository"` | ||
PullRequestID int `json:"pullRequestId"` | ||
Status string `json:"status"` | ||
CreatedBy User `json:"createdBy"` | ||
CreationDate Date `json:"creationDate"` | ||
ClosedDate Date `json:"closedDate"` | ||
Title string `json:"title"` | ||
Description string `json:"description"` | ||
SourceRefName string `json:"sourceRefName"` | ||
TargetRefName string `json:"targetRefName"` | ||
MergeStatus string `json:"mergeStatus"` | ||
MergeID string `json:"mergeId"` | ||
LastMergeSourceCommit Commit `json:"lastMergeSourceCommit"` | ||
LastMergeTargetCommit Commit `json:"lastMergeTargetCommit"` | ||
LastMergeCommit Commit `json:"lastMergeCommit"` | ||
Reviewers []Reviewer `json:"reviewers"` | ||
Commits []Commit `json:"commits"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type Repository struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
URL string `json:"url"` | ||
Project Project `json:"project"` | ||
DefaultBranch string `json:"defaultBranch"` | ||
RemoteURL string `json:"remoteUrl"` | ||
} | ||
|
||
type Project struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
URL string `json:"url"` | ||
State string `json:"state"` | ||
} | ||
|
||
type User struct { | ||
ID string `json:"id"` | ||
DisplayName string `json:"displayName"` | ||
UniqueName string `json:"uniqueName"` | ||
URL string `json:"url"` | ||
ImageURL string `json:"imageUrl"` | ||
} | ||
|
||
type Reviewer struct { | ||
ReviewerURL string `json:"reviewerUrl"` | ||
Vote int `json:"vote"` | ||
ID string `json:"id"` | ||
DisplayName string `json:"displayName"` | ||
UniqueName string `json:"uniqueName"` | ||
URL string `json:"url"` | ||
ImageURL string `json:"imageUrl"` | ||
IsContainer bool `json:"isContainer"` | ||
} | ||
|
||
type Build struct { | ||
URI string `json:"uri"` | ||
ID int `json:"id"` | ||
BuildNumber string `json:"buildNumber"` | ||
URL string `json:"url"` | ||
StartTime Date `json:"startTime"` | ||
FinishTime Date `json:"finishTime"` | ||
Reason string `json:"reason"` | ||
Status string `json:"status"` | ||
DropLocation string `json:"dropLocation"` | ||
Drop Drop `json:"drop"` | ||
Log Log `json:"log"` | ||
SourceGetVersion string `json:"sourceGetVersion"` | ||
LastChangedBy User `json:"lastChangedBy"` | ||
RetainIndefinitely bool `json:"retainIndefinitely"` | ||
HasDiagnostics bool `json:"hasDiagnostics"` | ||
Definition BuildDefinition `json:"definition"` | ||
Queue Queue `json:"queue"` | ||
Requests []Request `json:"requests"` | ||
} | ||
|
||
type Drop struct { | ||
Location string `json:"location"` | ||
Type string `json:"type"` | ||
URL string `json:"url"` | ||
DownloadURL string `json:"downloadUrl"` | ||
} | ||
|
||
type Log struct { | ||
Type string `json:"type"` | ||
URL string `json:"url"` | ||
DownloadURL string `json:"downloadUrl"` | ||
} | ||
|
||
type BuildDefinition struct { | ||
BatchSize int `json:"batchSize"` | ||
TriggerType string `json:"triggerType"` | ||
DefinitionType string `json:"definitionType"` | ||
ID int `json:"id"` | ||
Name string `json:"name"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type Queue struct { | ||
QueueType string `json:"queueType"` | ||
ID int `json:"id"` | ||
Name string `json:"name"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type Request struct { | ||
ID int `json:"id"` | ||
URL string `json:"url"` | ||
RequestedFor User `json:"requestedFor"` | ||
} | ||
|
||
type Date time.Time | ||
|
||
func (b *Date) UnmarshalJSON(p []byte) error { | ||
t, err := time.Parse(time.RFC3339Nano, strings.Replace(string(p), "\"", "", -1)) | ||
if err != nil { | ||
return err | ||
} | ||
*b = Date(t) | ||
return nil | ||
} | ||
|
||
func (b Date) MarshalJSON() ([]byte, error) { | ||
stamp := fmt.Sprintf("\"%s\"", time.Time(b).Format(time.RFC3339Nano)) | ||
return []byte(stamp), nil | ||
} |
Oops, something went wrong.