Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auto_cancel): support canceling pull_request:opened and abstract determination logic #1012

Merged
merged 4 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions api/build/auto_cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,8 @@ func AutoCancel(c *gin.Context, b *library.Build, rB *library.Build, r *library.
return false, nil
}

// ensure criteria is met before auto canceling (push to same branch, or pull with same action from same head_ref)
if (strings.EqualFold(rB.GetEvent(), constants.EventPush) &&
strings.EqualFold(b.GetEvent(), constants.EventPush) &&
strings.EqualFold(b.GetBranch(), rB.GetBranch())) ||
(strings.EqualFold(rB.GetEvent(), constants.EventPull) &&
strings.EqualFold(b.GetEventAction(), rB.GetEventAction()) &&
strings.EqualFold(b.GetHeadRef(), rB.GetHeadRef())) {
// ensure criteria is met
if isCancelable(rB, b) {
switch {
case strings.EqualFold(rB.GetStatus(), constants.StatusPending) && cancelOpts.Pending:
// pending build will be handled gracefully by worker once pulled off queue
Expand Down Expand Up @@ -185,3 +180,43 @@ func cancelRunning(c *gin.Context, b *library.Build, r *library.Repo) error {

return nil
}

// isCancelable is a helper function that determines whether a `target` build should be auto-canceled
// given a current build that intends to supersede it.
func isCancelable(target *library.Build, current *library.Build) bool {
switch target.GetEvent() {
case constants.EventPush:
// target is cancelable if current build is also a push event and the branches are the same
return strings.EqualFold(current.GetEvent(), constants.EventPush) && strings.EqualFold(current.GetBranch(), target.GetBranch())
case constants.EventPull:
cancelableAction := strings.EqualFold(target.GetEventAction(), constants.ActionOpened) || strings.EqualFold(target.GetEventAction(), constants.ActionSynchronize)

// target is cancelable if current build is also a pull event, target is an opened / synchronize action, and the current head ref matches target head ref
return strings.EqualFold(current.GetEvent(), constants.EventPull) && cancelableAction && strings.EqualFold(current.GetHeadRef(), target.GetHeadRef())
default:
return false
}
}

// ShouldAutoCancel is a helper function that determines whether or not a build should be eligible to
// auto cancel currently running / pending builds.
func ShouldAutoCancel(opts *pipeline.CancelOptions, b *library.Build, defaultBranch string) bool {
// if anything is provided in the auto_cancel metadata, then we start with true
runAutoCancel := opts.Running || opts.Pending || opts.DefaultBranch

switch b.GetEvent() {
case constants.EventPush:
// pushes to the default branch should only auto cancel if pipeline specifies default_branch: true
if !opts.DefaultBranch && strings.EqualFold(b.GetBranch(), defaultBranch) {
runAutoCancel = false
}

return runAutoCancel

case constants.EventPull:
// only synchronize actions of the pull_request event are eligible to auto cancel
return runAutoCancel && (strings.EqualFold(b.GetEventAction(), constants.ActionSynchronize))
default:
return false
}
}
268 changes: 268 additions & 0 deletions api/build/auto_cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"testing"

"github.com/go-vela/types/constants"
"github.com/go-vela/types/library"
"github.com/go-vela/types/pipeline"
)

func Test_isCancelable(t *testing.T) {
// setup types
pushEvent := constants.EventPush
pullEvent := constants.EventPull
tagEvent := constants.EventTag

branchDev := "dev"
branchPatch := "patch-1"

actionOpened := constants.ActionOpened
actionSync := constants.ActionSynchronize
actionEdited := constants.ActionEdited

tests := []struct {
name string
target *library.Build
current *library.Build
want bool
}{
{
name: "Wrong Event",
target: &library.Build{
Event: &tagEvent,
Branch: &branchDev,
},
current: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
want: false,
},
{
name: "Cancelable Push",
target: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
current: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
want: true,
},
{
name: "Push Branch Mismatch",
target: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
current: &library.Build{
Event: &pushEvent,
Branch: &branchPatch,
},
want: false,
},
{
name: "Event Mismatch",
target: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
current: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchPatch,
},
want: false,
},
{
name: "Cancelable Pull",
target: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchPatch,
EventAction: &actionOpened,
},
current: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchPatch,
EventAction: &actionSync,
},
want: true,
},
{
name: "Pull Head Ref Mismatch",
target: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchPatch,
EventAction: &actionSync,
},
current: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchDev,
EventAction: &actionSync,
},
want: false,
},
{
name: "Pull Ineligible Action",
target: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchPatch,
EventAction: &actionEdited,
},
current: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
HeadRef: &branchDev,
EventAction: &actionSync,
},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isCancelable(tt.target, tt.current); got != tt.want {
t.Errorf("test %s: isCancelable() = %v, want %v", tt.name, got, tt.want)
}
})
}
}

