diff --git a/cli/secret/secret_add.go b/cli/secret/secret_add.go index 7cd935396..16b75e0e0 100644 --- a/cli/secret/secret_add.go +++ b/cli/secret/secret_add.go @@ -102,5 +102,6 @@ func secretCreate(c *cli.Context) error { var defaultSecretEvents = []string{ woodpecker.EventPush, woodpecker.EventTag, + woodpecker.EventRelease, woodpecker.EventDeploy, } diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 3380e29fc..9536f0f34 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -3937,6 +3937,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "is_prerelease": { + "type": "boolean" + }, "message": { "type": "string" }, @@ -4383,6 +4386,7 @@ const docTemplate = `{ "pull_request", "pull_request_closed", "tag", + "release", "deployment", "cron", "manual" @@ -4392,6 +4396,7 @@ const docTemplate = `{ "EventPull", "EventPullClosed", "EventTag", + "EventRelease", "EventDeploy", "EventCron", "EventManual" diff --git a/docs/docs/20-usage/15-terminiology/index.md b/docs/docs/20-usage/15-terminiology/index.md index 9f24e4617..4e58b9630 100644 --- a/docs/docs/20-usage/15-terminiology/index.md +++ b/docs/docs/20-usage/15-terminiology/index.md @@ -38,6 +38,7 @@ - `pull_request`: A pull request event is triggered when a pull request is opened or a new commit is pushed to it. - `pull_request_closed`: A pull request closed event is triggered when a pull request is closed or merged. - `tag`: A tag event is triggered when a tag is pushed. +- `release`: A release event is triggered when a release is created. - `manual`: A manual event is triggered when a user manually triggers a pipeline. - `cron`: A cron event is triggered when a cron job is executed. diff --git a/docs/docs/20-usage/20-workflow-syntax.md b/docs/docs/20-usage/20-workflow-syntax.md index bb18e953e..a21360a81 100644 --- a/docs/docs/20-usage/20-workflow-syntax.md +++ b/docs/docs/20-usage/20-workflow-syntax.md @@ -269,7 +269,7 @@ when: #### `event` -Available events: `push`, `pull_request`, `pull_request_closed`, `tag`, `deployment`, `cron`, `manual` +Available events: `push`, `pull_request`, `pull_request_closed`, `tag`, `release`, `deployment`, `cron`, `manual` Execute a step if the build event is a `tag`: diff --git a/docs/docs/20-usage/50-environment.md b/docs/docs/20-usage/50-environment.md index 02cee4337..5e45ba0d4 100644 --- a/docs/docs/20-usage/50-environment.md +++ b/docs/docs/20-usage/50-environment.md @@ -77,6 +77,7 @@ This is the reference list of all environment variables available to your pipeli | `CI_COMMIT_AUTHOR` | commit author username | | `CI_COMMIT_AUTHOR_EMAIL` | commit author email address | | `CI_COMMIT_AUTHOR_AVATAR` | commit author avatar | +| `CI_COMMIT_PRERELEASE` | release is a pre-release (empty if event is not `release`) | | | **Current pipeline** | | `CI_PIPELINE_NUMBER` | pipeline number | | `CI_PIPELINE_PARENT` | number of parent pipeline | diff --git a/docs/docs/30-administration/11-forges/10-overview.md b/docs/docs/30-administration/11-forges/10-overview.md index 897d3d897..bacce1635 100644 --- a/docs/docs/30-administration/11-forges/10-overview.md +++ b/docs/docs/30-administration/11-forges/10-overview.md @@ -7,6 +7,7 @@ | Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Event: Release | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | Event: Deploy | :white_check_mark: | :x: | :x: | :x: | | [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [when.path filter](../../20-usage/20-workflow-syntax.md#path) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | diff --git a/pipeline/frontend/metadata/const.go b/pipeline/frontend/metadata/const.go index d7afbea07..a8777ffec 100644 --- a/pipeline/frontend/metadata/const.go +++ b/pipeline/frontend/metadata/const.go @@ -20,6 +20,7 @@ const ( EventPull = "pull_request" EventPullClosed = "pull_request_closed" EventTag = "tag" + EventRelease = "release" EventDeploy = "deployment" EventCron = "cron" EventManual = "manual" diff --git a/pipeline/frontend/metadata/environment.go b/pipeline/frontend/metadata/environment.go index b2629a9f7..dbb8ea56b 100644 --- a/pipeline/frontend/metadata/environment.go +++ b/pipeline/frontend/metadata/environment.go @@ -125,9 +125,12 @@ func (m *Metadata) Environ() map[string]string { // TODO Deprecated, remove in 3.x "CI_COMMIT_URL": m.Curr.ForgeURL, } - if m.Curr.Event == EventTag || strings.HasPrefix(m.Curr.Commit.Ref, "refs/tags/") { + if m.Curr.Event == EventTag || m.Curr.Event == EventRelease || strings.HasPrefix(m.Curr.Commit.Ref, "refs/tags/") { params["CI_COMMIT_TAG"] = strings.TrimPrefix(m.Curr.Commit.Ref, "refs/tags/") } + if m.Curr.Event == EventRelease { + params["CI_COMMIT_PRERELEASE"] = strconv.FormatBool(m.Curr.Commit.IsPrerelease) + } if m.Curr.Event == EventPull { params["CI_COMMIT_PULL_REQUEST"] = pullRegexp.FindString(m.Curr.Commit.Ref) params["CI_COMMIT_PULL_REQUEST_LABELS"] = strings.Join(m.Curr.Commit.PullRequestLabels, ",") diff --git a/pipeline/frontend/metadata/types.go b/pipeline/frontend/metadata/types.go index 6f28255c0..c75f321e6 100644 --- a/pipeline/frontend/metadata/types.go +++ b/pipeline/frontend/metadata/types.go @@ -69,6 +69,7 @@ type ( Author Author `json:"author,omitempty"` ChangedFiles []string `json:"changed_files,omitempty"` PullRequestLabels []string `json:"labels,omitempty"` + IsPrerelease bool `json:"is_prerelease,omitempty"` } // Author defines runtime metadata for a commit author. diff --git a/server/api/pipeline.go b/server/api/pipeline.go index 00a9f843a..b5ed9a11b 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -414,7 +414,7 @@ func PostPipeline(c *gin.Context) { if event, ok := c.GetQuery("event"); ok { pl.Event = model.WebhookEvent(event) - if err := model.ValidateWebhookEvent(pl.Event); err != nil { + if err := pl.Event.Validate(); err != nil { _ = c.AbortWithError(http.StatusBadRequest, err) return } diff --git a/server/forge/gitea/fixtures/hooks.go b/server/forge/gitea/fixtures/hooks.go index 397c9b40b..a551f91c3 100644 --- a/server/forge/gitea/fixtures/hooks.go +++ b/server/forge/gitea/fixtures/hooks.go @@ -1071,3 +1071,79 @@ const HookPullRequestClosed = ` "review": null } ` + +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/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index 084ca497e..30673e4d3 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -510,6 +510,15 @@ func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model. return nil, nil, err } + if pipeline != nil && pipeline.Event == model.EventRelease && pipeline.Commit == "" { + tagName := strings.Split(pipeline.Ref, "/")[2] + sha, err := c.getTagCommitSHA(ctx, repo, tagName) + if err != nil { + return nil, nil, err + } + pipeline.Commit = sha + } + if pipeline != nil && (pipeline.Event == model.EventPull || pipeline.Event == model.EventPullClosed) && len(pipeline.ChangedFiles) == 0 { index, err := strconv.ParseInt(strings.Split(pipeline.Ref, "/")[2], 10, 64) if err != nil { @@ -655,6 +664,36 @@ func (c *Gitea) getChangedFilesForPR(ctx context.Context, repo *model.Repo, inde }) } +func (c *Gitea) getTagCommitSHA(ctx context.Context, repo *model.Repo, tagName string) (string, error) { + _store, ok := store.TryFromContext(ctx) + if !ok { + log.Error().Msg("could not get store from context") + return "", nil + } + + repo, err := _store.GetRepoNameFallback(repo.ForgeRemoteID, repo.FullName) + if err != nil { + return "", err + } + + user, err := _store.GetUser(repo.UserID) + if err != nil { + return "", err + } + + client, err := c.newClientToken(ctx, user.Token) + if err != nil { + return "", err + } + + tag, _, err := client.GetTag(repo.Owner, repo.Name, tagName) + if err != nil { + return "", err + } + + return tag.Commit.SHA, nil +} + func (c *Gitea) perPage(ctx context.Context) int { if c.pageSize == 0 { client, err := c.newClientToken(ctx, "") diff --git a/server/forge/gitea/helper.go b/server/forge/gitea/helper.go index 44dcde56a..4383638c2 100644 --- a/server/forge/gitea/helper.go +++ b/server/forge/gitea/helper.go @@ -175,6 +175,25 @@ func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline { return pipeline } +func pipelineFromRelease(hook *releaseHook) *model.Pipeline { + avatar := expandAvatar( + hook.Repo.HTMLURL, + fixMalformedAvatar(hook.Sender.AvatarURL), + ) + + return &model.Pipeline{ + Event: model.EventRelease, + Ref: fmt.Sprintf("refs/tags/%s", hook.Release.TagName), + ForgeURL: hook.Release.HTMLURL, + Branch: hook.Release.Target, + Message: fmt.Sprintf("created release %s", hook.Release.Title), + Avatar: avatar, + Author: hook.Sender.UserName, + Sender: hook.Sender.UserName, + IsPrerelease: hook.Release.IsPrerelease, + } +} + // helper function that parses a push hook from a read closer. func parsePush(r io.Reader) (*pushHook, error) { push := new(pushHook) @@ -188,6 +207,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/forge/gitea/parse.go b/server/forge/gitea/parse.go index e007ab012..1fb8dacc6 100644 --- a/server/forge/gitea/parse.go +++ b/server/forge/gitea/parse.go @@ -31,6 +31,7 @@ const ( hookPush = "push" hookCreated = "create" hookPullRequest = "pull_request" + hookRelease = "release" actionOpen = "opened" actionSync = "synchronized" @@ -40,7 +41,7 @@ const ( refTag = "tag" ) -// parseHook parses a Gitea hook from an http.Request request and returns +// parseHook parses a Gitea hook from an http.Request and returns // Repo and Pipeline detail. If a hook type is unsupported nil values are returned. func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) { hookType := r.Header.Get(hookEvent) @@ -51,6 +52,8 @@ func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) { return parseCreatedHook(r.Body) case hookPullRequest: return parsePullRequestHook(r.Body) + case hookRelease: + return parseReleaseHook(r.Body) } log.Debug().Msgf("unsupported hook type: '%s'", hookType) return nil, nil, &types.ErrIgnoreEvent{Event: hookType} @@ -118,3 +121,20 @@ func parsePullRequestHook(payload io.Reader) (*model.Repo, *model.Pipeline, erro pipeline = pipelineFromPullRequest(pr) return repo, pipeline, err } + +// parseReleaseHook parses a release hook and returns the Repo and Pipeline details. +func parseReleaseHook(payload io.Reader) (*model.Repo, *model.Pipeline, error) { + var ( + repo *model.Repo + pipeline *model.Pipeline + ) + + release, err := parseRelease(payload) + if err != nil { + return nil, nil, err + } + + repo = toRepo(release.Repo) + pipeline = pipelineFromRelease(release) + return repo, pipeline, err +} diff --git a/server/forge/gitea/parse_test.go b/server/forge/gitea/parse_test.go index 6bca466e7..bda8e6b56 100644 --- a/server/forge/gitea/parse_test.go +++ b/server/forge/gitea/parse_test.go @@ -124,6 +124,17 @@ func Test_parser(t *testing.T) { g.Assert(err).IsNil() g.Assert(b.Event).Equal(model.EventPullClosed) }) + g.It("should handle release hook", 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/forge/gitea/types.go b/server/forge/gitea/types.go index b09d5daa1..2c27a324b 100644 --- a/server/forge/gitea/types.go +++ b/server/forge/gitea/types.go @@ -43,3 +43,10 @@ type pullRequestHook struct { Repo *gitea.Repository `json:"repository"` Sender *gitea.User `json:"sender"` } + +type releaseHook struct { + Action string `json:"action"` + Repo *gitea.Repository `json:"repository"` + Sender *gitea.User `json:"sender"` + Release *gitea.Release +} diff --git a/server/forge/github/fixtures/hooks.go b/server/forge/github/fixtures/hooks.go index 1c6403597..bae47d9e1 100644 --- a/server/forge/github/fixtures/hooks.go +++ b/server/forge/github/fixtures/hooks.go @@ -1391,3 +1391,178 @@ const HookPullRequestClosed = ` } } ` + +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/forge/github/github.go b/server/forge/github/github.go index e59e4cdc0..fb89a7448 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -582,6 +582,15 @@ func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model return nil, nil, err } + if pipeline != nil && pipeline.Event == model.EventRelease && pipeline.Commit == "" { + tagName := strings.Split(pipeline.Ref, "/")[2] + sha, err := c.getTagCommitSHA(ctx, repo, tagName) + if err != nil { + return nil, nil, err + } + pipeline.Commit = sha + } + if pull != nil && len(pipeline.ChangedFiles) == 0 { pipeline, err = c.loadChangedFilesFromPullRequest(ctx, pull, repo, pipeline) if err != nil { @@ -629,3 +638,49 @@ func (c *client) loadChangedFilesFromPullRequest(ctx context.Context, pull *gith return pipeline, err } + +func (c *client) getTagCommitSHA(ctx context.Context, repo *model.Repo, tagName string) (string, error) { + _store, ok := store.TryFromContext(ctx) + if !ok { + log.Error().Msg("could not get store from context") + return "", nil + } + + repo, err := _store.GetRepoNameFallback(repo.ForgeRemoteID, repo.FullName) + if err != nil { + return "", err + } + + user, err := _store.GetUser(repo.UserID) + if err != nil { + return "", err + } + + gh := c.newClientToken(ctx, user.Token) + if err != nil { + return "", err + } + + page := 1 + var tag *github.RepositoryTag + for { + tags, _, err := gh.Repositories.ListTags(ctx, repo.Owner, repo.Name, &github.ListOptions{Page: page}) + if err != nil { + return "", err + } + + for _, t := range tags { + if t.GetName() == tagName { + tag = t + break + } + } + if tag != nil { + break + } + } + if tag == nil { + return "", fmt.Errorf("could not find tag %s", tagName) + } + return tag.GetCommit().GetSHA(), nil +} diff --git a/server/forge/github/parse.go b/server/forge/github/parse.go index fa354a1db..9a69a79d2 100644 --- a/server/forge/github/parse.go +++ b/server/forge/github/parse.go @@ -32,9 +32,10 @@ import ( const ( hookField = "payload" - actionOpen = "opened" - actionClose = "closed" - actionSync = "synchronize" + actionOpen = "opened" + actionClose = "closed" + actionSync = "synchronize" + actionReleased = "released" stateOpen = "open" stateClose = "closed" @@ -68,6 +69,9 @@ func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, * return nil, repo, pipeline, nil case *github.PullRequestEvent: return parsePullHook(hook, merge) + case *github.ReleaseEvent: + repo, pipeline := parseReleaseHook(hook) + return nil, repo, pipeline, nil default: return nil, nil, nil, &types.ErrIgnoreEvent{Event: github.Stringify(hook)} } @@ -176,6 +180,33 @@ func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullReque return hook.GetPullRequest(), convertRepo(hook.GetRepo()), pipeline, nil } +// parseReleaseHook parses a release hook and returns the Repo and Pipeline +// details. +func parseReleaseHook(hook *github.ReleaseEvent) (*model.Repo, *model.Pipeline) { + if hook.GetAction() != actionReleased { + return nil, nil + } + + name := hook.GetRelease().GetName() + if name == "" { + name = hook.GetRelease().GetTagName() + } + + pipeline := &model.Pipeline{ + Event: model.EventRelease, + ForgeURL: hook.GetRelease().GetHTMLURL(), + Ref: fmt.Sprintf("refs/tags/%s", hook.GetRelease().GetTagName()), + Branch: hook.GetRelease().GetTargetCommitish(), + Message: fmt.Sprintf("created release %s", name), + Author: hook.GetRelease().GetAuthor().GetLogin(), + Avatar: hook.GetRelease().GetAuthor().GetAvatarURL(), + Sender: hook.GetSender().GetLogin(), + IsPrerelease: hook.GetRelease().GetPrerelease(), + } + + return convertRepo(hook.GetRepo()), pipeline +} + func getChangedFilesFromCommits(commits []*github.HeadCommit) []string { // assume a capacity of 4 changed files per commit files := make([]string, 0, len(commits)*4) diff --git a/server/forge/github/parse_test.go b/server/forge/github/parse_test.go index e3625dbee..bbd4fb57f 100644 --- a/server/forge/github/parse_test.go +++ b/server/forge/github/parse_test.go @@ -19,6 +19,7 @@ import ( "bytes" "net/http" "sort" + "strings" "testing" "github.com/franela/goblin" @@ -30,10 +31,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 { @@ -119,5 +121,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, false) + 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/forge/gitlab/convert.go b/server/forge/gitlab/convert.go index 2180c447b..da13ac2ef 100644 --- a/server/forge/gitlab/convert.go +++ b/server/forge/gitlab/convert.go @@ -240,6 +240,46 @@ func convertTagHook(hook *gitlab.TagEvent) (*model.Repo, *model.Pipeline, error) return repo, pipeline, nil } +func convertReleaseHook(hook *gitlab.ReleaseEvent) (*model.Repo, *model.Pipeline, error) { + repo := &model.Repo{} + + var err error + if repo.Owner, repo.Name, err = extractFromPath(hook.Project.PathWithNamespace); err != nil { + return nil, nil, err + } + + repo.ForgeRemoteID = model.ForgeRemoteID(fmt.Sprint(hook.Project.ID)) + repo.Avatar = "" + if hook.Project.AvatarURL != nil { + repo.Avatar = *hook.Project.AvatarURL + } + repo.ForgeURL = hook.Project.WebURL + repo.Clone = hook.Project.GitHTTPURL + repo.CloneSSH = hook.Project.GitSSHURL + repo.FullName = hook.Project.PathWithNamespace + repo.Branch = hook.Project.DefaultBranch + repo.IsSCMPrivate = hook.Project.VisibilityLevel > 10 + + pipeline := &model.Pipeline{ + Event: model.EventRelease, + Commit: hook.Commit.ID, + ForgeURL: hook.URL, + Message: fmt.Sprintf("created release %s", hook.Name), + Sender: hook.Commit.Author.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 it's a tag (git-plugin looks for it) + Ref: "refs/tags/" + hook.Tag, + } + if len(pipeline.Email) != 0 { + pipeline.Avatar = getUserAvatar(pipeline.Email) + } + + return repo, pipeline, nil +} + func getUserAvatar(email string) string { hasher := md5.New() hasher.Write([]byte(email)) diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 39a7cd5b7..d94da6526 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -625,6 +625,8 @@ func (g *GitLab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *mod return convertPushHook(event) case *gitlab.TagEvent: return convertTagHook(event) + case *gitlab.ReleaseEvent: + return convertReleaseHook(event) default: return nil, nil, &forge_types.ErrIgnoreEvent{Event: string(eventType)} } diff --git a/server/forge/gitlab/gitlab_test.go b/server/forge/gitlab/gitlab_test.go index e6ee0f77c..0c568c434 100644 --- a/server/forge/gitlab/gitlab_test.go +++ b/server/forge/gitlab/gitlab_test.go @@ -235,6 +235,23 @@ func Test_GitLab(t *testing.T) { assert.Len(t, pipeline.ChangedFiles, 0) // see L217 } }) + + g.It("Should parse release request hook", func() { + req, _ := http.NewRequest( + testdata.ServiceHookMethod, + testdata.ServiceHookURL.String(), + bytes.NewReader(testdata.WebhookReleaseBody), + ) + req.Header = testdata.ReleaseHookHeaders + + 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, "created release Awesome version 0.0.2", build.Message) + } + }) }) }) }) diff --git a/server/forge/gitlab/testdata/hooks.go b/server/forge/gitlab/testdata/hooks.go index d736420c8..7f5fe5569 100644 --- a/server/forge/gitlab/testdata/hooks.go +++ b/server/forge/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"}, + } ) // HookPush is payload of a push event @@ -599,3 +604,69 @@ var HookPullRequestMerged = []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/server/model/const.go b/server/model/const.go index fc0585896..152956c6c 100644 --- a/server/model/const.go +++ b/server/model/const.go @@ -27,6 +27,7 @@ const ( EventPull WebhookEvent = "pull_request" EventPullClosed WebhookEvent = "pull_request_closed" EventTag WebhookEvent = "tag" + EventRelease WebhookEvent = "release" EventDeploy WebhookEvent = "deployment" EventCron WebhookEvent = "cron" EventManual WebhookEvent = "manual" @@ -40,9 +41,9 @@ func (wel WebhookEventList) Less(i, j int) bool { return wel[i] < wel[j] } var ErrInvalidWebhookEvent = errors.New("invalid webhook event") -func ValidateWebhookEvent(s WebhookEvent) error { +func (s WebhookEvent) Validate() error { switch s { - case EventPush, EventPull, EventTag, EventDeploy, EventCron, EventManual: + case EventPush, EventPull, EventPullClosed, EventTag, EventRelease, EventDeploy, EventCron, EventManual: return nil default: return fmt.Errorf("%w: %s", ErrInvalidWebhookEvent, s) diff --git a/server/model/pipeline.go b/server/model/pipeline.go index e1eaabc99..410ba3f11 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -50,6 +50,7 @@ type Pipeline struct { ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` + IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` } // @name Pipeline // TableName return database table name for xorm diff --git a/server/model/secret.go b/server/model/secret.go index 4a941ca46..d5d97ca8a 100644 --- a/server/model/secret.go +++ b/server/model/secret.go @@ -116,7 +116,7 @@ var validDockerImageString = regexp.MustCompile( // Validate validates the required fields and formats. func (s *Secret) Validate() error { for _, event := range s.Events { - if err := ValidateWebhookEvent(event); err != nil { + if err := event.Validate(); err != nil { return errors.Join(err, ErrSecretEventInvalid) } } diff --git a/server/pipeline/create.go b/server/pipeline/create.go index 5182a4cbf..da467425c 100644 --- a/server/pipeline/create.go +++ b/server/pipeline/create.go @@ -43,7 +43,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline skipMatch := skipPipelineRegex.FindString(pipeline.Message) if len(skipMatch) > 0 { - log.Debug().Str("repo", repo.FullName).Msgf("ignoring pipeline as skip-ci was found in the commit (%s) message '%s'", pipeline.Commit, pipeline.Message) + ref := pipeline.Commit + if len(ref) == 0 { + ref = pipeline.Ref + } + log.Debug().Str("repo", repo.FullName).Msgf("ignoring pipeline as skip-ci was found in the commit (%s) message '%s'", ref, pipeline.Message) return nil, ErrFiltered } diff --git a/server/pipeline/stepbuilder/metadata.go b/server/pipeline/stepbuilder/metadata.go index 63643b6a0..2e2a3c106 100644 --- a/server/pipeline/stepbuilder/metadata.go +++ b/server/pipeline/stepbuilder/metadata.go @@ -129,6 +129,7 @@ func metadataPipelineFromModelPipeline(pipeline *model.Pipeline, includeParent b }, ChangedFiles: pipeline.ChangedFiles, PullRequestLabels: pipeline.PullRequestLabels, + IsPrerelease: pipeline.IsPrerelease, }, Cron: cron, } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index bc9110707..fbb89a8b2 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -267,7 +267,8 @@ "pr": "Pull Request", "deploy": "Deploy", "cron": "Cron", - "manual": "Manual" + "manual": "Manual", + "release": "Release" }, "status": { "status": "Status: {status}", diff --git a/web/src/components/repo/pipeline/PipelineItem.vue b/web/src/components/repo/pipeline/PipelineItem.vue index 7023bbfae..262a7585f 100644 --- a/web/src/components/repo/pipeline/PipelineItem.vue +++ b/web/src/components/repo/pipeline/PipelineItem.vue @@ -40,7 +40,7 @@ - + diff --git a/web/src/components/repo/pipeline/PipelineStepList.vue b/web/src/components/repo/pipeline/PipelineStepList.vue index 97494256b..9a6614e39 100644 --- a/web/src/components/repo/pipeline/PipelineStepList.vue +++ b/web/src/components/repo/pipeline/PipelineStepList.vue @@ -29,7 +29,7 @@ {{ prettyRef }}
- + {{ prettyRef }}
diff --git a/web/src/components/secrets/SecretEdit.vue b/web/src/components/secrets/SecretEdit.vue index 199313986..045ac5a77 100644 --- a/web/src/components/secrets/SecretEdit.vue +++ b/web/src/components/secrets/SecretEdit.vue @@ -102,6 +102,7 @@ function removeImage(image: string) { const secretEventsOptions: CheckboxOption[] = [ { value: WebhookEvents.Push, text: i18n.t('repo.pipeline.event.push') }, { value: WebhookEvents.Tag, text: i18n.t('repo.pipeline.event.tag') }, + { value: WebhookEvents.Release, text: i18n.t('repo.pipeline.event.release') }, { value: WebhookEvents.PullRequest, text: i18n.t('repo.pipeline.event.pr'), diff --git a/web/src/lib/api/types/webhook.ts b/web/src/lib/api/types/webhook.ts index 33aa24c7c..4b8ebb36b 100644 --- a/web/src/lib/api/types/webhook.ts +++ b/web/src/lib/api/types/webhook.ts @@ -1,6 +1,7 @@ export enum WebhookEvents { Push = 'push', Tag = 'tag', + Release = 'release', PullRequest = 'pull_request', PullRequestClosed = 'pull_request_closed', Deploy = 'deployment', diff --git a/woodpecker-go/woodpecker/const.go b/woodpecker-go/woodpecker/const.go index d85d0ce44..cf0792646 100644 --- a/woodpecker-go/woodpecker/const.go +++ b/woodpecker-go/woodpecker/const.go @@ -20,6 +20,7 @@ const ( EventPull = "pull_request" EventPullClosed = "pull_request_closed" EventTag = "tag" + EventRelease = "release" EventDeploy = "deployment" EventCron = "cron" EventManual = "manual"