From 01ecc72252c22cc61541be694ed3dced0e485160 Mon Sep 17 00:00:00 2001 From: Viktor Benei Date: Wed, 23 Mar 2016 10:24:56 +0100 Subject: [PATCH 1/3] todo - docker image --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8e02d90..4bf35236 100644 --- a/README.md +++ b/README.md @@ -331,5 +331,6 @@ response provider will be used. ## TODO * Re-try handling +* Docker image: auto-create & publish a Docker Image for the webhooks server, to make it easy to run it on your own server * Bitbucket V1 (aka "Services" on the Bitbucket web UI) - not sure whether we should support this, it's already kind of deprecated, and we already support the newer, V2 webhooks. From c828e7499fb9fbd652d0c014716fd9e14569689e Mon Sep 17 00:00:00 2001 From: Chad Robinson Date: Tue, 12 Apr 2016 14:43:36 -0400 Subject: [PATCH 2/3] Gogs webhook processor. --- README.md | 23 ++++ bitrise.yml | 3 - service/hook/endpoint.go | 2 + service/hook/gogs/gogs.go | 139 +++++++++++++++++++++ service/hook/gogs/gogs_test.go | 219 +++++++++++++++++++++++++++++++++ 5 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 service/hook/gogs/gogs.go create mode 100644 service/hook/gogs/gogs_test.go diff --git a/README.md b/README.md index 4bf35236..742cea6c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ For more information check the *How to add support for a new Provider* section. * 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` +* [Gogs](https://gogs.io) + * handled on the path: `/h/gogs/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN` ### GitHub - setup & usage: @@ -78,6 +80,22 @@ a [GitLab](https://gitlab.com) *project*. That's all, the next time you push code (into your repository) a build will be triggered. +### Gogs - setup & usage: + +All you have to do is register your `bitrise-webhooks` URL as a Webhook in your [Gogs](https://gogs.io) repository. + +1. Open your *project* on your repository's hosting URL. +1. Go to `Settings` of the *project* +1. Select `Webhooks`, `Add Webhook`, then `Gogs`. +1. Specify the `bitrise-webhooks` URL (`.../h/gogs/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN`) in the `Payload URL` field. +1. Set the `Content Type` to `application/json`. +1. A Secret is not required at this time. +1. Set the trigger to be fired on `Just the push event` +1. Save the Webhook. + +That's all, the next time you push code (into your repository) a build will be triggered. + + ### Visual Studio Online / Visual Studio Team Services - setup & usage: All you have to do is register your `bitrise-webhooks` URL for @@ -334,3 +352,8 @@ response provider will be used. * Docker image: auto-create & publish a Docker Image for the webhooks server, to make it easy to run it on your own server * Bitbucket V1 (aka "Services" on the Bitbucket web UI) - not sure whether we should support this, it's already kind of deprecated, and we already support the newer, V2 webhooks. + +## Contributors + +* [The Bitrise Team](https://github.com/bitrise-io) +* [Chad Robinson](https://github.com/crrobinson14) diff --git a/bitrise.yml b/bitrise.yml index d800b9eb..f81a20da 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -59,9 +59,6 @@ workflows: # Go lint go get -u github.com/golang/lint/golint - - # Go Vet - go get -u golang.org/x/tools/cmd/vet test: before_run: - _install_test_tools diff --git a/service/hook/endpoint.go b/service/hook/endpoint.go index 1842c480..a4da2fa7 100644 --- a/service/hook/endpoint.go +++ b/service/hook/endpoint.go @@ -14,6 +14,7 @@ import ( hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" "github.com/bitrise-io/bitrise-webhooks/service/hook/github" "github.com/bitrise-io/bitrise-webhooks/service/hook/gitlab" + "github.com/bitrise-io/bitrise-webhooks/service/hook/gogs" "github.com/bitrise-io/bitrise-webhooks/service/hook/slack" "github.com/bitrise-io/bitrise-webhooks/service/hook/visualstudioteamservices" "github.com/gorilla/mux" @@ -26,6 +27,7 @@ func supportedProviders() map[string]hookCommon.Provider { "slack": slack.HookProvider{}, "visualstudio": visualstudioteamservices.HookProvider{}, "gitlab": gitlab.HookProvider{}, + "gogs": gogs.HookProvider{}, } } diff --git a/service/hook/gogs/gogs.go b/service/hook/gogs/gogs.go new file mode 100644 index 00000000..5c4a4f21 --- /dev/null +++ b/service/hook/gogs/gogs.go @@ -0,0 +1,139 @@ +package gogs + +// # Infos / notes: +// +// ## Webhook calls +// +// Official API docs: https://gogs.io/docs/features/webhook +// +// This module works very similarly to the Gitlab processor. +// Please look there for more discussion of its operation. + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" + hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" + "github.com/bitrise-io/go-utils/httputil" +) + +// -------------------------- +// --- Webhook Data Model --- + +const ( + pushEventID = "push" +) + +// CommitModel ... +type CommitModel struct { + CommitHash string `json:"id"` + CommitMessage string `json:"message"` +} + +// CodePushEventModel ... +type CodePushEventModel struct { + Secret string `json:"secret"` + Ref string `json:"ref"` + CheckoutSHA string `json:"after"` + Commits []CommitModel `json:"commits"` +} + +// --------------------------------------- +// --- Webhook Provider Implementation --- + +// HookProvider ... +type HookProvider struct{} + +func detectContentTypeAndEventID(header http.Header) (string, string, error) { + contentType, err := httputil.GetSingleValueFromHeader("Content-Type", header) + if err != nil { + return "", "", fmt.Errorf("Issue with Content-Type Header: %s", err) + } + + eventID, err := httputil.GetSingleValueFromHeader("X-Gogs-Event", header) + if err != nil { + return "", "", fmt.Errorf("Issue with X-Gogs-Event Header: %s", err) + } + + return contentType, eventID, nil +} + +func transformCodePushEvent(codePushEvent CodePushEventModel) hookCommon.TransformResultModel { + if !strings.HasPrefix(codePushEvent.Ref, "refs/heads/") { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Ref (%s) is not a head ref", codePushEvent.Ref), + ShouldSkip: true, + } + } + branch := strings.TrimPrefix(codePushEvent.Ref, "refs/heads/") + + lastCommit := CommitModel{} + isLastCommitFound := false + for _, aCommit := range codePushEvent.Commits { + if aCommit.CommitHash == codePushEvent.CheckoutSHA { + isLastCommitFound = true + lastCommit = aCommit + break + } + } + + if !isLastCommitFound { + return hookCommon.TransformResultModel{ + Error: errors.New("The commit specified by 'after' was not included in the 'commits' array - no match found"), + } + } + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + CommitHash: lastCommit.CommitHash, + CommitMessage: lastCommit.CommitMessage, + Branch: branch, + }, + }, + }, + } +} + +// TransformRequest ... +func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { + contentType, eventID, err := detectContentTypeAndEventID(r.Header) + if err != nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Issue with Headers: %s", err), + } + } + + if contentType != "application/json" { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Content-Type is not supported: %s", contentType), + } + } + + if eventID != "push" { + // Unsupported Event + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Unsupported Webhook event: %s", eventID), + } + } + + if r.Body == nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Failed to read content of request body: no or empty request body"), + } + } + + // code push + var codePushEvent CodePushEventModel + if contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&codePushEvent); err != nil { + return hookCommon.TransformResultModel{Error: fmt.Errorf("Failed to parse request body: %s", err)} + } + } + return transformCodePushEvent(codePushEvent) +} diff --git a/service/hook/gogs/gogs_test.go b/service/hook/gogs/gogs_test.go new file mode 100644 index 00000000..3fdbd701 --- /dev/null +++ b/service/hook/gogs/gogs_test.go @@ -0,0 +1,219 @@ +package gogs + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" + "github.com/stretchr/testify/require" +) + +func Test_detectContentTypeAndEventID(t *testing.T) { + t.Log("Code Push event") + { + header := http.Header{ + "X-Gogs-Event": {"push"}, + "Content-Type": {"application/json"}, + } + contentType, eventID, err := detectContentTypeAndEventID(header) + require.NoError(t, err) + require.Equal(t, "application/json", contentType) + require.Equal(t, "push", eventID) + } + + t.Log("Missing X-Gogs-Event header") + { + header := http.Header{ + "Content-Type": {"application/json"}, + } + contentType, eventID, err := detectContentTypeAndEventID(header) + require.EqualError(t, err, "Issue with X-Gogs-Event Header: No value found in HEADER for the key: X-Gogs-Event") + require.Equal(t, "", contentType) + require.Equal(t, "", eventID) + } + + t.Log("Missing Content-Type") + { + header := http.Header{ + "X-Gogs-Event": {"push"}, + } + contentType, eventID, err := detectContentTypeAndEventID(header) + require.EqualError(t, err, "Issue with Content-Type Header: No value found in HEADER for the key: Content-Type") + require.Equal(t, "", contentType) + require.Equal(t, "", eventID) + } +} + +func Test_transformCodePushEvent(t *testing.T) { + t.Log("Do Transform - single commit") + { + codePush := CodePushEventModel{ + Secret: "", + Ref: "refs/heads/master", + CheckoutSHA: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + Commits: []CommitModel{ + { + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + }, + }, + } + hookTransformResult := transformCodePushEvent(codePush) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + Branch: "master", + }, + }, + }, hookTransformResult.TriggerAPIParams) + } + + t.Log("Do Transform - multiple commits - CheckoutSHA match should trigger the build") + { + codePush := CodePushEventModel{ + Secret: "", + Ref: "refs/heads/master", + CheckoutSHA: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + Commits: []CommitModel{ + { + CommitHash: "7782203aaf0daabbd245ec0370c751eac6a4eb55", + CommitMessage: `switch to three component versions`, + }, + { + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + }, + { + CommitHash: "ef77f9dba498f335e2e7078bdb52f9e11c214c58", + CommitMessage: `get version : three component version`, + }, + }, + } + hookTransformResult := transformCodePushEvent(codePush) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + Branch: "master", + }, + }, + }, hookTransformResult.TriggerAPIParams) + } + + t.Log("No commit matches CheckoutSHA") + { + codePush := CodePushEventModel{ + Secret: "", + Ref: "refs/heads/master", + CheckoutSHA: "checkout-sha", + Commits: []CommitModel{ + { + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + }, + }, + } + hookTransformResult := transformCodePushEvent(codePush) + require.EqualError(t, hookTransformResult.Error, "The commit specified by 'after' was not included in the 'commits' array - no match found") + require.False(t, hookTransformResult.ShouldSkip) + require.Nil(t, hookTransformResult.TriggerAPIParams) + } + + t.Log("Not a head ref") + { + codePush := CodePushEventModel{ + Secret: "", + Ref: "refs/not/head", + CheckoutSHA: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + Commits: []CommitModel{ + { + CommitHash: "f8f37818dc89a67516adfc21896d0c9ec43d05c2", + CommitMessage: `Response: omit the "failed_responses" array if empty`, + }, + }, + } + hookTransformResult := transformCodePushEvent(codePush) + require.True(t, hookTransformResult.ShouldSkip) + require.EqualError(t, hookTransformResult.Error, "Ref (refs/not/head) is not a head ref") + require.Nil(t, hookTransformResult.TriggerAPIParams) + } +} + +func Test_HookProvider_TransformRequest(t *testing.T) { + provider := HookProvider{} + + const sampleCodePushData = `{ + "secret": "", + "ref": "refs/heads/develop", + "after": "1606d3dd4c4dc83ee8fed8d3cfd911da851bf740", + "commits": [ + { + "id": "29da60ce2c47a6696bc82f2e6ec4a075695eb7c3", + "message": "first commit message" + }, + { + "id": "1606d3dd4c4dc83ee8fed8d3cfd911da851bf740", + "message": "second commit message" + } + ] +}` + + t.Log("Code Push - should be handled") + { + request := http.Request{ + Header: http.Header{ + "X-Gogs-Event": {"push"}, + "Content-Type": {"application/json"}, + }, + Body: ioutil.NopCloser(strings.NewReader(sampleCodePushData)), + } + hookTransformResult := provider.TransformRequest(&request) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + CommitHash: "1606d3dd4c4dc83ee8fed8d3cfd911da851bf740", + CommitMessage: "second commit message", + Branch: "develop", + }, + }, + }, hookTransformResult.TriggerAPIParams) + } + + t.Log("Unsuported Content-Type") + { + request := http.Request{ + Header: http.Header{ + "X-Gogs-Event": {"push"}, + "Content-Type": {"not/supported"}, + }, + Body: ioutil.NopCloser(strings.NewReader(sampleCodePushData)), + } + hookTransformResult := provider.TransformRequest(&request) + require.False(t, hookTransformResult.ShouldSkip) + require.EqualError(t, hookTransformResult.Error, "Content-Type is not supported: not/supported") + } + + t.Log("No Request Body") + { + request := http.Request{ + Header: http.Header{ + "X-Gogs-Event": {"push"}, + "Content-Type": {"application/json"}, + }, + } + hookTransformResult := provider.TransformRequest(&request) + require.False(t, hookTransformResult.ShouldSkip) + require.EqualError(t, hookTransformResult.Error, "Failed to read content of request body: no or empty request body") + } +} From 99d35c616a4c08590e88100ebea91325fd3d70cd Mon Sep 17 00:00:00 2001 From: Viktor Benei Date: Wed, 27 Apr 2016 13:29:28 +0200 Subject: [PATCH 3/3] v1.1.4 --- version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/version.go b/version/version.go index bd24fe6a..d51b3c9d 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ package version // VERSION ... -const VERSION = "1.1.3" +const VERSION = "1.1.4"