diff --git a/api/build/compile_publish.go b/api/build/compile_publish.go index 2cca915d5..3b1a99da8 100644 --- a/api/build/compile_publish.go +++ b/api/build/compile_publish.go @@ -447,6 +447,19 @@ func CompileAndPublish( // getPRNumberFromBuild is a helper function to // extract the pull request number from a Build. func getPRNumberFromBuild(b *types.Build) (int, error) { + // on PR merges and closures, grab number from message + if b.GetEventAction() == constants.ActionMerged && b.GetEvent() == constants.EventPull { + numStr := strings.TrimPrefix(b.GetMessage(), "Merged PR #") + + return strconv.Atoi(numStr) + } + + if b.GetEventAction() == constants.ActionClosed && b.GetEvent() == constants.EventPull { + numStr := strings.TrimPrefix(b.GetMessage(), "Closed PR #") + + return strconv.Atoi(numStr) + } + // parse out pull request number from base ref // // pattern: refs/pull/1/head diff --git a/api/types/actions/pull.go b/api/types/actions/pull.go index 1635709d5..2338e10f0 100644 --- a/api/types/actions/pull.go +++ b/api/types/actions/pull.go @@ -13,6 +13,8 @@ type Pull struct { Reopened *bool `json:"reopened"` Labeled *bool `json:"labeled"` Unlabeled *bool `json:"unlabeled"` + Merged *bool `json:"merged"` + Closed *bool `json:"closed"` } // FromMask returns the Pull type resulting from the provided integer mask. @@ -23,6 +25,8 @@ func (a *Pull) FromMask(mask int64) *Pull { a.SetReopened(mask&constants.AllowPullReopen > 0) a.SetLabeled(mask&constants.AllowPullLabel > 0) a.SetUnlabeled(mask&constants.AllowPullUnlabel > 0) + a.SetMerged(mask&constants.AllowPullMerged > 0) + a.SetClosed(mask&constants.AllowPullClosedUnmerged > 0) return a } @@ -55,6 +59,14 @@ func (a *Pull) ToMask() int64 { mask = mask | constants.AllowPullUnlabel } + if a.GetMerged() { + mask = mask | constants.AllowPullMerged + } + + if a.GetClosed() { + mask = mask | constants.AllowPullClosedUnmerged + } + return mask } @@ -124,6 +136,28 @@ func (a *Pull) GetUnlabeled() bool { return *a.Unlabeled } +// GetMerged returns the Merged field from the provided Pull. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (a *Pull) GetMerged() bool { + // return zero value if Pull type or Merged field is nil + if a == nil || a.Merged == nil { + return false + } + + return *a.Merged +} + +// GetClosed returns the Closed field from the provided Pull. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (a *Pull) GetClosed() bool { + // return zero value if Pull type or Closed field is nil + if a == nil || a.Closed == nil { + return false + } + + return *a.Closed +} + // SetOpened sets the Pull Opened field. // // When the provided Pull type is nil, it @@ -201,3 +235,29 @@ func (a *Pull) SetUnlabeled(v bool) { a.Unlabeled = &v } + +// SetMerged sets the Pull Merged field. +// +// When the provided Pull type is nil, it +// will set nothing and immediately return. +func (a *Pull) SetMerged(v bool) { + // return if Pull type is nil + if a == nil { + return + } + + a.Merged = &v +} + +// SetClosed sets the Pull Closed field. +// +// When the provided Pull type is nil, it +// will set nothing and immediately return. +func (a *Pull) SetClosed(v bool) { + // return if Pull type is nil + if a == nil { + return + } + + a.Closed = &v +} diff --git a/api/types/actions/pull_test.go b/api/types/actions/pull_test.go index 4be4b0b60..60e15af53 100644 --- a/api/types/actions/pull_test.go +++ b/api/types/actions/pull_test.go @@ -50,6 +50,14 @@ func TestActions_Pull_Getters(t *testing.T) { if test.actions.GetUnlabeled() != test.want.GetUnlabeled() { t.Errorf("GetUnlabeled is %v, want %v", test.actions.GetUnlabeled(), test.want.GetUnlabeled()) } + + if test.actions.GetMerged() != test.want.GetMerged() { + t.Errorf("GetMerged is %v, want %v", test.actions.GetMerged(), test.want.GetMerged()) + } + + if test.actions.GetClosed() != test.want.GetClosed() { + t.Errorf("GetClosed is %v, want %v", test.actions.GetClosed(), test.want.GetClosed()) + } } } @@ -80,6 +88,8 @@ func TestActions_Pull_Setters(t *testing.T) { test.actions.SetReopened(test.want.GetReopened()) test.actions.SetLabeled(test.want.GetLabeled()) test.actions.SetUnlabeled(test.want.GetUnlabeled()) + test.actions.SetMerged(test.want.GetMerged()) + test.actions.SetClosed(test.want.GetClosed()) if test.actions.GetOpened() != test.want.GetOpened() { t.Errorf("SetOpened is %v, want %v", test.actions.GetOpened(), test.want.GetOpened()) @@ -104,6 +114,14 @@ func TestActions_Pull_Setters(t *testing.T) { if test.actions.GetUnlabeled() != test.want.GetUnlabeled() { t.Errorf("SetUnlabeled is %v, want %v", test.actions.GetUnlabeled(), test.want.GetUnlabeled()) } + + if test.actions.GetMerged() != test.want.GetMerged() { + t.Errorf("SetMerged is %v, want %v", test.actions.GetMerged(), test.want.GetMerged()) + } + + if test.actions.GetClosed() != test.want.GetClosed() { + t.Errorf("SetClosed is %v, want %v", test.actions.GetClosed(), test.want.GetClosed()) + } } } @@ -125,7 +143,7 @@ func TestActions_Pull_ToMask(t *testing.T) { // setup types actions := testPull() - want := int64(constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen | constants.AllowPullUnlabel) + want := int64(constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen | constants.AllowPullUnlabel | constants.AllowPullMerged) // run test got := actions.ToMask() @@ -143,6 +161,8 @@ func testPull() *Pull { pr.SetReopened(true) pr.SetLabeled(false) pr.SetUnlabeled(true) + pr.SetMerged(true) + pr.SetClosed(false) return pr } diff --git a/api/types/actions/push_test.go b/api/types/actions/push_test.go index 1d1be093a..15bce2af6 100644 --- a/api/types/actions/push_test.go +++ b/api/types/actions/push_test.go @@ -129,6 +129,7 @@ func testMask() int64 { constants.AllowPullSync | constants.AllowPullReopen | constants.AllowPullUnlabel | + constants.AllowPullMerged | constants.AllowDeployCreate | constants.AllowCommentCreate | constants.AllowSchedule, diff --git a/api/types/build.go b/api/types/build.go index bc14d2e53..0880eae7b 100644 --- a/api/types/build.go +++ b/api/types/build.go @@ -144,7 +144,13 @@ func (b *Build) Environment(workspace, channel string) map[string]string { // check if the Build event is comment if strings.EqualFold(b.GetEvent(), constants.EventComment) { // capture the pull request number - number := ToString(strings.SplitN(b.GetRef(), "/", 4)[2]) + var number string + + split := strings.SplitN(b.GetRef(), "/", 4) + + if len(split) == 4 { + number = ToString(split[2]) + } // add the pull request number to the list envs["BUILD_PULL_REQUEST_NUMBER"] = number @@ -184,7 +190,19 @@ func (b *Build) Environment(workspace, channel string) map[string]string { // check if the Build event is pull_request if strings.EqualFold(b.GetEvent(), constants.EventPull) { // capture the pull request number - number := ToString(strings.SplitN(b.GetRef(), "/", 4)[2]) + var number string + + if b.GetEventAction() == constants.ActionMerged { + number = strings.TrimPrefix(b.GetMessage(), "Merged PR #") + } else if b.GetEventAction() == constants.ActionClosed { + number = strings.TrimPrefix(b.GetMessage(), "Closed PR #") + } else { + split := strings.SplitN(b.GetRef(), "/", 4) + + if len(split) == 4 { + number = ToString(split[2]) + } + } // add the pull request number to the list envs["BUILD_PULL_REQUEST_NUMBER"] = number @@ -198,7 +216,13 @@ func (b *Build) Environment(workspace, channel string) map[string]string { // check if the Build event is tag if strings.EqualFold(b.GetEvent(), constants.EventTag) { // capture the tag reference - tag := ToString(strings.SplitN(b.GetRef(), "refs/tags/", 2)[1]) + var tag string + + split := strings.SplitN(b.GetRef(), "refs/tags/", 2) + + if len(split) == 2 { + tag = ToString(split[1]) + } // add the tag reference to the list envs["BUILD_TAG"] = tag diff --git a/api/types/events.go b/api/types/events.go index 0bc0b10ee..0d7d45564 100644 --- a/api/types/events.go +++ b/api/types/events.go @@ -74,6 +74,10 @@ func NewEventsFromSlice(events []string) (*Events, error) { mask = mask | constants.AllowPullLabel case constants.EventPull + ":" + constants.ActionUnlabeled: mask = mask | constants.AllowPullUnlabel + case constants.EventPull + ":" + constants.ActionMerged: + mask = mask | constants.AllowPullMerged + case constants.EventPull + ":" + constants.ActionClosed: + mask = mask | constants.AllowPullClosedUnmerged // deployment actions case constants.EventDeploy, constants.EventDeployAlternate, constants.EventDeploy + ":" + constants.ActionCreated: @@ -124,6 +128,10 @@ func (e *Events) Allowed(event, action string) bool { allowed = e.GetPullRequest().GetLabeled() case constants.EventPull + ":" + constants.ActionUnlabeled: allowed = e.GetPullRequest().GetUnlabeled() + case constants.EventPull + ":" + constants.ActionMerged: + allowed = e.GetPullRequest().GetMerged() + case constants.EventPull + ":" + constants.ActionClosed: + allowed = e.GetPullRequest().GetClosed() case constants.EventTag: allowed = e.GetPush().GetTag() case constants.EventComment + ":" + constants.ActionCreated: @@ -176,6 +184,14 @@ func (e *Events) List() []string { eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionUnlabeled) } + if e.GetPullRequest().GetMerged() { + eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionMerged) + } + + if e.GetPullRequest().GetClosed() { + eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionClosed) + } + if e.GetPush().GetTag() { eventSlice = append(eventSlice, constants.EventTag) } diff --git a/api/types/events_test.go b/api/types/events_test.go index 882364f97..c79a554ce 100644 --- a/api/types/events_test.go +++ b/api/types/events_test.go @@ -124,6 +124,7 @@ func TestTypes_Events_List(t *testing.T) { "pull_request:synchronize", "pull_request:reopened", "pull_request:unlabeled", + "pull_request:merged", "tag", "comment:created", "schedule", @@ -133,6 +134,7 @@ func TestTypes_Events_List(t *testing.T) { wantTwo := []string{ "pull_request:edited", "pull_request:labeled", + "pull_request:closed", "deployment", "comment:edited", "delete:tag", @@ -162,6 +164,7 @@ func TestTypes_Events_NewEventsFromMask_ToDatabase(t *testing.T) { constants.AllowPullSync | constants.AllowPullReopen | constants.AllowPullUnlabel | + constants.AllowPullMerged | constants.AllowCommentCreate | constants.AllowSchedule, ) @@ -171,6 +174,7 @@ func TestTypes_Events_NewEventsFromMask_ToDatabase(t *testing.T) { constants.AllowPullEdit | constants.AllowCommentEdit | constants.AllowPullLabel | + constants.AllowPullClosedUnmerged | constants.AllowDeployCreate, ) @@ -215,13 +219,13 @@ func Test_NewEventsFromSlice(t *testing.T) { }{ { name: "action specific events to e1", - events: []string{"push:branch", "push:tag", "delete:branch", "pull_request:opened", "pull_request:synchronize", "pull_request:reopened", "comment:created", "schedule:run", "pull_request:unlabeled"}, + events: []string{"push:branch", "push:tag", "delete:branch", "pull_request:opened", "pull_request:synchronize", "pull_request:reopened", "pull_request:unlabeled", "pull_request:merged", "comment:created", "schedule:run"}, want: e1, failure: false, }, { name: "action specific events to e2", - events: []string{"delete:tag", "pull_request:edited", "deployment:created", "comment:edited", "pull_request:labeled"}, + events: []string{"delete:tag", "pull_request:edited", "pull_request:closed", "deployment:created", "comment:edited", "pull_request:labeled"}, want: e2, failure: false, }, @@ -242,6 +246,8 @@ func Test_NewEventsFromSlice(t *testing.T) { Synchronize: &tBool, Labeled: &fBool, Unlabeled: &fBool, + Merged: &fBool, + Closed: &fBool, }, Deployment: &actions.Deploy{ Created: &tBool, @@ -273,6 +279,8 @@ func Test_NewEventsFromSlice(t *testing.T) { Synchronize: &tBool, Labeled: &fBool, Unlabeled: &fBool, + Merged: &fBool, + Closed: &fBool, }, Deployment: &actions.Deploy{ Created: &fBool, @@ -340,6 +348,8 @@ func TestTypes_Events_Allowed(t *testing.T) { {event: "pull_request", action: "reopened", want: true}, {event: "pull_request", action: "labeled", want: false}, {event: "pull_request", action: "unlabeled", want: true}, + {event: "pull_request", action: "merged", want: true}, + {event: "pull_request", action: "closed", want: false}, {event: "deployment", action: "created", want: false}, {event: "comment", action: "created", want: true}, {event: "comment", action: "edited", want: false}, @@ -381,6 +391,8 @@ func testEvents() (*Events, *Events) { Reopened: &tBool, Labeled: &fBool, Unlabeled: &tBool, + Merged: &tBool, + Closed: &fBool, }, Deployment: &actions.Deploy{ Created: &fBool, @@ -408,6 +420,8 @@ func testEvents() (*Events, *Events) { Reopened: &fBool, Labeled: &tBool, Unlabeled: &fBool, + Merged: &fBool, + Closed: &tBool, }, Deployment: &actions.Deploy{ Created: &tBool, diff --git a/api/types/repo_test.go b/api/types/repo_test.go index 1231a64b1..9f8bdb50b 100644 --- a/api/types/repo_test.go +++ b/api/types/repo_test.go @@ -16,7 +16,7 @@ func TestTypes_Repo_Environment(t *testing.T) { // setup types want := map[string]string{ "VELA_REPO_ACTIVE": "true", - "VELA_REPO_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,tag,comment:created,schedule,delete:branch", + "VELA_REPO_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,pull_request:merged,tag,comment:created,schedule,delete:branch", "VELA_REPO_BRANCH": "main", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "10", @@ -34,7 +34,7 @@ func TestTypes_Repo_Environment(t *testing.T) { "VELA_REPO_APPROVAL_TIMEOUT": "7", "VELA_REPO_OWNER": "octocat", "REPOSITORY_ACTIVE": "true", - "REPOSITORY_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,tag,comment:created,schedule,delete:branch", + "REPOSITORY_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,pull_request:merged,tag,comment:created,schedule,delete:branch", "REPOSITORY_BRANCH": "main", "REPOSITORY_CLONE": "https://github.com/github/octocat.git", "REPOSITORY_FULL_NAME": "github/octocat", diff --git a/api/webhook/post.go b/api/webhook/post.go index 570933b85..9e2b5248a 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -171,6 +171,40 @@ func PostWebhook(c *gin.Context) { h, r, b := webhook.Hook, webhook.Repo, webhook.Build + // check if repo was parsed from webhook + if r == nil { + retErr := fmt.Errorf("%s: failed to parse repo from webhook", baseErr) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture parsed repo from webhook + repo, err := database.FromContext(c).GetRepoForOrg(ctx, r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get repo %s: %w", baseErr, r.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // verify the webhook from the source control provider + if c.Value("webhookvalidation").(bool) { + err = scm.FromContext(c).VerifyWebhook(ctx, dupRequest, repo) + if err != nil { + retErr := fmt.Errorf("unable to verify webhook: %w", err) + util.HandleError(c, http.StatusUnauthorized, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } + l.Debugf("hook generated from SCM: %v", h) l.Debugf("repo generated from SCM: %v", r) diff --git a/constants/action.go b/constants/action.go index 67e4e8354..c77b7949f 100644 --- a/constants/action.go +++ b/constants/action.go @@ -28,6 +28,12 @@ const ( // ActionUnlabeled defines the action for the unlabeling of pull requests. ActionUnlabeled = "unlabeled" + // ActionMerged defines the action for the merging of a pull request. + ActionMerged = "merged" + + // ActionClosed defines the action for closing pull requests (unmerged). + ActionClosed = "closed" + // ActionTransferred defines the action for transferring repository ownership. ActionTransferred = "transferred" diff --git a/constants/allow_events.go b/constants/allow_events.go index 5df539264..7e7ff419c 100644 --- a/constants/allow_events.go +++ b/constants/allow_events.go @@ -16,7 +16,7 @@ const ( _ // AllowPullReady - Not Implemented AllowPullReopen _ // AllowPullReviewRequest - Not Implemented - _ // AllowPullClosed - Not Implemented + AllowPullMerged AllowDeployCreate AllowCommentCreate AllowCommentEdit @@ -24,4 +24,5 @@ const ( AllowPushDeleteBranch AllowPushDeleteTag AllowPullUnlabel + AllowPullClosedUnmerged ) diff --git a/database/testutils/api_resources.go b/database/testutils/api_resources.go index 3ab6b2e63..9329aead9 100644 --- a/database/testutils/api_resources.go +++ b/database/testutils/api_resources.go @@ -90,6 +90,8 @@ func APIEvents() *api.Events { Reopened: new(bool), Labeled: new(bool), Unlabeled: new(bool), + Merged: new(bool), + Closed: new(bool), }, Deployment: &actions.Deploy{ Created: new(bool), diff --git a/scm/github/testdata/hooks/pull_request_closed_action.json b/scm/github/testdata/hooks/pull_request_closed_action.json index 5aecb6806..1f2948d63 100644 --- a/scm/github/testdata/hooks/pull_request_closed_action.json +++ b/scm/github/testdata/hooks/pull_request_closed_action.json @@ -37,7 +37,7 @@ "created_at": "2018-05-30T20:18:30Z", "updated_at": "2018-05-30T20:18:50Z", "closed_at": "2018-05-30T20:18:50Z", - "merged_at": null, + "merged_at": "2018-05-30T20:18:50Z", "merge_commit_sha": "414cb0069601a32b00bd122a2380cd283626a8e5", "assignee": null, "assignees": [ @@ -321,7 +321,7 @@ } }, "author_association": "OWNER", - "merged": false, + "merged": true, "mergeable": true, "rebaseable": true, "mergeable_state": "clean", diff --git a/scm/github/webhook.go b/scm/github/webhook.go index c5bf0dc8a..1644cf3fb 100644 --- a/scm/github/webhook.go +++ b/scm/github/webhook.go @@ -251,18 +251,19 @@ func (c *client) processPREvent(h *api.Hook, payload *github.PullRequestEvent) ( fmt.Sprintf("https://%s/%s/settings/hooks", h.GetHost(), payload.GetRepo().GetFullName()), ) - // if the pull request state isn't open we ignore it - if payload.GetPullRequest().GetState() != "open" { + // if the pull request state isn't open or actively being closed we ignore it + if payload.GetPullRequest().GetState() != "open" && payload.GetAction() != "closed" { return &internal.Webhook{Hook: h}, nil } - // skip if the pull request action is not opened, synchronize, reopened, edited, labeled, or unlabeled - if !strings.EqualFold(payload.GetAction(), "opened") && - !strings.EqualFold(payload.GetAction(), "synchronize") && - !strings.EqualFold(payload.GetAction(), "reopened") && - !strings.EqualFold(payload.GetAction(), "edited") && - !strings.EqualFold(payload.GetAction(), "labeled") && - !strings.EqualFold(payload.GetAction(), "unlabeled") { + // skip if the pull request action is not configured + if payload.GetAction() != "opened" && + payload.GetAction() != "synchronize" && + payload.GetAction() != "reopened" && + payload.GetAction() != "edited" && + payload.GetAction() != "labeled" && + payload.GetAction() != "unlabeled" && + payload.GetAction() != "closed" { return &internal.Webhook{Hook: h}, nil } @@ -301,9 +302,20 @@ func (c *client) processPREvent(h *api.Hook, payload *github.PullRequestEvent) ( b.SetBaseRef(payload.GetPullRequest().GetBase().GetRef()) b.SetHeadRef(payload.GetPullRequest().GetHead().GetRef()) - // ensure the build reference is set + // PR merge action grabs merge commit as ref and commit if payload.GetPullRequest().GetMerged() { - b.SetRef(fmt.Sprintf("refs/pull/%d/merge", payload.GetNumber())) + b.SetMessage(fmt.Sprintf("Merged PR #%d", payload.GetNumber())) + b.SetRef(payload.GetPullRequest().GetMergeCommitSHA()) + b.SetCommit(payload.GetPullRequest().GetMergeCommitSHA()) + b.SetEventAction(constants.ActionMerged) + } + + // PR close action grabs base ref and commit + if payload.GetAction() == constants.ActionClosed && !payload.GetPullRequest().GetMerged() { + b.SetMessage(fmt.Sprintf("Closed PR #%d", payload.GetNumber())) + b.SetRef(payload.GetPullRequest().GetBase().GetRef()) + b.SetCommit(payload.GetPullRequest().GetBase().GetSHA()) + b.SetEventAction(constants.ActionClosed) } // ensure the build author is set @@ -334,7 +346,6 @@ func (c *client) processPREvent(h *api.Hook, payload *github.PullRequestEvent) ( } // determine if pull request head is a fork and does not match the repo name of base - b.SetFork(payload.GetPullRequest().GetHead().GetRepo().GetFork() && !strings.EqualFold(payload.GetPullRequest().GetBase().GetRepo().GetFullName(), payload.GetPullRequest().GetHead().GetRepo().GetFullName())) diff --git a/scm/github/webhook_test.go b/scm/github/webhook_test.go index e8fa9157d..69900e4a0 100644 --- a/scm/github/webhook_test.go +++ b/scm/github/webhook_test.go @@ -19,6 +19,7 @@ import ( "github.com/go-vela/server/compiler/types/raw" "github.com/go-vela/server/constants" "github.com/go-vela/server/internal" + "github.com/go-vela/server/util" ) func TestGithub_ProcessWebhook_Push(t *testing.T) { @@ -378,6 +379,12 @@ func TestGithub_ProcessWebhook_PullRequest(t *testing.T) { wantBuild.SetBaseRef("main") wantBuild.SetHeadRef("changes") + wantBuildMerge := *wantBuild + wantBuildMerge.EventAction = util.Ptr("merged") + wantBuildMerge.Commit = util.Ptr("414cb0069601a32b00bd122a2380cd283626a8e5") + wantBuildMerge.Ref = util.Ptr("414cb0069601a32b00bd122a2380cd283626a8e5") + wantBuildMerge.Message = util.Ptr("Merged PR #1") + wantBuildFork := *wantBuild tBool := true wantBuildFork.Fork = &tBool @@ -482,9 +489,12 @@ func TestGithub_ProcessWebhook_PullRequest(t *testing.T) { name: "closed action", testData: "testdata/hooks/pull_request_closed_action.json", want: &internal.Webhook{ + PullRequest: internal.PullRequest{ + Number: wantHook.GetNumber(), + }, Hook: wantHook, - Repo: nil, - Build: nil, + Repo: wantRepo, + Build: &wantBuildMerge, }, }, { diff --git a/util/util.go b/util/util.go index f56b7670f..af1caf99e 100644 --- a/util/util.go +++ b/util/util.go @@ -145,3 +145,9 @@ func Sanitize(field string) string { // return the unmodified field return field } + +// Ptr is a helper routine that allocates a new T value +// to store v and returns a pointer to it. +func Ptr[T any](v T) *T { + return &v +}