service to batch cancel pending builds

This commit is contained in:
Brad Rydzewski 2019-10-01 22:51:23 -07:00
parent c8d74457ab
commit 3fcbf1c78f
21 changed files with 664 additions and 29 deletions

27
core/cancel.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
import "context"
// Canceler cancels a build.
type Canceler interface {
// Cancel cancels the provided build.
Cancel(context.Context, *Repository, *Build) error
// CancelPending cancels all pending builds of the same
// type of as the provided build.
CancelPending(context.Context, *Repository, *Build) error
}

View file

@ -17,6 +17,7 @@ package core
// Hook event constants.
const (
EventCron = "cron"
EventCustom = "custom"
EventPush = "push"
EventPullRequest = "pull_request"
EventTag = "tag"

View file

@ -51,6 +51,8 @@ type (
Protected bool `json:"protected"`
IgnoreForks bool `json:"ignore_forks"`
IgnorePulls bool `json:"ignore_pull_requests"`
CancelPulls bool `json:"auto_cancel_pull_requests"`
CancelPush bool `json:"auto_cancel_branch"`
Timeout int64 `json:"timeout"`
Counter int64 `json:"counter"`
Synced int64 `json:"synced"`

View file

@ -69,7 +69,7 @@ func HandleCreate(
hook := &core.Hook{
Trigger: user.Login,
Event: core.EventPush,
Event: core.EventCustom,
Link: commit.Link,
Timestamp: commit.Author.Date,
Title: "", // we expect this to be empty.

View file

@ -41,7 +41,7 @@ func TestCreate(t *testing.T) {
if got, want := hook.Trigger, mockUser.Login; got != want {
t.Errorf("Want hook Trigger By %s, got %s", want, got)
}
if got, want := hook.Event, core.EventPush; got != want {
if got, want := hook.Event, core.EventCustom; got != want {
t.Errorf("Want hook Event %s, got %s", want, got)
}
if got, want := hook.Link, mockCommit.Link; got != want {

View file

@ -0,0 +1,232 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package canceler
import (
"context"
"time"
"github.com/drone/drone/core"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
)
type service struct {
builds core.BuildStore
repos core.RepositoryStore
scheduler core.Scheduler
stages core.StageStore
status core.StatusService
steps core.StepStore
users core.UserStore
webhooks core.WebhookSender
}
// New returns a new cancellation service that encapsulates
// all cancellation operations.
func New(
builds core.BuildStore,
repos core.RepositoryStore,
scheduler core.Scheduler,
stages core.StageStore,
status core.StatusService,
steps core.StepStore,
users core.UserStore,
webhooks core.WebhookSender,
) core.Canceler {
return &service{
builds: builds,
repos: repos,
scheduler: scheduler,
stages: stages,
status: status,
steps: steps,
users: users,
webhooks: webhooks,
}
}
// Cancel cancels a build.
func (s *service) Cancel(ctx context.Context, repo *core.Repository, build *core.Build) error {
return s.cancel(ctx, repo, build, core.StatusKilled)
}
// CancelPending cancels all pending builds of the same event
// and reference with lower build numbers.
func (s *service) CancelPending(ctx context.Context, repo *core.Repository, build *core.Build) error {
switch build.Event {
// on the push and pull request builds can be automatically
// cancelled by the system.
case core.EventPush, core.EventPullRequest:
default:
return nil
}
// get a list of all incomplete builds from the database
// for all repositories. this will need to be filtered.
incomplete, err := s.repos.ListIncomplete(ctx)
if err != nil {
return err
}
var result error
for _, item := range incomplete {
// ignore incomplete items in the list that do
// not match the repository or build, are already
// running, or are newer than the current build.
if !match(build, item) {
continue
}
err := s.cancel(ctx, repo, build, core.StatusSkipped)
if err != nil {
result = multierror.Append(result, err)
}
}
return result
}
func (s *service) cancel(ctx context.Context, repo *core.Repository, build *core.Build, status string) error {
logger := logrus.WithFields(
logrus.Fields{
"repo": repo.Slug,
"ref": build.Ref,
"build": build.Number,
"event": build.Event,
"status": build.Status,
},
)
// do not cancel the build if the build status is
// complete. only cancel the build if the status is
// running or pending.
switch build.Status {
case core.StatusPending, core.StatusRunning:
default:
return nil
}
// update the build status to killed. if the update fails
// due to an optimistic lock error it means the build has
// already started, and should now be ignored.
build.Status = status
build.Finished = time.Now().Unix()
if build.Started == 0 {
build.Started = time.Now().Unix()
}
err := s.builds.Update(ctx, build)
if err != nil {
logger.WithError(err).
Warnln("api: cannot update build status to cancelled")
return err
}
// notify the scheduler to cancel the build. this will
// instruct runners subscribing to the scheduler to
// cancel execution.
err = s.scheduler.Cancel(ctx, build.ID)
if err != nil {
logger.WithError(err).
Warnln("api: cannot signal cancelled build is complete")
}
// update the commit status in the remote source
// control management system.
user, err := s.users.Find(ctx, repo.UserID)
if err == nil {
err := s.status.Send(ctx, user, &core.StatusInput{
Repo: repo,
Build: build,
})
if err != nil {
logger.WithError(err).
Debugln("api: cannot set status")
}
}
stages, err := s.stages.ListSteps(ctx, build.ID)
if err != nil {
logger.WithError(err).
Debugln("api: cannot list build stages")
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, stage := range stages {
if stage.IsDone() {
continue
}
if stage.Started != 0 {
stage.Status = core.StatusKilled
} else {
stage.Status = core.StatusSkipped
stage.Started = time.Now().Unix()
}
stage.Stopped = time.Now().Unix()
err := s.stages.Update(ctx, stage)
if err != nil {
logger.WithError(err).
WithField("stage", stage.Number).
Debugln("api: cannot update stage status")
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, step := range stage.Steps {
if step.IsDone() {
continue
}
if step.Started != 0 {
step.Status = core.StatusKilled
} else {
step.Status = core.StatusSkipped
step.Started = time.Now().Unix()
}
step.Stopped = time.Now().Unix()
step.ExitCode = 130
err := s.steps.Update(ctx, step)
if err != nil {
logger.WithError(err).
WithField("stage", stage.Number).
WithField("step", step.Number).
Debugln("api: cannot update step status")
}
}
}
logger.WithError(err).
Debugln("api: successfully cancelled build")
build.Stages = stages
// trigger a webhook to notify subscribing systems that
// the build was cancelled.
payload := &core.WebhookData{
Event: core.WebhookEventBuild,
Action: core.WebhookActionUpdated,
Repo: repo,
Build: build,
}
err = s.webhooks.Send(ctx, payload)
if err != nil {
logger.WithError(err).
Warnln("manager: cannot send global webhook")
}
return nil
}

View file

@ -0,0 +1,126 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package canceler
import (
"context"
"testing"
"github.com/drone/drone/core"
"github.com/drone/drone/mock"
"github.com/go-chi/chi"
"github.com/golang/mock/gomock"
)
var noContext = context.Background()
func TestCancelPending_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) {
controller := gomock.NewController(t)
defer controller.Finish()
mockStages := []*core.Stage{
{Status: core.StatusPassing},
{
Status: core.StatusPending,
Steps: []*core.Step{
{Status: core.StatusPassing},
{Status: core.StatusPending},
},
},
}
mockBuildCopy := new(core.Build)
*mockBuildCopy = *mockBuild
repos := mock.NewMockRepositoryStore(controller)
builds := mock.NewMockBuildStore(controller)
builds.EXPECT().Update(gomock.Any(), mockBuildCopy).Return(nil)
users := mock.NewMockUserStore(controller)
users.EXPECT().Find(gomock.Any(), mockRepo.UserID).Return(mockUser, nil)
stages := mock.NewMockStageStore(controller)
stages.EXPECT().ListSteps(gomock.Any(), mockBuild.ID).Return(mockStages, nil)
stages.EXPECT().Update(gomock.Any(), mockStages[1]).Return(nil)
steps := mock.NewMockStepStore(controller)
steps.EXPECT().Update(gomock.Any(), mockStages[1].Steps[1]).Return(nil)
status := mock.NewMockStatusService(controller)
status.EXPECT().Send(gomock.Any(), mockUser, gomock.Any()).Return(nil)
webhook := mock.NewMockWebhookSender(controller)
webhook.EXPECT().Send(gomock.Any(), gomock.Any()).Return(nil)
scheduler := mock.NewMockScheduler(controller)
scheduler.EXPECT().Cancel(gomock.Any(), mockBuild.ID).Return(nil)
c := new(chi.Context)
c.URLParams.Add("owner", "octocat")
c.URLParams.Add("name", "hello-world")
c.URLParams.Add("number", "1")
s := New(builds, repos, scheduler, stages, status, steps, users, webhook)
err := s.Cancel(noContext, mockRepo, mockBuildCopy)
if err != nil {
t.Error(err)
}
}
var (
mockRepo = &core.Repository{
ID: 1,
Namespace: "octocat",
Name: "hello-world",
Slug: "octocat/hello-world",
Counter: 42,
Branch: "master",
}
mockBuild = &core.Build{
ID: 1,
Number: 1,
RepoID: 1,
Status: core.StatusPending,
Event: core.EventPush,
Link: "https://github.com/octocat/Hello-World/commit/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
Timestamp: 1299283200,
Message: "first commit",
Before: "553c2077f0edc3d5dc5d17262f6aa498e69d6f8e",
After: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
Ref: "refs/heads/master",
Source: "master",
Target: "master",
Author: "octocat",
AuthorName: "The Octocat",
AuthorEmail: "octocat@hello-world.com",
AuthorAvatar: "https://avatars3.githubusercontent.com/u/583231",
Sender: "octocat",
}
mockUser = &core.User{
ID: 1,
Login: "octocat",
}
)

46
service/canceler/match.go Normal file
View file

@ -0,0 +1,46 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package canceler
import "github.com/drone/drone/core"
func match(build *core.Build, with *core.Repository) bool {
// filter out existing builds for others
// repositories.
if with.ID != build.RepoID {
return false
}
// filter out builds that are newer than
// the current build.
if with.Build.Number >= build.Number {
return false
}
// filter out builds that are not in a
// pending state.
if with.Build.Status != core.StatusPending {
return false
}
// filter out builds that do not match
// the same event type.
if with.Build.Event != build.Event {
return false
}
// filter out builds that do not match
// the same reference.
if with.Build.Ref != build.Ref {
return false
}
return true
}

View file

@ -0,0 +1,95 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package canceler
import (
"testing"
"github.com/drone/drone/core"
)
func TestMatch(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.StatusPassing}},
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.StatusPending,
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.StatusPending,
Event: core.EventPush,
Ref: "refs/heads/develop",
}},
want: false,
},
//
// successful matches
//
{
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.StatusPending,
Event: core.EventPush,
Ref: "refs/heads/master",
}},
want: true,
},
{
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.StatusPending,
Event: core.EventPullRequest,
Ref: "refs/heads/master",
}},
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)
}
}
}

