diff --git a/cli/secret/secret_add.go b/cli/secret/secret_add.go index 2a368fcf31..e175f6a4ad 100644 --- a/cli/secret/secret_add.go +++ b/cli/secret/secret_add.go @@ -91,5 +91,6 @@ func secretCreate(c *cli.Context) error { var defaultSecretEvents = []string{ woodpecker.EventPush, woodpecker.EventTag, + woodpecker.EventRelease, woodpecker.EventDeploy, } diff --git a/cmd/server/flags.go b/cmd/server/flags.go index 29e4b7ac01..91c12e460f 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -271,6 +271,12 @@ var flags = []cli.Flag{ Name: "github-skip-verify", Usage: "github skip ssl verification", }, + &cli.StringFlag{ + EnvVars: []string{"WOODPECKER_GITHUB_RELEASE_ACTIONS"}, + Name: "github-release-actions", + Usage: "On which actions to trigger a release pipeline", + Value: "prereleased, released", + }, // // Gogs // diff --git a/cmd/server/setup.go b/cmd/server/setup.go index a4b3da3c0d..ac9324935f 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -23,6 +23,7 @@ import ( "fmt" "net/url" "os" + "regexp" "strings" "time" @@ -276,13 +277,19 @@ func setupGitlab(c *cli.Context) (remote.Remote, error) { // helper function to setup the GitHub remote from the CLI arguments. func setupGithub(c *cli.Context) (remote.Remote, error) { + releaseActions := regexp. + MustCompile(`[\s,]+`). + Split(c.String("github-release-actions"), -1) + opts := github.Opts{ - URL: c.String("github-server"), - Client: c.String("github-client"), - Secret: c.String("github-secret"), - SkipVerify: c.Bool("github-skip-verify"), - MergeRef: c.Bool("github-merge-ref"), + URL: c.String("github-server"), + Client: c.String("github-client"), + Secret: c.String("github-secret"), + SkipVerify: c.Bool("github-skip-verify"), + MergeRef: c.Bool("github-merge-ref"), + ReleaseActions: releaseActions, } + log.Trace().Msgf("Remote (github) opts: %#v", opts) return github.New(opts) } diff --git a/docs/docs/30-administration/11-forges/10-overview.md b/docs/docs/30-administration/11-forges/10-overview.md index 7ce694c510..08c25059e3 100644 --- a/docs/docs/30-administration/11-forges/10-overview.md +++ b/docs/docs/30-administration/11-forges/10-overview.md @@ -8,6 +8,7 @@ | Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | | Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | | Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| Event: Release | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | OAuth | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | | [Multi pipeline](/docs/usage/multi-pipeline) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | | [when.path filter](/docs/usage/pipeline-syntax#path) | :white_check_mark: | :white_check_mark:ยน | :white_check_mark: | :x: | :x: | :x: | :x: | diff --git a/docs/docs/30-administration/11-forges/20-github.md b/docs/docs/30-administration/11-forges/20-github.md index 5d21a8a9f8..9f38d9784a 100644 --- a/docs/docs/30-administration/11-forges/20-github.md +++ b/docs/docs/30-administration/11-forges/20-github.md @@ -32,41 +32,55 @@ Please use this screenshot for reference: This is a full list of configuration options. Please note that many of these options use default configuration values that should work for the majority of installations. ### `WOODPECKER_GITHUB` + > Default: `false` Enables the GitHub driver. ### `WOODPECKER_GITHUB_URL` + > Default: `https://github.com` Configures the GitHub server address. ### `WOODPECKER_GITHUB_CLIENT` + > Default: empty Configures the GitHub OAuth client id. This is used to authorize access. ### `WOODPECKER_GITHUB_CLIENT_FILE` + > Default: empty Read the value for `WOODPECKER_GITHUB_CLIENT` from the specified filepath ### `WOODPECKER_GITHUB_SECRET` + > Default: empty Configures the GitHub OAuth client secret. This is used to authorize access. ### `WOODPECKER_GITHUB_SECRET_FILE` + > Default: empty Read the value for `WOODPECKER_GITHUB_SECRET` from the specified filepath ### `WOODPECKER_GITHUB_MERGE_REF` + > Default: `true` TODO ### `WOODPECKER_GITHUB_SKIP_VERIFY` + > Default: `false` Configure if SSL verification should be skipped. + +### `WOODPECKER_GITHUB_RELEASE_ACTIONS` + +> Default: `released, prereleased` + +The actions to allow on release (trigger), comma seperated. diff --git a/pipeline/frontend/metadata.go b/pipeline/frontend/metadata.go index 6601c066c7..b7a8e7dea9 100644 --- a/pipeline/frontend/metadata.go +++ b/pipeline/frontend/metadata.go @@ -10,10 +10,11 @@ import ( // Event types corresponding to scm hooks. const ( - EventPush = "push" - EventPull = "pull_request" - EventTag = "tag" - EventDeploy = "deployment" + EventPush = "push" + EventPull = "pull_request" + EventTag = "tag" + EventDeploy = "deployment" + EventRelease = "release" ) type ( @@ -187,7 +188,7 @@ func (m *Metadata) Environ() map[string]string { "CI_SYSTEM_ARCH": m.Sys.Platform, // TODO: remove after next version } - if m.Curr.Event == EventTag { + if m.Curr.Event == EventTag || m.Curr.Event == EventRelease { params["CI_COMMIT_TAG"] = strings.TrimPrefix(m.Curr.Commit.Ref, "refs/tags/") } if m.Curr.Event == EventPull { diff --git a/server/api/hook.go b/server/api/hook.go index 548dde88a4..f3e86791d6 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -94,7 +94,11 @@ func PostHook(c *gin.Context) { // wrapped in square brackets appear in the commit message skipMatch := skipRe.FindString(tmpBuild.Message) if len(skipMatch) > 0 { - msg := fmt.Sprintf("ignoring hook: %s found in %s", skipMatch, tmpBuild.Commit) + ref := tmpBuild.Commit + if len(ref) == 0 { + ref = tmpBuild.Ref + } + msg := fmt.Sprintf("ignoring hook: %s found in %s", skipMatch, ref) log.Debug().Msg(msg) c.String(http.StatusNoContent, msg) return diff --git a/server/model/const.go b/server/model/const.go index 6430c0d118..292ab42bda 100644 --- a/server/model/const.go +++ b/server/model/const.go @@ -17,15 +17,16 @@ package model type WebhookEvent string const ( - EventPush WebhookEvent = "push" - EventPull WebhookEvent = "pull_request" - EventTag WebhookEvent = "tag" - EventDeploy WebhookEvent = "deployment" + EventPush WebhookEvent = "push" + EventPull WebhookEvent = "pull_request" + EventTag WebhookEvent = "tag" + EventDeploy WebhookEvent = "deployment" + EventRelease WebhookEvent = "release" ) func ValidateWebhookEvent(s WebhookEvent) bool { switch s { - case EventPush, EventPull, EventTag, EventDeploy: + case EventPush, EventPull, EventTag, EventDeploy, EventRelease: return true default: return false diff --git a/server/remote/gitea/fixtures/hooks.go b/server/remote/gitea/fixtures/hooks.go index 30f1905326..4be723b7c9 100644 --- a/server/remote/gitea/fixtures/hooks.go +++ b/server/remote/gitea/fixtures/hooks.go @@ -157,3 +157,79 @@ const HookPullRequest = `{ "avatar_url": "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87" } }` + +const HookRelease = ` +{ + "action": "published", + "release": { + "id": 48, + "tag_name": "0.0.5", + "target_commitish": "main", + "name": "Version 0.0.5", + "body": "", + "url": "https://git.xxx/api/v1/repos/anbraten/demo/releases/48", + "html_url": "https://git.xxx/anbraten/demo/releases/tag/0.0.5", + "tarball_url": "https://git.xxx/anbraten/demo/archive/0.0.5.tar.gz", + "zipball_url": "https://git.xxx/anbraten/demo/archive/0.0.5.zip", + "draft": false, + "prerelease": false, + "created_at": "2022-02-09T20:23:05Z", + "published_at": "2022-02-09T20:23:05Z", + "author": {"id":1,"login":"anbraten","full_name":"Anton Bracke","email":"anbraten@noreply.xxx","avatar_url":"https://git.xxx/user/avatar/anbraten/-1","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-03-21T10:04:48Z","restricted":false,"active":false,"prohibit_login":false,"location":"world","website":"https://xxx","description":"","visibility":"public","followers_count":1,"following_count":1,"starred_repos_count":1,"username":"anbraten"}, + "assets": [] + }, + "repository": { + "id": 77, + "owner": {"id":1,"login":"anbraten","full_name":"Anton Bracke","email":"anbraten@noreply.xxx","avatar_url":"https://git.xxx/user/avatar/anbraten/-1","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-03-21T10:04:48Z","restricted":false,"active":false,"prohibit_login":false,"location":"world","website":"https://xxx","description":"","visibility":"public","followers_count":1,"following_count":1,"starred_repos_count":1,"username":"anbraten"}, + "name": "demo", + "full_name": "anbraten/demo", + "description": "", + "empty": false, + "private": true, + "fork": false, + "template": false, + "parent": null, + "mirror": false, + "size": 59, + "html_url": "https://git.xxx/anbraten/demo", + "ssh_url": "ssh://git@git.xxx:22/anbraten/demo.git", + "clone_url": "https://git.xxx/anbraten/demo.git", + "original_url": "", + "website": "", + "stars_count": 0, + "forks_count": 1, + "watchers_count": 1, + "open_issues_count": 2, + "open_pr_counter": 2, + "release_counter": 4, + "default_branch": "main", + "archived": false, + "created_at": "2021-08-30T20:54:13Z", + "updated_at": "2022-01-09T01:29:23Z", + "permissions": { + "admin": true, + "push": true, + "pull": true + }, + "has_issues": true, + "internal_tracker": { + "enable_time_tracker": true, + "allow_only_contributors_to_track_time": true, + "enable_issue_dependencies": true + }, + "has_wiki": false, + "has_pull_requests": true, + "has_projects": true, + "ignore_whitespace_conflicts": false, + "allow_merge_commits": true, + "allow_rebase": true, + "allow_rebase_explicit": true, + "allow_squash_merge": true, + "default_merge_style": "squash", + "avatar_url": "", + "internal": false, + "mirror_interval": "" + }, + "sender": {"id":1,"login":"anbraten","full_name":"Anbraten","email":"anbraten@noreply.xxx","avatar_url":"https://git.xxx/user/avatar/anbraten/-1","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-03-21T10:04:48Z","restricted":false,"active":false,"prohibit_login":false,"location":"World","website":"https://xxx","description":"","visibility":"public","followers_count":1,"following_count":1,"starred_repos_count":1,"username":"anbraten"} +} +` diff --git a/server/remote/gitea/helper.go b/server/remote/gitea/helper.go index 1132bb33b8..1f35b184f2 100644 --- a/server/remote/gitea/helper.go +++ b/server/remote/gitea/helper.go @@ -176,23 +176,40 @@ func buildFromPullRequest(hook *pullRequestHook) *model.Build { return build } -// helper function that extracts the Repository data from a Gitea push hook -func repoFromPush(hook *pushHook) *model.Repo { - return &model.Repo{ - Name: hook.Repo.Name, - Owner: hook.Repo.Owner.Username, - FullName: hook.Repo.FullName, - Link: hook.Repo.URL, +func buildFromRelease(hook *releaseHook) *model.Build { + avatar := expandAvatar( + hook.Repo.URL, + fixMalformedAvatar(hook.Sender.Avatar), + ) + author := hook.Sender.Login + if author == "" { + author = hook.Sender.Username + } + sender := hook.Sender.Username + if sender == "" { + sender = hook.Sender.Login + } + + return &model.Build{ + Event: model.EventRelease, + Ref: fmt.Sprintf("refs/tags/%s", hook.Release.TagName), + Link: fmt.Sprintf("%s/src/tag/%s", hook.Repo.URL, hook.Release.TagName), + Branch: fmt.Sprintf("refs/tags/%s", hook.Release.TagName), + Message: fmt.Sprintf("created release %s", hook.Release.Name), + Avatar: avatar, + Author: author, + Sender: sender, + Timestamp: time.Now().UTC().Unix(), } } // helper function that extracts the Repository data from a Gitea pull_request hook -func repoFromPullRequest(hook *pullRequestHook) *model.Repo { +func repoFromHook(repo *giteaRepo) *model.Repo { return &model.Repo{ - Name: hook.Repo.Name, - Owner: hook.Repo.Owner.Username, - FullName: hook.Repo.FullName, - Link: hook.Repo.URL, + Name: repo.Name, + Owner: repo.Owner.Username, + FullName: repo.FullName, + Link: repo.URL, } } @@ -209,6 +226,12 @@ func parsePullRequest(r io.Reader) (*pullRequestHook, error) { return pr, err } +func parseRelease(r io.Reader) (*releaseHook, error) { + pr := new(releaseHook) + err := json.NewDecoder(r).Decode(pr) + return pr, err +} + // fixMalformedAvatar is a helper function that fixes an avatar url if malformed // (currently a known bug with gitea) func fixMalformedAvatar(url string) string { diff --git a/server/remote/gitea/helper_test.go b/server/remote/gitea/helper_test.go index f462b54f5e..48b488ed4d 100644 --- a/server/remote/gitea/helper_test.go +++ b/server/remote/gitea/helper_test.go @@ -113,7 +113,7 @@ func Test_parse(t *testing.T) { g.It("Should return a Repo struct from a push hook", func() { buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) - repo := repoFromPush(hook) + repo := repoFromHook(&hook.Repo) g.Assert(repo.Name).Equal(hook.Repo.Name) g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) g.Assert(repo.FullName).Equal("gordon/hello-world") @@ -150,7 +150,7 @@ func Test_parse(t *testing.T) { g.It("Should return a Repo struct from a pull_request hook", func() { buf := bytes.NewBufferString(fixtures.HookPullRequest) hook, _ := parsePullRequest(buf) - repo := repoFromPullRequest(hook) + repo := repoFromHook(&hook.Repo) g.Assert(repo.Name).Equal(hook.Repo.Name) g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) g.Assert(repo.FullName).Equal("gordon/hello-world") diff --git a/server/remote/gitea/parse.go b/server/remote/gitea/parse.go index 3869cff0f4..10ee0bdd09 100644 --- a/server/remote/gitea/parse.go +++ b/server/remote/gitea/parse.go @@ -26,6 +26,7 @@ const ( hookEvent = "X-Gitea-Event" hookPush = "push" hookCreated = "create" + hookRelease = "release" hookPullRequest = "pull_request" actionOpen = "opened" @@ -47,7 +48,10 @@ func parseHook(r *http.Request) (*model.Repo, *model.Build, error) { return parseCreatedHook(r.Body) case hookPullRequest: return parsePullRequestHook(r.Body) + case hookRelease: + return parseReleaseHook(r.Body) } + return nil, nil, nil } @@ -69,7 +73,7 @@ func parsePushHook(payload io.Reader) (repo *model.Repo, build *model.Build, err return nil, nil, nil } - repo = repoFromPush(push) + repo = repoFromHook(&push.Repo) build = buildFromPush(push) return repo, build, err } @@ -86,7 +90,7 @@ func parseCreatedHook(payload io.Reader) (repo *model.Repo, build *model.Build, return nil, nil, nil } - repo = repoFromPush(push) + repo = repoFromHook(&push.Repo) build = buildFromTag(push) return repo, build, nil } @@ -111,7 +115,24 @@ func parsePullRequestHook(payload io.Reader) (*model.Repo, *model.Build, error) return nil, nil, nil } - repo = repoFromPullRequest(pr) + repo = repoFromHook(&pr.Repo) build = buildFromPullRequest(pr) return repo, build, err } + +// parsePullRequestHook parses a pull_request hook and returns the Repo and Build details. +func parseReleaseHook(payload io.Reader) (*model.Repo, *model.Build, error) { + var ( + repo *model.Repo + build *model.Build + ) + + release, err := parseRelease(payload) + if err != nil { + return nil, nil, err + } + + repo = repoFromHook(&release.Repo) + build = buildFromRelease(release) + return repo, build, err +} diff --git a/server/remote/gitea/parse_test.go b/server/remote/gitea/parse_test.go index 3193fd593a..9a9fad14f3 100644 --- a/server/remote/gitea/parse_test.go +++ b/server/remote/gitea/parse_test.go @@ -53,5 +53,18 @@ func Test_parser(t *testing.T) { g.Assert(utils.EqualStringSlice(b.ChangedFiles, []string{"CHANGELOG.md", "app/controller/application.rb"})).IsTrue() }) }) + g.Describe("given a release hook", func() { + g.It("should extract repository and build details", func() { + buf := bytes.NewBufferString(fixtures.HookRelease) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookRelease) + r, b, err := parseHook(req) + g.Assert(err).IsNil() + g.Assert(r).IsNotNil() + g.Assert(b).IsNotNil() + g.Assert(b.Event).Equal(model.EventRelease) + }) + }) }) } diff --git a/server/remote/gitea/types.go b/server/remote/gitea/types.go index e7c87cadb6..b381c8f3df 100644 --- a/server/remote/gitea/types.go +++ b/server/remote/gitea/types.go @@ -14,6 +14,34 @@ package gitea +type giteaUser struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + FullName string `json:"full_name"` + Email string `json:"email"` + Login string `json:"login"` + Avatar string `json:"avatar_url"` +} + +type giteaRepo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner giteaUser `json:"owner,omitempty"` +} + +type giteaSender struct { + ID int64 `json:"id"` + Login string `json:"login"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` +} + type pushHook struct { Sha string `json:"sha"` Ref string `json:"ref"` @@ -22,25 +50,9 @@ type pushHook struct { Compare string `json:"compare_url"` RefType string `json:"ref_type"` - Pusher struct { - Name string `json:"name"` - Email string `json:"email"` - Login string `json:"login"` - Username string `json:"username"` - } `json:"pusher"` - - Repo struct { - ID int64 `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - URL string `json:"html_url"` - Private bool `json:"private"` - Owner struct { - Name string `json:"name"` - Email string `json:"email"` - Username string `json:"username"` - } `json:"owner"` - } `json:"repository"` + Sender giteaSender `json:"sender"` + Repo giteaRepo `json:"repository"` + Pusher giteaUser `json:"pusher"` Commits []struct { ID string `json:"id"` @@ -50,19 +62,14 @@ type pushHook struct { Removed []string `json:"removed"` Modified []string `json:"modified"` } `json:"commits"` - - Sender struct { - ID int64 `json:"id"` - Login string `json:"login"` - Username string `json:"username"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - } `json:"sender"` } type pullRequestHook struct { - Action string `json:"action"` - Number int64 `json:"number"` + Action string `json:"action"` + Number int64 `json:"number"` + Repo giteaRepo `json:"repository"` + Sender giteaSender `json:"sender"` + PullRequest struct { ID int64 `json:"id"` User struct { @@ -84,18 +91,12 @@ type pullRequestHook struct { Ref string `json:"ref"` Sha string `json:"sha"` Repo struct { - ID int64 `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - URL string `json:"html_url"` - Private bool `json:"private"` - Owner struct { - ID int64 `json:"id"` - Username string `json:"username"` - Name string `json:"full_name"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - } `json:"owner"` + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner giteaUser `json:"owner"` } `json:"repo"` } `json:"base"` Head struct { @@ -103,41 +104,36 @@ type pullRequestHook struct { Ref string `json:"ref"` Sha string `json:"sha"` Repo struct { - ID int64 `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - URL string `json:"html_url"` - Private bool `json:"private"` - Owner struct { - ID int64 `json:"id"` - Username string `json:"username"` - Name string `json:"full_name"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - } `json:"owner"` + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner giteaUser `json:"owner"` } `json:"repo"` } `json:"head"` } `json:"pull_request"` - Repo struct { - ID int64 `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - URL string `json:"html_url"` - Private bool `json:"private"` - Owner struct { - ID int64 `json:"id"` - Username string `json:"username"` - Name string `json:"full_name"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - } `json:"owner"` - } `json:"repository"` - Sender struct { - ID int64 `json:"id"` - Login string `json:"login"` - Username string `json:"username"` - Name string `json:"full_name"` - Email string `json:"email"` - Avatar string `json:"avatar_url"` - } `json:"sender"` +} + +type releaseHook struct { + Action string `json:"action"` + Repo giteaRepo `json:"repository"` + Sender giteaSender `json:"sender"` + Release struct { + ID int64 `json:"id"` + TagName string `json:"tag_name"` + TargetCommitish string `json:"target_commitish"` + Name string `json:"name"` + Body string `json:"body"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + TarballURL string `json:"tarball_url"` + ZipballURL string `json:"zipball_url"` + Draft bool `json:"draft"` + Prerelease bool `json:"prerelease"` + CreatedAt string `json:"created_at"` + PublishedAt string `json:"published_at"` + Assets []string `json:"assets"` + Author giteaUser `json:"author"` + } } diff --git a/server/remote/github/fixtures/hooks.go b/server/remote/github/fixtures/hooks.go index be0bfaff80..74a85acb8a 100644 --- a/server/remote/github/fixtures/hooks.go +++ b/server/remote/github/fixtures/hooks.go @@ -352,3 +352,181 @@ const HookDeploy = ` } } ` + +// HookPush is a sample release hook. +// https://developer.github.com/v3/activity/events/types/#release +const HookRelease = ` +{ + "action": "released", + "release": { + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases/2", + "assets_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases/2/assets", + "upload_url": "https://octocoders.github.io/api/uploads/repos/Codertocat/Hello-World/releases/2/assets{?name,label}", + "html_url": "https://octocoders.github.io/Codertocat/Hello-World/releases/tag/0.0.1", + "id": 2, + "node_id": "MDc6UmVsZWFzZTI=", + "tag_name": "0.0.1", + "target_commitish": "master", + "name": null, + "draft": false, + "author": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "prerelease": false, + "created_at": "2019-05-15T19:37:08Z", + "published_at": "2019-05-15T19:38:20Z", + "assets": [], + "tarball_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tarball/0.0.1", + "zipball_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/zipball/0.0.1", + "body": null + }, + "repository": { + "id": 118, + "node_id": "MDEwOlJlcG9zaXRvcnkxMTg=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://octocoders.github.io/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World", + "forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks", + "keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events", + "assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges", + "archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T19:37:07Z", + "updated_at": "2019-05-15T19:38:15Z", + "pushed_at": "2019-05-15T19:38:19Z", + "git_url": "git://octocoders.github.io/Codertocat/Hello-World.git", + "ssh_url": "git@octocoders.github.io:Codertocat/Hello-World.git", + "clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git", + "svn_url": "https://octocoders.github.io/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "enterprise": { + "id": 1, + "slug": "github", + "name": "GitHub", + "node_id": "MDg6QnVzaW5lc3Mx", + "avatar_url": "https://octocoders.github.io/avatars/b/1?", + "description": null, + "website_url": null, + "html_url": "https://octocoders.github.io/businesses/github", + "created_at": "2019-05-14T19:31:12Z", + "updated_at": "2019-05-14T19:31:12Z" + }, + "sender": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 5, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ==" + } +} + +` diff --git a/server/remote/github/github.go b/server/remote/github/github.go index 0b90aaa80e..9c8df6e280 100644 --- a/server/remote/github/github.go +++ b/server/remote/github/github.go @@ -41,6 +41,17 @@ const ( defaultAPI = "https://api.github.com/" // Default GitHub API URL ) +// Release events. +/* + published: a release, pre-release, or draft of a release is published + unpublished: a release or pre-release is deleted + created: a draft is saved, or a release or pre-release is published without previously being saved as a draft + edited: a release, pre-release, or draft release is edited + deleted: a release, pre-release, or draft release is deleted + prereleased: a pre-release is created + released: a release or draft of a release is published, or a pre-release is changed to a release +*/ + // Opts defines configuration options. type Opts struct { URL string // GitHub server url. @@ -48,18 +59,25 @@ type Opts struct { Secret string // GitHub oauth client secret. SkipVerify bool // Skip ssl verification. MergeRef bool // Clone pull requests using the merge ref. + + /* + On which actions to trigger a release. + Can be: published, unpublished, created, edited, deleted, prereleased, released + */ + ReleaseActions []string } // New returns a Remote implementation that integrates with a GitHub Cloud or // GitHub Enterprise version control hosting provider. func New(opts Opts) (remote.Remote, error) { r := &client{ - API: defaultAPI, - URL: defaultURL, - Client: opts.Client, - Secret: opts.Secret, - SkipVerify: opts.SkipVerify, - MergeRef: opts.MergeRef, + API: defaultAPI, + URL: defaultURL, + Client: opts.Client, + Secret: opts.Secret, + SkipVerify: opts.SkipVerify, + MergeRef: opts.MergeRef, + ReleaseActions: opts.ReleaseActions, } if opts.URL != defaultURL { r.URL = strings.TrimSuffix(opts.URL, "/") @@ -70,12 +88,13 @@ func New(opts Opts) (remote.Remote, error) { } type client struct { - URL string - API string - Client string - Secret string - SkipVerify bool - MergeRef bool + URL string + API string + Client string + Secret string + SkipVerify bool + MergeRef bool + ReleaseActions []string // On which actions to trigger a release. } // Name returns the string name of this driver @@ -496,7 +515,7 @@ func (c *client) Branches(ctx context.Context, u *model.User, r *model.Repo) ([] // Hook parses the post-commit hook from the Request body // and returns the required data in a standard format. func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) { - pull, repo, build, err := parseHook(r, c.MergeRef) + pull, repo, build, err := parseHook(r, c) if err != nil { return nil, nil, err } diff --git a/server/remote/github/parse.go b/server/remote/github/parse.go index d58e4c1625..12a9cd40f8 100644 --- a/server/remote/github/parse.go +++ b/server/remote/github/parse.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/google/go-github/v39/github" + "github.com/rs/zerolog/log" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/shared/utils" @@ -38,7 +39,7 @@ const ( // parseHook parses a GitHub hook from an http.Request request and returns // Repo and Build detail. If a hook type is unsupported nil values are returned. -func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, *model.Build, error) { +func parseHook(r *http.Request, c *client) (*github.PullRequest, *model.Repo, *model.Build, error) { var reader io.Reader = r.Body if payload := r.FormValue(hookField); payload != "" { @@ -50,7 +51,8 @@ func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, * return nil, nil, nil, err } - payload, err := github.ParseWebHook(github.WebHookType(r), raw) + var webhookType string = github.WebHookType(r) + payload, err := github.ParseWebHook(webhookType, raw) if err != nil { return nil, nil, nil, err } @@ -62,10 +64,34 @@ func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, * case *github.DeploymentEvent: repo, build, err := parseDeployHook(hook) return nil, repo, build, err + case *github.ReleaseEvent: + hasAction := false + println(*hook.Action) + for _, actionType := range c.ReleaseActions { + if actionType == *hook.Action { + hasAction = true + break + } + } + if !hasAction { + log.Debug(). + Str("Hook", webhookType). + Str("Action", *hook.Action). + Msg( + "Github release action ignored. " + + "See WOODPECKER_GITHUB_RELEASE_ACTIONS") + return nil, nil, nil, nil + } + repo, build, err := parseReleaseHook(hook) + return nil, repo, build, err case *github.PullRequestEvent: - return parsePullHook(hook, merge) + return parsePullHook(hook, c.MergeRef) + default: + log.Debug(). + Str("Hook", webhookType). + Msg("Github event ignored, and will not be parsed") + return nil, nil, nil, nil } - return nil, nil, nil, nil } // parsePushHook parses a push hook and returns the Repo and Build details. @@ -142,6 +168,36 @@ func parseDeployHook(hook *github.DeploymentEvent) (*model.Repo, *model.Build, e return convertRepo(hook.GetRepo()), build, nil } +// parseDeployHook parses a deployment and returns the Repo and Build details. +// If the commit type is unsupported nil values are returned. +func parseReleaseHook(hook *github.ReleaseEvent) (*model.Repo, *model.Build, error) { + release := hook.GetRelease() + + build := &model.Build{ + Event: model.EventRelease, + // Commit: "", + // Cannot retrieve the commit since + // it is hidden in the tag. It seems that github dose + // not provide the commit SHA with a release. + Created: release.CreatedAt.UTC().Unix(), + Link: release.GetURL(), + Message: "Release (" + hook.GetAction() + "):" + release.GetName(), // Use the body of the release. There is no message. + Title: release.GetName(), + // Tag name here is the ref. We should add the refs/tags so + // it is known its a tag (git-plugin looks for it) + Ref: "refs/tags/" + release.GetTagName(), + // Branch: *release.TagName, Dose not exist here. Github releases + // is always a tag + + Avatar: hook.GetSender().GetAvatarURL(), + Author: hook.GetSender().GetLogin(), + Sender: hook.GetSender().GetLogin(), + Remote: hook.GetRepo().GetCloneURL(), + } + + return convertRepo(hook.GetRepo()), build, nil +} + // parsePullHook parses a pull request hook and returns the Repo and Build // details. If the pull request is closed nil values are returned. func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullRequest, *model.Repo, *model.Build, error) { diff --git a/server/remote/github/parse_test.go b/server/remote/github/parse_test.go index 862a3c0a08..8a9f726e57 100644 --- a/server/remote/github/parse_test.go +++ b/server/remote/github/parse_test.go @@ -18,6 +18,7 @@ import ( "bytes" "net/http" "sort" + "strings" "testing" "github.com/franela/goblin" @@ -27,10 +28,11 @@ import ( ) const ( - hookEvent = "X-Github-Event" - hookDeploy = "deployment" - hookPush = "push" - hookPull = "pull_request" + hookEvent = "X-Github-Event" + hookDeploy = "deployment" + hookPush = "push" + hookPull = "pull_request" + hookRelease = "release" ) func testHookRequest(payload []byte, event string) *http.Request { @@ -44,9 +46,13 @@ func testHookRequest(payload []byte, event string) *http.Request { func Test_parser(t *testing.T) { g := goblin.Goblin(t) g.Describe("GitHub parser", func() { + client := &client{ + ReleaseActions: []string{"released", "prereleased"}, + MergeRef: false, + } g.It("should ignore unsupported hook events", func() { req := testHookRequest([]byte(fixtures.HookPullRequest), "issues") - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(r).IsNil() g.Assert(b).IsNil() g.Assert(err).IsNil() @@ -56,7 +62,7 @@ func Test_parser(t *testing.T) { g.Describe("given a push hook", func() { g.It("should skip when action is deleted", func() { req := testHookRequest([]byte(fixtures.HookPushDeleted), hookPush) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(r).IsNil() g.Assert(b).IsNil() g.Assert(err).IsNil() @@ -64,7 +70,7 @@ func Test_parser(t *testing.T) { }) g.It("should extract repository and build details", func() { req := testHookRequest([]byte(fixtures.HookPush), hookPush) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(err).IsNil() g.Assert(p).IsNil() g.Assert(r).IsNotNil() @@ -78,7 +84,7 @@ func Test_parser(t *testing.T) { g.Describe("given a pull request hook", func() { g.It("should skip when action is not open or sync", func() { req := testHookRequest([]byte(fixtures.HookPullRequestInvalidAction), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(r).IsNil() g.Assert(b).IsNil() g.Assert(err).IsNil() @@ -86,7 +92,7 @@ func Test_parser(t *testing.T) { }) g.It("should skip when state is not open", func() { req := testHookRequest([]byte(fixtures.HookPullRequestInvalidState), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(r).IsNil() g.Assert(b).IsNil() g.Assert(err).IsNil() @@ -94,7 +100,7 @@ func Test_parser(t *testing.T) { }) g.It("should extract repository and build details", func() { req := testHookRequest([]byte(fixtures.HookPullRequest), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(err).IsNil() g.Assert(r).IsNotNil() g.Assert(b).IsNotNil() @@ -106,7 +112,7 @@ func Test_parser(t *testing.T) { g.Describe("given a deployment hook", func() { g.It("should extract repository and build details", func() { req := testHookRequest([]byte(fixtures.HookDeploy), hookDeploy) - p, r, b, err := parseHook(req, false) + p, r, b, err := parseHook(req, client) g.Assert(err).IsNil() g.Assert(r).IsNotNil() g.Assert(b).IsNotNil() @@ -114,5 +120,19 @@ func Test_parser(t *testing.T) { g.Assert(b.Event).Equal(model.EventDeploy) }) }) + + g.Describe("given a release hook", func() { + g.It("should extract repository and build details", func() { + req := testHookRequest([]byte(fixtures.HookRelease), hookRelease) + p, r, b, err := parseHook(req, client) + g.Assert(err).IsNil() + g.Assert(r).IsNotNil() + g.Assert(b).IsNotNil() + g.Assert(p).IsNil() + g.Assert(b.Event).Equal(model.EventRelease) + g.Assert(len(strings.Split(b.Ref, "/")) == 3).IsTrue() + g.Assert(strings.HasPrefix(b.Ref, "refs/tags/")).IsTrue() + }) + }) }) } diff --git a/server/remote/gitlab/convert.go b/server/remote/gitlab/convert.go index 1e14bc08be..7a9d576442 100644 --- a/server/remote/gitlab/convert.go +++ b/server/remote/gitlab/convert.go @@ -227,6 +227,41 @@ func convertTagHook(hook *gitlab.TagEvent) (*model.Repo, *model.Build, error) { return repo, build, nil } +func convertReleaseHook(hook *gitlab.ReleaseEvent) (*model.Repo, *model.Build, error) { + repo := &model.Repo{ + Name: hook.Project.Name, + Link: hook.Project.WebURL, + Clone: hook.Project.GitHTTPURL, + FullName: hook.Project.PathWithNamespace, + Branch: hook.Project.DefaultBranch, + IsSCMPrivate: hook.Project.VisibilityLevel > 10, + } + + build := &model.Build{ + Event: model.EventRelease, + Commit: hook.Commit.ID, + Link: hook.URL, + Message: "Release (" + hook.Action + "):" + hook.Name, + Title: hook.Name, + Author: hook.Commit.Author.Name, + Email: hook.Commit.Author.Email, + + // Tag name here is the ref. We should add the refs/tags so + // it is known its a tag (git-plugin looks for it) + Ref: "refs/tags/" + hook.Tag, + } + + repo.IsSCMPrivate = false + if hook.Project.VisibilityLevel > 10 { + repo.IsSCMPrivate = true + } + + build.Commit = hook.Commit.ID + build.Title = hook.Name + + return repo, build, nil +} + func getUserAvatar(email string) string { hasher := md5.New() hasher.Write([]byte(email)) diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index 2b14f65cbc..73929d8448 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -21,6 +21,7 @@ import ( "io" "net/http" "net/url" + "reflect" "strings" "time" @@ -554,13 +555,17 @@ func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod return nil, nil, err } - parsed, err := gitlab.ParseWebhook(gitlab.WebhookEventType(req), payload) + gitlabEventType := gitlab.WebhookEventType(req) + println("Received event: " + gitlabEventType) + parsed, err := gitlab.ParseWebhook(gitlabEventType, payload) + println("Parsed event: ", parsed) if err != nil { return nil, nil, err } switch event := parsed.(type) { case *gitlab.MergeEvent: + println("Processing merge hook: " + reflect.TypeOf(*event).Name()) mergeIID, repo, build, err := convertMergeRequestHook(event, req) if err != nil { return nil, nil, err @@ -569,12 +574,18 @@ func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod if build, err = g.loadChangedFilesFromMergeRequest(ctx, repo, build, mergeIID); err != nil { return nil, nil, err } - return repo, build, nil case *gitlab.PushEvent: + println("Processing push hook: " + reflect.TypeOf(*event).Name()) return convertPushHook(event) case *gitlab.TagEvent: + println("Processing tag hook: " + reflect.TypeOf(*event).Name()) return convertTagHook(event) + case *gitlab.ReleaseEvent: + println("Processing release hook: " + reflect.TypeOf(*event).Name()) + // will create a run for all release types. + // TODO: add support for event action filtering. + return convertReleaseHook(event) default: return nil, nil, nil } diff --git a/server/remote/gitlab/gitlab_test.go b/server/remote/gitlab/gitlab_test.go index 8a3ed7381b..a01952e4a5 100644 --- a/server/remote/gitlab/gitlab_test.go +++ b/server/remote/gitlab/gitlab_test.go @@ -227,6 +227,26 @@ func Test_Gitlab(t *testing.T) { } }) }) + + g.Describe("Release request hook", func() { + g.It("Should parse release request hook", func() { + req, _ := http.NewRequest( + testdata.ServiceHookMethod, + testdata.ServiceHookURL.String(), + bytes.NewReader(testdata.WebhookReleaseBody), + ) + req.Header = testdata.ReleaseHookHeaders + + // TODO: insert fake store into context to retrieve user & repo, this will activate fetching of ChangedFiles + hookRepo, build, err := client.Hook(ctx, req) + assert.NoError(t, err) + if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) { + assert.Equal(t, "refs/tags/0.0.2", build.Ref) + assert.Equal(t, "ci", hookRepo.Name) + assert.Equal(t, "Awesome version 0.0.2", build.Title) + } + }) + }) }) }) } diff --git a/server/remote/gitlab/testdata/hooks.go b/server/remote/gitlab/testdata/hooks.go index 7d39306b62..86b1c75117 100644 --- a/server/remote/gitlab/testdata/hooks.go +++ b/server/remote/gitlab/testdata/hooks.go @@ -29,6 +29,11 @@ var ( "User-Agent": []string{"GitLab/14.3.0"}, "X-Gitlab-Event": []string{"Service Hook"}, } + ReleaseHookHeaders = http.Header{ + "Content-Type": []string{"application/json"}, + "User-Agent": []string{"GitLab/14.3.0"}, + "X-Gitlab-Event": []string{"Release Hook"}, + } ) // ServiceHookPushBody is payload of ServiceHook: Push @@ -314,3 +319,70 @@ var WebhookMergeRequestBody = []byte(`{ ] } `) + +var WebhookReleaseBody = []byte(` +{ + "id": 4268085, + "created_at": "2022-02-09 20:19:09 UTC", + "description": "new version desc", + "name": "Awesome version 0.0.2", + "released_at": "2022-02-09 20:19:09 UTC", + "tag": "0.0.2", + "object_kind": "release", + "project": { + "id": 32521798, + "name": "ci", + "description": "", + "web_url": "https://gitlab.com/anbratens-test/ci", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:anbratens-test/ci.git", + "git_http_url": "https://gitlab.com/anbratens-test/ci.git", + "namespace": "anbratens-test", + "visibility_level": 0, + "path_with_namespace": "anbratens-test/ci", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/anbratens-test/ci", + "url": "git@gitlab.com:anbratens-test/ci.git", + "ssh_url": "git@gitlab.com:anbratens-test/ci.git", + "http_url": "https://gitlab.com/anbratens-test/ci.git" + }, + "url": "https://gitlab.com/anbratens-test/ci/-/releases/0.0.2", + "action": "create", + "assets": { + "count": 4, + "links": [ + + ], + "sources": [ + { + "format": "zip", + "url": "https://gitlab.com/anbratens-test/ci/-/archive/0.0.2/ci-0.0.2.zip" + }, + { + "format": "tar.gz", + "url": "https://gitlab.com/anbratens-test/ci/-/archive/0.0.2/ci-0.0.2.tar.gz" + }, + { + "format": "tar.bz2", + "url": "https://gitlab.com/anbratens-test/ci/-/archive/0.0.2/ci-0.0.2.tar.bz2" + }, + { + "format": "tar", + "url": "https://gitlab.com/anbratens-test/ci/-/archive/0.0.2/ci-0.0.2.tar" + } + ] + }, + "commit": { + "id": "0b8c02955ba445ea70d22824d9589678852e2b93", + "message": "Initial commit", + "title": "Initial commit", + "timestamp": "2022-01-03T10:39:51+00:00", + "url": "https://gitlab.com/anbratens-test/ci/-/commit/0b8c02955ba445ea70d22824d9589678852e2b93", + "author": { + "name": "Anbraten", + "email": "2251488-anbraten@users.noreply.gitlab.com" + } + } +} +`) diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 77cdcc4be8..983347ac50 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -179,7 +179,8 @@ "push": "Push", "tag": "Tag", "pr": "Pull Request", - "deploy": "Deploy" + "deploy": "Deploy", + "release": "Release" } } }, diff --git a/web/src/components/secrets/SecretEdit.vue b/web/src/components/secrets/SecretEdit.vue index 99c594b397..d807bd04e0 100644 --- a/web/src/components/secrets/SecretEdit.vue +++ b/web/src/components/secrets/SecretEdit.vue @@ -111,6 +111,7 @@ export default defineComponent({ description: i18n.t('repo.settings.secrets.events.pr_warning'), }, { value: WebhookEvents.Deploy, text: i18n.t('repo.build.event.deploy') }, + { value: WebhookEvents.Release, text: i18n.t('repo.build.event.release') }, ]; function save() { diff --git a/web/src/lib/api/types/webhook.ts b/web/src/lib/api/types/webhook.ts index 9b121f6574..7b1b318a5e 100644 --- a/web/src/lib/api/types/webhook.ts +++ b/web/src/lib/api/types/webhook.ts @@ -3,4 +3,5 @@ export enum WebhookEvents { Tag = 'tag', PullRequest = 'pull_request', Deploy = 'deployment', + Release = 'release', } diff --git a/woodpecker-go/woodpecker/const.go b/woodpecker-go/woodpecker/const.go index e77fb9c7fe..5ea6da85de 100644 --- a/woodpecker-go/woodpecker/const.go +++ b/woodpecker-go/woodpecker/const.go @@ -2,10 +2,11 @@ package woodpecker // Event values. const ( - EventPush = "push" - EventPull = "pull_request" - EventTag = "tag" - EventDeploy = "deployment" + EventPush = "push" + EventPull = "pull_request" + EventTag = "tag" + EventDeploy = "deployment" + EventRelease = "release" ) // Status values.