ability to cancel running builds if new commit is pushed

This commit is contained in:
Eoin McAfee 2021-08-26 17:21:00 +01:00
parent 0b054a6aaa
commit 9b86e80f5f
18 changed files with 245 additions and 83 deletions

View file

@ -21,7 +21,7 @@ type Canceler interface {
// Cancel cancels the provided build. // Cancel cancels the provided build.
Cancel(context.Context, *Repository, *Build) error Cancel(context.Context, *Repository, *Build) error
// CancelPending cancels all pending builds of the same // CancelByStatus cancels all builds by a status, passed to the function
// type of as the provided build. // and reference with lower build numbers.
CancelPending(context.Context, *Repository, *Build) error CancelPending(context.Context, *Repository, *Build) error
} }

View file

@ -32,38 +32,39 @@ const (
type ( type (
// Repository represents a source code repository. // Repository represents a source code repository.
Repository struct { Repository struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UID string `json:"uid"` UID string `json:"uid"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Namespace string `json:"namespace"` Namespace string `json:"namespace"`
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug"` Slug string `json:"slug"`
SCM string `json:"scm"` SCM string `json:"scm"`
HTTPURL string `json:"git_http_url"` HTTPURL string `json:"git_http_url"`
SSHURL string `json:"git_ssh_url"` SSHURL string `json:"git_ssh_url"`
Link string `json:"link"` Link string `json:"link"`
Branch string `json:"default_branch"` Branch string `json:"default_branch"`
Private bool `json:"private"` Private bool `json:"private"`
Visibility string `json:"visibility"` Visibility string `json:"visibility"`
Active bool `json:"active"` Active bool `json:"active"`
Config string `json:"config_path"` Config string `json:"config_path"`
Trusted bool `json:"trusted"` Trusted bool `json:"trusted"`
Protected bool `json:"protected"` Protected bool `json:"protected"`
IgnoreForks bool `json:"ignore_forks"` IgnoreForks bool `json:"ignore_forks"`
IgnorePulls bool `json:"ignore_pull_requests"` IgnorePulls bool `json:"ignore_pull_requests"`
CancelPulls bool `json:"auto_cancel_pull_requests"` CancelPulls bool `json:"auto_cancel_pull_requests"`
CancelPush bool `json:"auto_cancel_pushes"` CancelPush bool `json:"auto_cancel_pushes"`
Timeout int64 `json:"timeout"` CancelRunning bool `json:"auto_cancel_running"`
Throttle int64 `json:"throttle,omitempty"` Timeout int64 `json:"timeout"`
Counter int64 `json:"counter"` Throttle int64 `json:"throttle,omitempty"`
Synced int64 `json:"synced"` Counter int64 `json:"counter"`
Created int64 `json:"created"` Synced int64 `json:"synced"`
Updated int64 `json:"updated"` Created int64 `json:"created"`
Version int64 `json:"version"` Updated int64 `json:"updated"`
Signer string `json:"-"` Version int64 `json:"version"`
Secret string `json:"-"` Signer string `json:"-"`
Build *Build `json:"build,omitempty"` Secret string `json:"-"`
Perms *Perm `json:"permissions,omitempty"` Build *Build `json:"build,omitempty"`
Perms *Perm `json:"permissions,omitempty"`
} }
// RepositoryStore defines operations for working with repositories. // RepositoryStore defines operations for working with repositories.

View file

@ -28,17 +28,18 @@ import (
type ( type (
repositoryInput struct { repositoryInput struct {
Visibility *string `json:"visibility"` Visibility *string `json:"visibility"`
Config *string `json:"config_path"` Config *string `json:"config_path"`
Trusted *bool `json:"trusted"` Trusted *bool `json:"trusted"`
Protected *bool `json:"protected"` Protected *bool `json:"protected"`
IgnoreForks *bool `json:"ignore_forks"` IgnoreForks *bool `json:"ignore_forks"`
IgnorePulls *bool `json:"ignore_pull_requests"` IgnorePulls *bool `json:"ignore_pull_requests"`
CancelPulls *bool `json:"auto_cancel_pull_requests"` CancelPulls *bool `json:"auto_cancel_pull_requests"`
CancelPush *bool `json:"auto_cancel_pushes"` CancelPush *bool `json:"auto_cancel_pushes"`
Timeout *int64 `json:"timeout"` CancelRunning *bool `json:"auto_cancel_running"`
Throttle *int64 `json:"throttle"` Timeout *int64 `json:"timeout"`
Counter *int64 `json:"counter"` Throttle *int64 `json:"throttle"`
Counter *int64 `json:"counter"`
} }
) )
@ -95,6 +96,9 @@ func HandleUpdate(repos core.RepositoryStore) http.HandlerFunc {
if in.CancelPush != nil { if in.CancelPush != nil {
repo.CancelPush = *in.CancelPush repo.CancelPush = *in.CancelPush
} }
if in.CancelRunning != nil {
repo.CancelRunning = *in.CancelRunning
}
// //
// system administrator only // system administrator only

View file

@ -12,9 +12,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/errors" "github.com/drone/drone/handler/api/errors"
"github.com/drone/drone/mock" "github.com/drone/drone/mock"
"github.com/drone/drone/core"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
@ -41,6 +41,7 @@ func TestUpdate(t *testing.T) {
repoInput := &core.Repository{ repoInput := &core.Repository{
Visibility: core.VisibilityPublic, Visibility: core.VisibilityPublic,
CancelRunning: true,
} }
checkUpdate := func(_ context.Context, updated *core.Repository) error { checkUpdate := func(_ context.Context, updated *core.Repository) error {
@ -83,6 +84,7 @@ func TestUpdate(t *testing.T) {
HTTPURL: "https://github.com/octocat/hello-world.git", HTTPURL: "https://github.com/octocat/hello-world.git",
SSHURL: "git@github.com:octocat/hello-world.git", SSHURL: "git@github.com:octocat/hello-world.git",
Link: "https://github.com/octocat/hello-world", Link: "https://github.com/octocat/hello-world",
CancelRunning: true,
} }
json.NewDecoder(w.Body).Decode(got) json.NewDecoder(w.Body).Decode(got)
if diff := cmp.Diff(got, want); len(diff) > 0 { if diff := cmp.Diff(got, want); len(diff) > 0 {

View file

@ -107,6 +107,7 @@ func (s *service) CancelPending(ctx context.Context, repo *core.Repository, buil
// ignore incomplete items in the list that do // ignore incomplete items in the list that do
// not match the repository or build, are already // not match the repository or build, are already
// running, or are newer than the current build. // running, or are newer than the current build.
if !match(build, item) { if !match(build, item) {
continue continue
} }

View file

@ -31,6 +31,23 @@ func TestCancelPending_IgnoreEvent(t *testing.T) {
} }
} }
func TestCancelRunning_IgnoreEvent(t *testing.T) {
ignore := []string{
core.EventCron,
core.EventCustom,
core.EventPromote,
core.EventRollback,
core.EventTag,
}
for _, event := range ignore {
s := new(service)
err := s.CancelPending(noContext, nil, &core.Build{Event: event})
if err != nil {
t.Errorf("Expect cancel skipped for event type %s", event)
}
}
}
func TestCancel(t *testing.T) { func TestCancel(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()

View file

@ -27,11 +27,17 @@ func match(build *core.Build, with *core.Repository) bool {
if with.Build.Number >= build.Number { if with.Build.Number >= build.Number {
return false return false
} }
// filter out builds that are not in a
// pending state. if with.CancelRunning == true {
if with.Build.Status != core.StatusPending { if with.Build.Status != core.StatusRunning && with.Build.Status != core.StatusPending {
return false return false
}
} else {
if with.Build.Status != core.StatusPending {
return false
}
} }
// filter out builds that do not match // filter out builds that do not match
// the same event type. // the same event type.
if with.Build.Event != build.Event { if with.Build.Event != build.Event {

View file

@ -10,7 +10,7 @@ import (
"github.com/drone/drone/core" "github.com/drone/drone/core"
) )
func TestMatch(t *testing.T) { func TestMatchPendingBuild(t *testing.T) {
tests := []struct { tests := []struct {
build *core.Build build *core.Build
repo *core.Repository repo *core.Repository
@ -72,7 +72,7 @@ func TestMatch(t *testing.T) {
Status: core.StatusPending, Status: core.StatusPending,
Event: core.EventPush, Event: core.EventPush,
Ref: "refs/heads/master", Ref: "refs/heads/master",
}}, }, CancelRunning: false},
want: true, want: true,
}, },
{ {
@ -82,7 +82,91 @@ func TestMatch(t *testing.T) {
Status: core.StatusPending, Status: core.StatusPending,
Event: core.EventPullRequest, Event: core.EventPullRequest,
Ref: "refs/heads/master", Ref: "refs/heads/master",
}}, }, CancelRunning: false},
want: true,
},
}
for i, test := range tests {
if got, want := match(test.build, test.repo), test.want; got != want {
t.Errorf("Want match %v at index %d, got %v", want, i, got)
}
}
}
func TestMatchRunningBuilds(t *testing.T) {
tests := []struct {
build *core.Build
repo *core.Repository
want bool
}{
// does not match repository id
{
build: &core.Build{RepoID: 2},
repo: &core.Repository{ID: 1},
want: false,
},
// does not match build number requirement that
// must be older than current build
{
build: &core.Build{RepoID: 1, Number: 2},
repo: &core.Repository{ID: 1, Build: &core.Build{Number: 3}},
want: false,
},
{
build: &core.Build{RepoID: 1, Number: 2},
repo: &core.Repository{ID: 1, Build: &core.Build{Number: 2}},
want: false,
},
// does not match required status
{
build: &core.Build{RepoID: 1, Number: 2},
repo: &core.Repository{ID: 1, Build: &core.Build{Number: 1, Status: core.StatusError}},
want: false,
},
// does not match (one of) required event types
{
build: &core.Build{RepoID: 1, Number: 2, Event: core.EventPullRequest},
repo: &core.Repository{ID: 1, Build: &core.Build{
Number: 1,
Status: core.StatusRunning,
Event: core.EventPush,
}},
want: false,
},
// does not match ref
{
build: &core.Build{RepoID: 1, Number: 2, Event: core.EventPush, Ref: "refs/heads/master"},
repo: &core.Repository{ID: 1, Build: &core.Build{
Number: 1,
Status: core.StatusRunning,
Event: core.EventPush,
Ref: "refs/heads/develop",
}},
want: false,
},
//
// successful matches
//
{
build: &core.Build{RepoID: 1, Number: 2, Event: core.EventPullRequest, Ref: "refs/heads/master"},
repo: &core.Repository{ID: 1, Build: &core.Build{
Number: 1,
Status: core.StatusRunning,
Event: core.EventPullRequest,
Ref: "refs/heads/master",
}, CancelRunning: true},
want: true,
},
{
build: &core.Build{RepoID: 1, Number: 2, Event: core.EventPush, Ref: "refs/heads/master"},
repo: &core.Repository{ID: 1, Build: &core.Build{
Number: 1,
Status: core.StatusRunning,
Event: core.EventPush,
Ref: "refs/heads/master",
}, CancelRunning: true},
want: true, want: true,
}, },
} }

View file

@ -186,6 +186,7 @@ const stmtInsertBase = `
,repo_no_pulls ,repo_no_pulls
,repo_cancel_pulls ,repo_cancel_pulls
,repo_cancel_push ,repo_cancel_push
,repo_cancel_running
,repo_synced ,repo_synced
,repo_created ,repo_created
,repo_updated ,repo_updated
@ -216,6 +217,7 @@ const stmtInsertBase = `
,:repo_no_pulls ,:repo_no_pulls
,:repo_cancel_pulls ,:repo_cancel_pulls
,:repo_cancel_push ,:repo_cancel_push
,:repo_cancel_running
,:repo_synced ,:repo_synced
,:repo_created ,:repo_created
,:repo_updated ,:repo_updated

View file

@ -244,6 +244,7 @@ const stmtInsertBase = `
,repo_no_pulls ,repo_no_pulls
,repo_cancel_pulls ,repo_cancel_pulls
,repo_cancel_push ,repo_cancel_push
,repo_cancel_running
,repo_synced ,repo_synced
,repo_created ,repo_created
,repo_updated ,repo_updated
@ -274,6 +275,7 @@ const stmtInsertBase = `
,:repo_no_pulls ,:repo_no_pulls
,:repo_cancel_pulls ,:repo_cancel_pulls
,:repo_cancel_push ,:repo_cancel_push
,:repo_cancel_running
,:repo_synced ,:repo_synced
,:repo_created ,:repo_created
,:repo_updated ,:repo_updated

View file

@ -290,6 +290,7 @@ SELECT
,repo_no_pulls ,repo_no_pulls
,repo_cancel_pulls ,repo_cancel_pulls
,repo_cancel_push ,repo_cancel_push
,repo_cancel_running
,repo_synced ,repo_synced
,repo_created ,repo_created
,repo_updated ,repo_updated
@ -386,6 +387,7 @@ INSERT INTO repos (
,repo_no_pulls ,repo_no_pulls
,repo_cancel_pulls ,repo_cancel_pulls
,repo_cancel_push ,repo_cancel_push
,repo_cancel_running
,repo_synced ,repo_synced
,repo_created ,repo_created
,repo_updated ,repo_updated
@ -416,6 +418,7 @@ INSERT INTO repos (
,:repo_no_pulls ,:repo_no_pulls
,:repo_cancel_pulls ,:repo_cancel_pulls
,:repo_cancel_push ,:repo_cancel_push
,:repo_cancel_running
,:repo_synced ,:repo_synced
,:repo_created ,:repo_created
,:repo_updated ,:repo_updated
@ -463,6 +466,7 @@ UPDATE repos SET
,repo_no_pulls = :repo_no_pulls ,repo_no_pulls = :repo_no_pulls
,repo_cancel_pulls = :repo_cancel_pulls ,repo_cancel_pulls = :repo_cancel_pulls
,repo_cancel_push = :repo_cancel_push ,repo_cancel_push = :repo_cancel_push
,repo_cancel_running = :repo_cancel_running
,repo_timeout = :repo_timeout ,repo_timeout = :repo_timeout
,repo_throttle = :repo_throttle ,repo_throttle = :repo_throttle
,repo_counter = :repo_counter ,repo_counter = :repo_counter

View file

@ -25,36 +25,37 @@ import (
// of named query parameters. // of named query parameters.
func ToParams(v *core.Repository) map[string]interface{} { func ToParams(v *core.Repository) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"repo_id": v.ID, "repo_id": v.ID,
"repo_uid": v.UID, "repo_uid": v.UID,
"repo_user_id": v.UserID, "repo_user_id": v.UserID,
"repo_namespace": v.Namespace, "repo_namespace": v.Namespace,
"repo_name": v.Name, "repo_name": v.Name,
"repo_slug": v.Slug, "repo_slug": v.Slug,
"repo_scm": v.SCM, "repo_scm": v.SCM,
"repo_clone_url": v.HTTPURL, "repo_clone_url": v.HTTPURL,
"repo_ssh_url": v.SSHURL, "repo_ssh_url": v.SSHURL,
"repo_html_url": v.Link, "repo_html_url": v.Link,
"repo_branch": v.Branch, "repo_branch": v.Branch,
"repo_private": v.Private, "repo_private": v.Private,
"repo_visibility": v.Visibility, "repo_visibility": v.Visibility,
"repo_active": v.Active, "repo_active": v.Active,
"repo_config": v.Config, "repo_config": v.Config,
"repo_trusted": v.Trusted, "repo_trusted": v.Trusted,
"repo_protected": v.Protected, "repo_protected": v.Protected,
"repo_no_forks": v.IgnoreForks, "repo_no_forks": v.IgnoreForks,
"repo_no_pulls": v.IgnorePulls, "repo_no_pulls": v.IgnorePulls,
"repo_cancel_pulls": v.CancelPulls, "repo_cancel_pulls": v.CancelPulls,
"repo_cancel_push": v.CancelPush, "repo_cancel_push": v.CancelPush,
"repo_timeout": v.Timeout, "repo_cancel_running": v.CancelRunning,
"repo_throttle": v.Throttle, "repo_timeout": v.Timeout,
"repo_counter": v.Counter, "repo_throttle": v.Throttle,
"repo_synced": v.Synced, "repo_counter": v.Counter,
"repo_created": v.Created, "repo_synced": v.Synced,
"repo_updated": v.Updated, "repo_created": v.Created,
"repo_version": v.Version, "repo_updated": v.Updated,
"repo_signer": v.Signer, "repo_version": v.Version,
"repo_secret": v.Secret, "repo_signer": v.Signer,
"repo_secret": v.Secret,
} }
} }
@ -86,6 +87,7 @@ func scanRow(scanner db.Scanner, dest *core.Repository) error {
&dest.IgnorePulls, &dest.IgnorePulls,
&dest.CancelPulls, &dest.CancelPulls,
&dest.CancelPush, &dest.CancelPush,
&dest.CancelRunning,
&dest.Synced, &dest.Synced,
&dest.Created, &dest.Created,
&dest.Updated, &dest.Updated,
@ -141,6 +143,7 @@ func scanRowBuild(scanner db.Scanner, dest *core.Repository) error {
&dest.IgnorePulls, &dest.IgnorePulls,
&dest.CancelPulls, &dest.CancelPulls,
&dest.CancelPush, &dest.CancelPush,
&dest.CancelRunning,
&dest.Synced, &dest.Synced,
&dest.Created, &dest.Created,
&dest.Updated, &dest.Updated,

View file

@ -36,6 +36,10 @@ var migrations = []struct {
name: "alter-table-repos-add-column-throttle", name: "alter-table-repos-add-column-throttle",
stmt: alterTableReposAddColumnThrottle, stmt: alterTableReposAddColumnThrottle,
}, },
{
name: "alter-table-repos-add-column-cancel-running",
stmt: alterTableReposAddColumnCancelRunning,
},
{ {
name: "create-table-perms", name: "create-table-perms",
stmt: createTablePerms, stmt: createTablePerms,
@ -334,6 +338,10 @@ var alterTableReposAddColumnThrottle = `
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
` `
var alterTableReposAddColumnCancelRunning = `
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT false;
`
// //
// 003_create_table_perms.sql // 003_create_table_perms.sql
// //

View file

@ -49,3 +49,7 @@ ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-throttle -- name: alter-table-repos-add-column-throttle
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-cancel-running
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT false;

View file

@ -36,6 +36,10 @@ var migrations = []struct {
name: "alter-table-repos-add-column-throttle", name: "alter-table-repos-add-column-throttle",
stmt: alterTableReposAddColumnThrottle, stmt: alterTableReposAddColumnThrottle,
}, },
{
name: "alter-table-repos-add-column-cancel-running",
stmt: alterTableReposAddColumnCancelRunning,
},
{ {
name: "create-table-perms", name: "create-table-perms",
stmt: createTablePerms, stmt: createTablePerms,
@ -326,6 +330,10 @@ var alterTableReposAddColumnThrottle = `
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
` `
var alterTableReposAddColumnCancelRunning = `
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT false;
`
// //
// 003_create_table_perms.sql // 003_create_table_perms.sql
// //

View file

@ -49,3 +49,7 @@ ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-throttle -- name: alter-table-repos-add-column-throttle
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-cancel-running
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT false;

View file

@ -36,6 +36,10 @@ var migrations = []struct {
name: "alter-table-repos-add-column-throttle", name: "alter-table-repos-add-column-throttle",
stmt: alterTableReposAddColumnThrottle, stmt: alterTableReposAddColumnThrottle,
}, },
{
name: "alter-table-repos-add-column-cancel-running",
stmt: alterTableReposAddColumnCancelRunning,
},
{ {
name: "create-table-perms", name: "create-table-perms",
stmt: createTablePerms, stmt: createTablePerms,
@ -326,6 +330,10 @@ var alterTableReposAddColumnThrottle = `
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
` `
var alterTableReposAddColumnCancelRunning = `
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT 0;
`
// //
// 003_create_table_perms.sql // 003_create_table_perms.sql
// //

View file

@ -49,3 +49,7 @@ ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-throttle -- name: alter-table-repos-add-column-throttle
ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0; ALTER TABLE repos ADD COLUMN repo_throttle INTEGER NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-cancel-running
ALTER TABLE repos ADD COLUMN repo_cancel_running BOOLEAN NOT NULL DEFAULT 0;