View file

@ -183,6 +183,8 @@ const stmtInsertBase = `
,repo_protected
,repo_no_forks
,repo_no_pulls
,repo_cancel_pulls
,repo_cancel_push
,repo_synced
,repo_created
,repo_updated
@ -210,6 +212,8 @@ const stmtInsertBase = `
,:repo_protected
,:repo_no_forks
,:repo_no_pulls
,:repo_cancel_pulls
,:repo_cancel_push
,:repo_synced
,:repo_created
,:repo_updated

View file

@ -241,6 +241,8 @@ const stmtInsertBase = `
,repo_protected
,repo_no_forks
,repo_no_pulls
,repo_cancel_pulls
,repo_cancel_push
,repo_synced
,repo_created
,repo_updated
@ -268,6 +270,8 @@ const stmtInsertBase = `
,:repo_protected
,:repo_no_forks
,:repo_no_pulls
,:repo_cancel_pulls
,:repo_cancel_push
,:repo_synced
,:repo_created
,:repo_updated

View file

@ -266,6 +266,8 @@ SELECT
,repo_protected
,repo_no_forks
,repo_no_pulls
,repo_cancel_pulls
,repo_cancel_push
,repo_synced
,repo_created
,repo_updated
@ -353,6 +355,8 @@ INSERT INTO repos (
,repo_protected
,repo_no_forks
,repo_no_pulls
,repo_cancel_pulls
,repo_cancel_push
,repo_synced
,repo_created
,repo_updated
@ -380,6 +384,8 @@ INSERT INTO repos (
,:repo_protected
,:repo_no_forks
,:repo_no_pulls
,:repo_cancel_pulls
,:repo_cancel_push
,:repo_synced
,:repo_created
,:repo_updated
@ -425,6 +431,8 @@ UPDATE repos SET
,repo_protected = :repo_protected
,repo_no_forks = :repo_no_forks
,repo_no_pulls = :repo_no_pulls
,repo_cancel_pulls = :repo_cancel_pulls
,repo_cancel_push = :repo_cancel_push
,repo_timeout = :repo_timeout
,repo_counter = :repo_counter
,repo_synced = :repo_synced

View file

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

View file

@ -24,6 +24,14 @@ var migrations = []struct {
name: "alter-table-repos-add-column-no-pulls",
stmt: alterTableReposAddColumnNoPulls,
},
{
name: "alter-table-repos-add-column-cancel-pulls",
stmt: alterTableReposAddColumnCancelPulls,
},
{
name: "alter-table-repos-add-column-cancel-push",
stmt: alterTableReposAddColumnCancelPush,
},
{
name: "create-table-perms",
stmt: createTablePerms,
@ -274,6 +282,14 @@ var alterTableReposAddColumnNoPulls = `
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT false;
`
var alterTableReposAddColumnCancelPulls = `
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT false;
`
var alterTableReposAddColumnCancelPush = `
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;
`
//
// 003_create_table_perms.sql
//

View file

@ -37,3 +37,11 @@ ALTER TABLE repos ADD COLUMN repo_no_forks BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-no-pulls
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-cancel-pulls
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-cancel-push
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;

View file

@ -24,6 +24,14 @@ var migrations = []struct {
name: "alter-table-repos-add-column-no-pulls",
stmt: alterTableReposAddColumnNoPulls,
},
{
name: "alter-table-repos-add-column-cancel-pulls",
stmt: alterTableReposAddColumnCancelPulls,
},
{
name: "alter-table-repos-add-column-cancel-push",
stmt: alterTableReposAddColumnCancelPush,
},
{
name: "create-table-perms",
stmt: createTablePerms,
@ -270,6 +278,14 @@ var alterTableReposAddColumnNoPulls = `
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT false;
`
var alterTableReposAddColumnCancelPulls = `
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT false;
`
var alterTableReposAddColumnCancelPush = `
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;
`
//
// 003_create_table_perms.sql
//

View file

@ -37,3 +37,11 @@ ALTER TABLE repos ADD COLUMN repo_no_forks BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-no-pulls
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-cancel-pulls
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT false;
-- name: alter-table-repos-add-column-cancel-push
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT false;

View file

@ -24,6 +24,14 @@ var migrations = []struct {
name: "alter-table-repos-add-column-no-pulls",
stmt: alterTableReposAddColumnNoPulls,
},
{
name: "alter-table-repos-add-column-cancel-pulls",
stmt: alterTableReposAddColumnCancelPulls,
},
{
name: "alter-table-repos-add-column-cancel-push",
stmt: alterTableReposAddColumnCancelPush,
},
{
name: "create-table-perms",
stmt: createTablePerms,
@ -270,6 +278,14 @@ var alterTableReposAddColumnNoPulls = `
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT 0;
`
var alterTableReposAddColumnCancelPulls = `
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT 0;
`
var alterTableReposAddColumnCancelPush = `
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT 0;
`
//
// 003_create_table_perms.sql
//

View file

@ -37,3 +37,11 @@ ALTER TABLE repos ADD COLUMN repo_no_forks BOOLEAN NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-no-pulls
ALTER TABLE repos ADD COLUMN repo_no_pulls BOOLEAN NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-cancel-pulls
ALTER TABLE repos ADD COLUMN repo_cancel_pulls BOOLEAN NOT NULL DEFAULT 0;
-- name: alter-table-repos-add-column-cancel-push
ALTER TABLE repos ADD COLUMN repo_cancel_push BOOLEAN NOT NULL DEFAULT 0;

View file

@ -59,6 +59,8 @@ func skipMessage(hook *core.Hook) bool {
return false
case hook.Event == core.EventCron:
return false
case hook.Event == core.EventCustom:
return false
case skipMessageEval(hook.Message):
return true
case skipMessageEval(hook.Title):

View file

@ -190,6 +190,16 @@ func Test_skipMessage(t *testing.T) {
title: "update readme [CI SKIP]",
want: false,
},
{
event: "custom",
title: "update readme [CI SKIP]",
want: false,
},
{
event: "custom",
title: "update readme [CI SKIP]",
want: false,
},
}
for _, test := range tests {
hook := &core.Hook{