From f1b2637be75874308b160a5467bd949d0a028273 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Wed, 8 Sep 2021 13:39:35 +0900 Subject: [PATCH] Skip autoplan if a pull request matches skip keywords for GitHub This is an attempt to partially support for #932. If an author of the pull request has a confidence of "no changes for real resources", it would be great if atlantis could skip autoplan. The initial implementation of this feature as follows: If a title of pull request contains the following keywords, skip autoplan. * [skip atlantis] * [skip ci] * [atlantis skip] * [ci skip] We should always force to invoke plan with explicit comment command. This feature is currently implemented only for GitHub just because I'm a user of GitHub, but I expect it's possible to support other VCS providers. Note: Most of general purpose CI/CD platforms support a concept for skip build. For examples: https://circleci.com/docs/2.0/skip-build/ https://docs.github.com/en/actions/guides/about-continuous-integration#skipping-workflow-runs As far as I know, they check all commits included in the pull request, not a title of the pull request because they need to support triggered on push event. On the other hand, the current implementation of atlantis doesn't triggered on push event and doesn't have all commits on open event. To simplify the implementation, I think checking the title is reasonable. Of course it's possible to get all commits included in the pull request dynamically via additional API calls, please let me know if we should check commit messages instead of the title. The original feature request said that the 'keyword' could be configurable, but I don't think most of users including me need such a flexibility. So the initial implementation embeds keywords in source. If someone need to be configurable, feel free to open another feature request. --- runatlantis.io/docs/autoplanning.md | 9 ++++++ server/events/command_runner.go | 6 ++++ server/events/command_runner_test.go | 9 ++++++ server/events/event_parser.go | 6 ++++ server/events/models/models.go | 15 ++++++++++ server/events/models/models_test.go | 45 ++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+) 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",