Add release event trigger (#3226)

Supersedes #764 

Bitbucket does not support release webhooks.

---------

Co-authored-by: Patrick Schratz <patrick.schratz@gmail.com>
This commit is contained in:
qwerty287 2024-01-30 17:39:00 +01:00 committed by GitHub
parent da4bd8b97d
commit 9df572ef31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 628 additions and 18 deletions

View file

@ -102,5 +102,6 @@ func secretCreate(c *cli.Context) error {
var defaultSecretEvents = []string{
woodpecker.EventPush,
woodpecker.EventTag,
woodpecker.EventRelease,
woodpecker.EventDeploy,
}

View file

@ -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"

View file

@ -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.

View file

@ -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`:

View file

@ -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 |

View file

@ -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: |

View file

@ -20,6 +20,7 @@ const (
EventPull = "pull_request"
EventPullClosed = "pull_request_closed"
EventTag = "tag"
EventRelease = "release"
EventDeploy = "deployment"
EventCron = "cron"
EventManual = "manual"

View file

@ -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, ",")

View file

@ -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.

View file

@ -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
}

View file

@ -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"}
}
`

View file

@ -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, "")

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
})
})
})
}

View file

@ -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
}

View file

@ -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=="
}
}
`

View file

@ -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
}

View file

@ -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)

View file

@ -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()
})
})
})
}

View file

@ -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))

View file

@ -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)}
}

View file

@ -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)
}
})
})
})
})

View file

@ -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"
}
}
}
`)

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -129,6 +129,7 @@ func metadataPipelineFromModelPipeline(pipeline *model.Pipeline, includeParent b
},
ChangedFiles: pipeline.ChangedFiles,
PullRequestLabels: pipeline.PullRequestLabels,
IsPrerelease: pipeline.IsPrerelease,
},
Cron: cron,
}

View file

@ -267,7 +267,8 @@
"pr": "Pull Request",
"deploy": "Deploy",
"cron": "Cron",
"manual": "Manual"
"manual": "Manual",
"release": "Release"
},
"status": {
"status": "Status: {status}",

View file

@ -40,7 +40,7 @@
<Icon v-if="pipeline.event === 'pull_request'" name="pull-request" />
<Icon v-else-if="pipeline.event === 'pull_request_closed'" name="pull-request-closed" />
<Icon v-else-if="pipeline.event === 'deployment'" name="deployment" />
<Icon v-else-if="pipeline.event === 'tag'" name="tag" />
<Icon v-else-if="pipeline.event === 'tag' || pipeline.event === 'release'" name="tag" />
<Icon v-else-if="pipeline.event === 'cron'" name="push" />
<Icon v-else-if="pipeline.event === 'manual'" name="manual-pipeline" />
<Icon v-else name="push" />

View file

@ -29,7 +29,7 @@
<span class="truncate">{{ prettyRef }}</span>
</router-link>
<div v-else class="flex space-x-1 items-center min-w-0">
<Icon v-if="pipeline.event === 'tag'" name="tag" />
<Icon v-if="pipeline.event === 'tag' || pipeline.event === 'release'" name="tag" />
<span class="truncate">{{ prettyRef }}</span>
</div>

View file

@ -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'),

View file

@ -1,6 +1,7 @@
export enum WebhookEvents {
Push = 'push',
Tag = 'tag',
Release = 'release',
PullRequest = 'pull_request',
PullRequestClosed = 'pull_request_closed',
Deploy = 'deployment',

View file

@ -20,6 +20,7 @@ const (
EventPull = "pull_request"
EventPullClosed = "pull_request_closed"
EventTag = "tag"
EventRelease = "release"
EventDeploy = "deployment"
EventCron = "cron"
EventManual = "manual"