func Test_ShouldAutoCancel(t *testing.T) {
// setup types
pushEvent := constants.EventPush
pullEvent := constants.EventPull
tagEvent := constants.EventTag

branchDev := "dev"
branchPatch := "patch-1"

actionOpened := constants.ActionOpened
actionSync := constants.ActionSynchronize

tests := []struct {
name string
opts *pipeline.CancelOptions
build *library.Build
branch string
want bool
}{
{
name: "Wrong Event",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: true,
},
build: &library.Build{
Event: &tagEvent,
Branch: &branchPatch,
},
branch: branchDev,
want: false,
},
{
name: "Auto Cancel Disabled",
opts: &pipeline.CancelOptions{
Running: false,
Pending: false,
DefaultBranch: false,
},
build: &library.Build{
Event: &pushEvent,
Branch: &branchPatch,
},
branch: branchDev,
want: false,
},
{
name: "Eligible Push",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: false,
},
build: &library.Build{
Event: &pushEvent,
Branch: &branchPatch,
},
branch: branchDev,
want: true,
},
{
name: "Eligible Push - Default Branch",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: true,
},
build: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
branch: branchDev,
want: true,
},
{
name: "Push Mismatch - Default Branch",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: false,
},
build: &library.Build{
Event: &pushEvent,
Branch: &branchDev,
},
branch: branchDev,
want: false,
},
{
name: "Eligible Pull",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: false,
},
build: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
EventAction: &actionSync,
},
branch: branchDev,
want: true,
},
{
name: "Pull Mismatch - Action",
opts: &pipeline.CancelOptions{
Running: true,
Pending: true,
DefaultBranch: false,
},
build: &library.Build{
Event: &pullEvent,
Branch: &branchDev,
EventAction: &actionOpened,
},
branch: branchDev,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ShouldAutoCancel(tt.opts, tt.build, tt.branch); got != tt.want {
t.Errorf("test %s: ShouldAutoCancel() = %v, want %v", tt.name, got, tt.want)
}
})
}
}
15 changes: 1 addition & 14 deletions api/webhook/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@

defer func() {
// send API call to update the webhook
_, err = database.FromContext(c).UpdateHook(ctx, h)

Check failure on line 180 in api/webhook/post.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] api/webhook/post.go#L180

Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
Raw output
api/webhook/post.go:180:32: Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
		_, err = database.FromContext(c).UpdateHook(ctx, h)
		                             ^
if err != nil {
logrus.Errorf("unable to update webhook %s/%d: %v", r.GetFullName(), h.GetNumber(), err)
}
Expand Down Expand Up @@ -682,20 +682,7 @@
u,
)

// if anything is provided in the auto_cancel metadata, then we start with true
runAutoCancel := p.Metadata.AutoCancel.Running || p.Metadata.AutoCancel.Pending || p.Metadata.AutoCancel.DefaultBranch

// if the event is a push to the default branch and the AutoCancel.DefaultBranch value is false, bypass auto cancel
if strings.EqualFold(b.GetEvent(), constants.EventPush) && strings.EqualFold(b.GetBranch(), repo.GetBranch()) && !p.Metadata.AutoCancel.DefaultBranch {
runAutoCancel = false
}

// if event is push or pull_request:synchronize, there is a chance this build could be superceding a stale build
//
// fetch pending and running builds for this repo in order to validate their merit to continue running.
if runAutoCancel &&
((strings.EqualFold(b.GetEvent(), constants.EventPull) && strings.EqualFold(b.GetEventAction(), constants.ActionSynchronize)) ||
strings.EqualFold(b.GetEvent(), constants.EventPush)) {
if build.ShouldAutoCancel(p.Metadata.AutoCancel, b, repo.GetBranch()) {
// fetch pending and running builds
rBs, err := database.FromContext(c).ListPendingAndRunningBuildsForRepo(c, repo)
if err != nil {
Expand Down Expand Up @@ -746,7 +733,7 @@
case "archived", "unarchived", constants.ActionEdited:
logrus.Debugf("repository action %s for %s", h.GetEventAction(), r.GetFullName())
// send call to get repository from database
dbRepo, err := database.FromContext(c).GetRepoForOrg(ctx, r.GetOrg(), r.GetName())

Check failure on line 736 in api/webhook/post.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] api/webhook/post.go#L736

Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
Raw output
api/webhook/post.go:736:38: Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
		dbRepo, 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)

Expand All @@ -757,7 +744,7 @@
}

// send API call to capture the last hook for the repo
lastHook, err := database.FromContext(c).LastHookForRepo(ctx, dbRepo)

Check failure on line 747 in api/webhook/post.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] api/webhook/post.go#L747

Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
Raw output
api/webhook/post.go:747:40: Non-inherited new context, use function like `context.WithXXX` instead (contextcheck)
		lastHook, err := database.FromContext(c).LastHookForRepo(ctx, dbRepo)
		                                     ^
if err != nil {
retErr := fmt.Errorf("unable to get last hook for repo %s: %w", r.GetFullName(), err)

Expand Down
Loading