diff --git a/runatlantis.io/docs/autoplanning.md b/runatlantis.io/docs/autoplanning.md index 9ababb10fd..9bf48ec1b8 100644 --- a/runatlantis.io/docs/autoplanning.md +++ b/runatlantis.io/docs/autoplanning.md @@ -37,3 +37,12 @@ or disable it all together you need to create an `atlantis.yaml` file. See * [Disabling Autoplanning](repo-level-atlantis-yaml.html#disabling-autoplanning) * [Configuring Planning](repo-level-atlantis-yaml.html#configuring-planning) + +::: tip +If a title of pull request contains the following keywords, Atlantis will skip autoplanning. (This feature is currently implemented only for GitHub) + +* [skip atlantis] +* [skip ci] +* [atlantis skip] +* [ci skip] +::: diff --git a/server/events/command_runner.go b/server/events/command_runner.go index cb15e75eb7..b627e7c3b6 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -151,6 +151,12 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo return } + // Skip autoplan if a pull request matches skip keywords. + // We can always force to invoke plan with explicit comment command. + if pull.SkipByKeyword() { + return + } + err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx) if err != nil { diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 0184d226e2..8ad486e76f 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -777,3 +777,12 @@ func TestRunAutoplanCommand_DrainNotOngoing(t *testing.T) { projectCommandBuilder.VerifyWasCalledOnce().BuildAutoplanCommands(matchers.AnyPtrToEventsCommandContext()) Equals(t, 0, drainer.GetStatus().InProgressOps) } + +func TestRunAutoplanCommand_SkipKeywordMatch(t *testing.T) { + t.Log("if a title of the pull request contains skip keywords then skip autoplan") + setup(t) + fixtures.Pull.BaseRepo = fixtures.GithubRepo + fixtures.Pull.Title = "[skip atlantis] foo" + ch.RunAutoplanCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) + projectCommandBuilder.VerifyWasCalled(Never()).BuildAutoplanCommands(matchers.AnyPtrToEventsCommandContext()) +} diff --git a/server/events/event_parser.go b/server/events/event_parser.go index d427a4202a..74f76203c4 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -517,6 +517,11 @@ func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (pullModel model pullState = models.OpenPullState } + title := "" + if pull.Title != nil { + title = *pull.Title + } + pullModel = models.PullRequest{ Author: authorUsername, HeadBranch: headBranch, @@ -526,6 +531,7 @@ func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (pullModel model State: pullState, BaseRepo: baseRepo, BaseBranch: baseBranch, + Title: title, } return } diff --git a/server/events/models/models.go b/server/events/models/models.go index fb66ac1230..c98da56066 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -169,6 +169,21 @@ type PullRequest struct { State PullRequestState // BaseRepo is the repository that the pull request will be merged into. BaseRepo Repo + // Title is a title of the pull request. + // Note: This attribute is currently implemented only for GitHub. + Title string +} + +// A regex for skip ci +// - [skip atlantis] +// - [skip ci] +// - [atlantis skip] +// - [ci skip] +var skipPullRequestKeywordRegex = regexp.MustCompile(`(\[skip (atlantis|ci)\])|(\[(atlantis|ci) skip\])`) + +// SkipByKeyword returns true if a title of the pull request motches skip keywords. +func (p *PullRequest) SkipByKeyword() bool { + return skipPullRequestKeywordRegex.MatchString(p.Title) } // PullRequestOptions is used to set optional paralmeters for PullRequest diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 7692205bdb..d06a87c752 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -160,6 +160,51 @@ func TestNewRepo_HTTPSAuth(t *testing.T) { }, repo) } +func TestPullRequest_SkipByKeyword(t *testing.T) { + cases := []struct { + title string + exp bool + }{ + { + "[skip atlantis] foo", + true, + }, + { + "[skip ci] foo", + true, + }, + { + "[atlantis skip] foo", + true, + }, + { + "[ci skip] foo", + true, + }, + { + "foo [ci skip]", + true, + }, + { + "foo", + false, + }, + { + "", + false, + }, + } + + for _, c := range cases { + t.Run(c.title, func(t *testing.T) { + pull := &models.PullRequest{ + Title: c.title, + } + Equals(t, c.exp, pull.SkipByKeyword()) + }) + } +} + func TestProject_String(t *testing.T) { Equals(t, "repofullname=owner/repo path=my/path", (models.Project{ RepoFullName: "owner/repo",