From 220b666666d3e39316f14bbedf5380386b3701a1 Mon Sep 17 00:00:00 2001 From: Alexandre Couedelo Date: Tue, 13 Jun 2023 11:08:54 +0100 Subject: [PATCH 1/2] feat: implement gitlab release webhook https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#release-events --- gitlab/gitlab.go | 79 ++++++------ gitlab/gitlab_test.go | 201 ++++++++++++++++------------- gitlab/payload.go | 37 ++++++ testdata/gitlab/release-event.json | 70 ++++++++++ 4 files changed, 262 insertions(+), 125 deletions(-) create mode 100644 testdata/gitlab/release-event.json diff --git a/gitlab/gitlab.go b/gitlab/gitlab.go index a6bb11e..3926871 100644 --- a/gitlab/gitlab.go +++ b/gitlab/gitlab.go @@ -25,43 +25,44 @@ var ( // GitLab hook types const ( - PushEvents Event = "Push Hook" - TagEvents Event = "Tag Push Hook" - IssuesEvents Event = "Issue Hook" - ConfidentialIssuesEvents Event = "Confidential Issue Hook" - CommentEvents Event = "Note Hook" - ConfidentialCommentEvents Event = "Confidential Note Hook" - MergeRequestEvents Event = "Merge Request Hook" - WikiPageEvents Event = "Wiki Page Hook" - PipelineEvents Event = "Pipeline Hook" - BuildEvents Event = "Build Hook" - JobEvents Event = "Job Hook" - DeploymentEvents Event = "Deployment Hook" - SystemHookEvents Event = "System Hook" - objectPush string = "push" - objectTag string = "tag_push" - objectMergeRequest string = "merge_request" - objectBuild string = "build" - eventProjectCreate string = "project_create" - eventProjectDestroy string = "project_destroy" - eventProjectRename string = "project_rename" - eventProjectTransfer string = "project_transfer" - eventProjectUpdate string = "project_update" - eventUserAddToTeam string = "user_add_to_team" - eventUserRemoveFromTeam string = "user_remove_from_team" - eventUserUpdateForTeam string = "user_update_for_team" - eventUserCreate string = "user_create" - eventUserDestroy string = "user_destroy" - eventUserFailedLogin string = "user_failed_login" - eventUserRename string = "user_rename" - eventKeyCreate string = "key_create" - eventKeyDestroy string = "key_destroy" - eventGroupCreate string = "group_create" - eventGroupDestroy string = "group_destroy" - eventGroupRename string = "group_rename" - eventUserAddToGroup string = "user_add_to_group" - eventUserRemoveFromGroup string = "user_remove_from_group" - eventUserUpdateForGroup string = "user_update_for_group" + PushEvents Event = "Push Hook" + TagEvents Event = "Tag Push Hook" + IssuesEvents Event = "Issue Hook" + ConfidentialIssuesEvents Event = "Confidential Issue Hook" + CommentEvents Event = "Note Hook" + ConfidentialCommentEvents Event = "Confidential Note Hook" + MergeRequestEvents Event = "Merge Request Hook" + WikiPageEvents Event = "Wiki Page Hook" + PipelineEvents Event = "Pipeline Hook" + BuildEvents Event = "Build Hook" + JobEvents Event = "Job Hook" + DeploymentEvents Event = "Deployment Hook" + ReleaseEvents Event = "Release Hook" + SystemHookEvents Event = "System Hook" + objectPush string = "push" + objectTag string = "tag_push" + objectMergeRequest string = "merge_request" + objectBuild string = "build" + eventProjectCreate string = "project_create" + eventProjectDestroy string = "project_destroy" + eventProjectRename string = "project_rename" + eventProjectTransfer string = "project_transfer" + eventProjectUpdate string = "project_update" + eventUserAddToTeam string = "user_add_to_team" + eventUserRemoveFromTeam string = "user_remove_from_team" + eventUserUpdateForTeam string = "user_update_for_team" + eventUserCreate string = "user_create" + eventUserDestroy string = "user_destroy" + eventUserFailedLogin string = "user_failed_login" + eventUserRename string = "user_rename" + eventKeyCreate string = "key_create" + eventKeyDestroy string = "key_destroy" + eventGroupCreate string = "group_create" + eventGroupDestroy string = "group_destroy" + eventGroupRename string = "group_rename" + eventUserAddToGroup string = "user_add_to_group" + eventUserRemoveFromGroup string = "user_remove_from_group" + eventUserUpdateForGroup string = "user_update_for_group" ) // Option is a configuration option for the webhook @@ -354,6 +355,10 @@ func eventParsing(gitLabEvent Event, events []Event, payload []byte) (interface{ return nil, fmt.Errorf("unknown system hook event %s", gitLabEvent) } } + case ReleaseEvents: + var pl ReleaseEventPayload + err := json.Unmarshal([]byte(payload), &pl) + return pl, err default: return nil, fmt.Errorf("unknown event %s", gitLabEvent) } diff --git a/gitlab/gitlab_test.go b/gitlab/gitlab_test.go index 6ecc5bc..4d71c12 100644 --- a/gitlab/gitlab_test.go +++ b/gitlab/gitlab_test.go @@ -94,23 +94,27 @@ func TestBadRequests(t *testing.T) { for _, tt := range tests { tc := tt client := &http.Client{} - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var parseError error - server := newServer(func(w http.ResponseWriter, r *http.Request) { - _, parseError = hook.Parse(r, tc.event) - }) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, tc.payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") + t.Run( + tt.name, func(t *testing.T) { + t.Parallel() + var parseError error + server := newServer( + func(w http.ResponseWriter, r *http.Request) { + _, parseError = hook.Parse(r, tc.event) + }, + ) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, tc.payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - assert.NoError(err) - assert.Equal(http.StatusOK, resp.StatusCode) - assert.Error(parseError) - }) + resp, err := client.Do(req) + assert.NoError(err) + assert.Equal(http.StatusOK, resp.StatusCode) + assert.Error(parseError) + }, + ) } } @@ -249,37 +253,50 @@ func TestWebhooks(t *testing.T) { "X-Gitlab-Event": []string{"Deployment Hook"}, }, }, + { + name: "ReleaseEvent", + event: ReleaseEvents, + typ: ReleaseEventPayload{}, + filename: "../testdata/gitlab/release-event.json", + headers: http.Header{ + "X-Gitlab-Event": []string{"Release Hook"}, + }, + }, } 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() - }() + 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+path, payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") + 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+path, payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") - 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)) - }) + 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)) + }, + ) } } @@ -306,32 +323,36 @@ func TestJobHooks(t *testing.T) { 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() - }() + 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.events...) - }) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") + var parseError error + var results interface{} + server := newServer( + func(w http.ResponseWriter, r *http.Request) { + results, parseError = hook.Parse(r, tc.events...) + }, + ) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") - 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)) - }) + 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)) + }, + ) } } @@ -485,31 +506,35 @@ func TestSystemHooks(t *testing.T) { 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() - }() + 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, SystemHookEvents, tc.event) - }) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) - assert.NoError(err) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") - req.Header.Set("X-Gitlab-Event", "System Hook") + var parseError error + var results interface{} + server := newServer( + func(w http.ResponseWriter, r *http.Request) { + results, parseError = hook.Parse(r, SystemHookEvents, tc.event) + }, + ) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) + assert.NoError(err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") + req.Header.Set("X-Gitlab-Event", "System Hook") - 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)) - }) + 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)) + }, + ) } } diff --git a/gitlab/payload.go b/gitlab/payload.go index 9d668ee..06afc93 100644 --- a/gitlab/payload.go +++ b/gitlab/payload.go @@ -485,6 +485,43 @@ type GroupMemberUpdatedEventPayload struct { UserID int64 `json:"user_id"` } +// ReleaseEventPayload contains the information about GitLab's release event +type ReleaseEventPayload struct { + ID int `json:"id"` + CreatedAt customTime `json:"created_at"` + Description string `json:"description"` + Name string `json:"name"` + ReleasedAt customTime `json:"released_at"` + Tag string `json:"tag"` + ObjectKind string `json:"object_kind"` + Project Project `json:"project"` + URL string `json:"url"` + Action string `json:"action"` + Assets Assets `json:"assets"` +} + +// Assets represent artefacts and links associated to a release +type Assets struct { + Count int `json:"count"` + Links []Link `json:"links"` + Sources []AssetSource `json:"sources"` +} + +// Link represent a generic html link +type Link struct { + ID int `json:"id"` + External bool `json:"external"` + LinkType string `json:"link_type"` + Name string `json:"name"` + URL string `json:"url"` +} + +// AssetSource represent the download url for an asset +type AssetSource struct { + Format string `json:"format"` + URL string `json:"url"` +} + // Issue contains all of the GitLab issue information type Issue struct { ID int64 `json:"id"` diff --git a/testdata/gitlab/release-event.json b/testdata/gitlab/release-event.json new file mode 100644 index 0000000..c900700 --- /dev/null +++ b/testdata/gitlab/release-event.json @@ -0,0 +1,70 @@ +{ + "id": 1, + "created_at": "2020-11-02 12:55:12 UTC", + "description": "v1.1 has been released", + "name": "v1.1", + "released_at": "2020-11-02 12:55:12 UTC", + "tag": "v1.1", + "object_kind": "release", + "project": { + "id": 2, + "name": "release-webhook-example", + "description": "", + "web_url": "https://example.com/gitlab-org/release-webhook-example", + "avatar_url": null, + "git_ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git", + "git_http_url": "https://example.com/gitlab-org/release-webhook-example.git", + "namespace": "Gitlab", + "visibility_level": 0, + "path_with_namespace": "gitlab-org/release-webhook-example", + "default_branch": "master", + "ci_config_path": null, + "homepage": "https://example.com/gitlab-org/release-webhook-example", + "url": "ssh://git@example.com/gitlab-org/release-webhook-example.git", + "ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git", + "http_url": "https://example.com/gitlab-org/release-webhook-example.git" + }, + "url": "https://example.com/gitlab-org/release-webhook-example/-/releases/v1.1", + "action": "create", + "assets": { + "count": 5, + "links": [ + { + "id": 1, + "external": true, + "link_type": "other", + "name": "Changelog", + "url": "https://example.net/changelog" + } + ], + "sources": [ + { + "format": "zip", + "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.zip" + }, + { + "format": "tar.gz", + "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.gz" + }, + { + "format": "tar.bz2", + "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.bz2" + }, + { + "format": "tar", + "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar" + } + ] + }, + "commit": { + "id": "ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8", + "message": "Release v1.1", + "title": "Release v1.1", + "timestamp": "2020-10-31T14:58:32+11:00", + "url": "https://example.com/gitlab-org/release-webhook-example/-/commit/ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8", + "author": { + "name": "Example User", + "email": "user@example.com" + } + } +} \ No newline at end of file From 4766a275acc63def9434b162f5bbe4883f9309af Mon Sep 17 00:00:00 2001 From: Alexandre Couedelo Date: Tue, 10 Oct 2023 10:58:05 +0100 Subject: [PATCH 2/2] chore: the gitlab merge request payload is outdated updated the structs based on https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#merge-request-events --- gitlab/gitlab_test.go | 192 +++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 104 deletions(-) diff --git a/gitlab/gitlab_test.go b/gitlab/gitlab_test.go index 4d71c12..69e7f6c 100644 --- a/gitlab/gitlab_test.go +++ b/gitlab/gitlab_test.go @@ -94,27 +94,23 @@ func TestBadRequests(t *testing.T) { for _, tt := range tests { tc := tt client := &http.Client{} - t.Run( - tt.name, func(t *testing.T) { - t.Parallel() - var parseError error - server := newServer( - func(w http.ResponseWriter, r *http.Request) { - _, parseError = hook.Parse(r, tc.event) - }, - ) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, tc.payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var parseError error + server := newServer(func(w http.ResponseWriter, r *http.Request) { + _, parseError = hook.Parse(r, tc.event) + }) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, tc.payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - assert.NoError(err) - assert.Equal(http.StatusOK, resp.StatusCode) - assert.Error(parseError) - }, - ) + resp, err := client.Do(req) + assert.NoError(err) + assert.Equal(http.StatusOK, resp.StatusCode) + assert.Error(parseError) + }) } } @@ -267,36 +263,32 @@ func TestWebhooks(t *testing.T) { 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() - }() + 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+path, payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") + 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+path, payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") - 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)) - }, - ) + 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)) + }) } } @@ -323,36 +315,32 @@ func TestJobHooks(t *testing.T) { 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() - }() + 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.events...) - }, - ) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) - assert.NoError(err) - req.Header = tc.headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") + var parseError error + var results interface{} + server := newServer(func(w http.ResponseWriter, r *http.Request) { + results, parseError = hook.Parse(r, tc.events...) + }) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") - 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)) - }, - ) + 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)) + }) } } @@ -506,35 +494,31 @@ func TestSystemHooks(t *testing.T) { 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() - }() + 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, SystemHookEvents, tc.event) - }, - ) - defer server.Close() - req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) - assert.NoError(err) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Gitlab-Token", "sampleToken!") - req.Header.Set("X-Gitlab-Event", "System Hook") + var parseError error + var results interface{} + server := newServer(func(w http.ResponseWriter, r *http.Request) { + results, parseError = hook.Parse(r, SystemHookEvents, tc.event) + }) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) + assert.NoError(err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Gitlab-Token", "sampleToken!") + req.Header.Set("X-Gitlab-Event", "System Hook") - 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)) - }, - ) + 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)) + }) } }