diff --git a/cmd/server.go b/cmd/server.go index 0bb589411e..04f553a5b5 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -43,110 +43,111 @@ const ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. const ( // Flag names. - ADWebhookPasswordFlag = "azuredevops-webhook-password" // nolint: gosec - ADWebhookUserFlag = "azuredevops-webhook-user" - ADTokenFlag = "azuredevops-token" // nolint: gosec - ADUserFlag = "azuredevops-user" - ADHostnameFlag = "azuredevops-hostname" - AllowCommandsFlag = "allow-commands" - AllowForkPRsFlag = "allow-fork-prs" - AtlantisURLFlag = "atlantis-url" - AutoDiscoverModeFlag = "autodiscover-mode" - AutomergeFlag = "automerge" - ParallelPlanFlag = "parallel-plan" - ParallelApplyFlag = "parallel-apply" - AutoplanModules = "autoplan-modules" - AutoplanModulesFromProjects = "autoplan-modules-from-projects" - AutoplanFileListFlag = "autoplan-file-list" - BitbucketBaseURLFlag = "bitbucket-base-url" - BitbucketTokenFlag = "bitbucket-token" - BitbucketUserFlag = "bitbucket-user" - BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" - CheckoutDepthFlag = "checkout-depth" - CheckoutStrategyFlag = "checkout-strategy" - ConfigFlag = "config" - DataDirFlag = "data-dir" - DefaultTFVersionFlag = "default-tf-version" - DisableApplyAllFlag = "disable-apply-all" - DisableAutoplanFlag = "disable-autoplan" - DisableAutoplanLabelFlag = "disable-autoplan-label" - DisableMarkdownFoldingFlag = "disable-markdown-folding" - DisableRepoLockingFlag = "disable-repo-locking" - DisableGlobalApplyLockFlag = "disable-global-apply-lock" - DisableUnlockLabelFlag = "disable-unlock-label" - DiscardApprovalOnPlanFlag = "discard-approval-on-plan" - EmojiReaction = "emoji-reaction" - EnableDiffMarkdownFormat = "enable-diff-markdown-format" - EnablePolicyChecksFlag = "enable-policy-checks" - EnableRegExpCmdFlag = "enable-regexp-cmd" - ExecutableName = "executable-name" - FailOnPreWorkflowHookError = "fail-on-pre-workflow-hook-error" - HideUnchangedPlanComments = "hide-unchanged-plan-comments" - GHHostnameFlag = "gh-hostname" - GHTeamAllowlistFlag = "gh-team-allowlist" - GHTokenFlag = "gh-token" - GHUserFlag = "gh-user" - GHAppIDFlag = "gh-app-id" - GHAppKeyFlag = "gh-app-key" - GHAppKeyFileFlag = "gh-app-key-file" - GHAppSlugFlag = "gh-app-slug" - GHAppInstallationIDFlag = "gh-app-installation-id" - GHOrganizationFlag = "gh-org" - GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec - GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec - GiteaBaseURLFlag = "gitea-base-url" - GiteaTokenFlag = "gitea-token" - GiteaUserFlag = "gitea-user" - GiteaWebhookSecretFlag = "gitea-webhook-secret" // nolint: gosec - GiteaPageSizeFlag = "gitea-page-size" - GitlabHostnameFlag = "gitlab-hostname" - GitlabTokenFlag = "gitlab-token" - GitlabUserFlag = "gitlab-user" - GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec - IncludeGitUntrackedFiles = "include-git-untracked-files" - APISecretFlag = "api-secret" - HidePrevPlanComments = "hide-prev-plan-comments" - QuietPolicyChecks = "quiet-policy-checks" - LockingDBType = "locking-db-type" - LogLevelFlag = "log-level" - MarkdownTemplateOverridesDirFlag = "markdown-template-overrides-dir" - MaxCommentsPerCommand = "max-comments-per-command" - ParallelPoolSize = "parallel-pool-size" - StatsNamespace = "stats-namespace" - AllowDraftPRs = "allow-draft-prs" - PortFlag = "port" - RedisDB = "redis-db" - RedisHost = "redis-host" - RedisPassword = "redis-password" - RedisPort = "redis-port" - RedisTLSEnabled = "redis-tls-enabled" - RedisInsecureSkipVerify = "redis-insecure-skip-verify" - RepoConfigFlag = "repo-config" - RepoConfigJSONFlag = "repo-config-json" - RepoAllowlistFlag = "repo-allowlist" - SilenceNoProjectsFlag = "silence-no-projects" - SilenceForkPRErrorsFlag = "silence-fork-pr-errors" - SilenceVCSStatusNoPlans = "silence-vcs-status-no-plans" - SilenceVCSStatusNoProjectsFlag = "silence-vcs-status-no-projects" - SilenceAllowlistErrorsFlag = "silence-allowlist-errors" - SkipCloneNoChanges = "skip-clone-no-changes" - SlackTokenFlag = "slack-token" - SSLCertFileFlag = "ssl-cert-file" - SSLKeyFileFlag = "ssl-key-file" - RestrictFileList = "restrict-file-list" - TFDownloadFlag = "tf-download" - TFDownloadURLFlag = "tf-download-url" - UseTFPluginCache = "use-tf-plugin-cache" - VarFileAllowlistFlag = "var-file-allowlist" - VCSStatusName = "vcs-status-name" - TFEHostnameFlag = "tfe-hostname" - TFELocalExecutionModeFlag = "tfe-local-execution-mode" - TFETokenFlag = "tfe-token" - WriteGitCredsFlag = "write-git-creds" // nolint: gosec - WebBasicAuthFlag = "web-basic-auth" - WebUsernameFlag = "web-username" - WebPasswordFlag = "web-password" - WebsocketCheckOrigin = "websocket-check-origin" + ADWebhookPasswordFlag = "azuredevops-webhook-password" // nolint: gosec + ADWebhookUserFlag = "azuredevops-webhook-user" + ADTokenFlag = "azuredevops-token" // nolint: gosec + ADUserFlag = "azuredevops-user" + ADHostnameFlag = "azuredevops-hostname" + AllowCommandsFlag = "allow-commands" + AllowForkPRsFlag = "allow-fork-prs" + AtlantisURLFlag = "atlantis-url" + AutoDiscoverModeFlag = "autodiscover-mode" + AutomergeFlag = "automerge" + ParallelPlanFlag = "parallel-plan" + ParallelApplyFlag = "parallel-apply" + AutoplanModules = "autoplan-modules" + AutoplanModulesFromProjects = "autoplan-modules-from-projects" + AutoplanFileListFlag = "autoplan-file-list" + BitbucketBaseURLFlag = "bitbucket-base-url" + BitbucketTokenFlag = "bitbucket-token" + BitbucketUserFlag = "bitbucket-user" + BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" + CheckoutDepthFlag = "checkout-depth" + CheckoutStrategyFlag = "checkout-strategy" + ConfigFlag = "config" + DataDirFlag = "data-dir" + DefaultTFVersionFlag = "default-tf-version" + DisableApplyAllFlag = "disable-apply-all" + DisableAutoplanFlag = "disable-autoplan" + DisableAutoplanLabelFlag = "disable-autoplan-label" + DisableMarkdownFoldingFlag = "disable-markdown-folding" + DisableRepoLockingFlag = "disable-repo-locking" + DisableGlobalApplyLockFlag = "disable-global-apply-lock" + DisableUnlockLabelFlag = "disable-unlock-label" + DiscardApprovalOnPlanFlag = "discard-approval-on-plan" + EmojiReaction = "emoji-reaction" + EnableDiffMarkdownFormat = "enable-diff-markdown-format" + EnablePolicyChecksFlag = "enable-policy-checks" + EnableRegExpCmdFlag = "enable-regexp-cmd" + ExecutableName = "executable-name" + FailOnPreWorkflowHookError = "fail-on-pre-workflow-hook-error" + HideUnchangedPlanComments = "hide-unchanged-plan-comments" + GHHostnameFlag = "gh-hostname" + GHTeamAllowlistFlag = "gh-team-allowlist" + GHTokenFlag = "gh-token" + GHUserFlag = "gh-user" + GHAppIDFlag = "gh-app-id" + GHAppKeyFlag = "gh-app-key" + GHAppKeyFileFlag = "gh-app-key-file" + GHAppSlugFlag = "gh-app-slug" + GHAppInstallationIDFlag = "gh-app-installation-id" + GHOrganizationFlag = "gh-org" + GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec + GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec + GiteaBaseURLFlag = "gitea-base-url" + GiteaTokenFlag = "gitea-token" + GiteaUserFlag = "gitea-user" + GiteaWebhookSecretFlag = "gitea-webhook-secret" // nolint: gosec + GiteaPageSizeFlag = "gitea-page-size" + GitlabHostnameFlag = "gitlab-hostname" + GitlabTokenFlag = "gitlab-token" + GitlabUserFlag = "gitlab-user" + GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec + IncludeGitUntrackedFiles = "include-git-untracked-files" + APISecretFlag = "api-secret" + HidePrevPlanComments = "hide-prev-plan-comments" + QuietPolicyChecks = "quiet-policy-checks" + LockingDBType = "locking-db-type" + LogLevelFlag = "log-level" + MarkdownTemplateOverridesDirFlag = "markdown-template-overrides-dir" + MaxCommentsPerCommand = "max-comments-per-command" + ParallelPoolSize = "parallel-pool-size" + StatsNamespace = "stats-namespace" + AllowDraftPRs = "allow-draft-prs" + PortFlag = "port" + RedisDB = "redis-db" + RedisHost = "redis-host" + RedisPassword = "redis-password" + RedisPort = "redis-port" + RedisTLSEnabled = "redis-tls-enabled" + RedisInsecureSkipVerify = "redis-insecure-skip-verify" + RepoConfigFlag = "repo-config" + RepoConfigJSONFlag = "repo-config-json" + RepoAllowlistFlag = "repo-allowlist" + SetAtlantisApplyCheckSuccessfulIfNoChangesFlag = "set-atlantis-apply-check-successful-if-no-changes" + SilenceNoProjectsFlag = "silence-no-projects" + SilenceForkPRErrorsFlag = "silence-fork-pr-errors" + SilenceVCSStatusNoPlans = "silence-vcs-status-no-plans" + SilenceVCSStatusNoProjectsFlag = "silence-vcs-status-no-projects" + SilenceAllowlistErrorsFlag = "silence-allowlist-errors" + SkipCloneNoChanges = "skip-clone-no-changes" + SlackTokenFlag = "slack-token" + SSLCertFileFlag = "ssl-cert-file" + SSLKeyFileFlag = "ssl-key-file" + RestrictFileList = "restrict-file-list" + TFDownloadFlag = "tf-download" + TFDownloadURLFlag = "tf-download-url" + UseTFPluginCache = "use-tf-plugin-cache" + VarFileAllowlistFlag = "var-file-allowlist" + VCSStatusName = "vcs-status-name" + TFEHostnameFlag = "tfe-hostname" + TFELocalExecutionModeFlag = "tfe-local-execution-mode" + TFETokenFlag = "tfe-token" + WriteGitCredsFlag = "write-git-creds" // nolint: gosec + WebBasicAuthFlag = "web-basic-auth" + WebUsernameFlag = "web-username" + WebPasswordFlag = "web-password" + WebsocketCheckOrigin = "websocket-check-origin" // NOTE: Must manually set these as defaults in the setDefaults function. DefaultADBasicUser = "" @@ -526,6 +527,10 @@ var boolFlags = map[string]boolFlag{ description: "Controls whether the Redis client verifies the Redis server's certificate chain and host name. If true, accepts any certificate presented by the server and any host name in that certificate.", defaultValue: DefaultRedisInsecureSkipVerify, }, + SetAtlantisApplyCheckSuccessfulIfNoChangesFlag: { + description: "Set the `atlantis/apply` pull request status check to \"passing\" if \"No Changes\" are detected.", + defaultValue: true, + }, SilenceNoProjectsFlag: { description: "Silences Atlants from responding to PRs when it finds no projects.", defaultValue: false, diff --git a/cmd/server_test.go b/cmd/server_test.go index 5402b28ce5..d5bcd4af6f 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -126,35 +126,36 @@ var testFlags = map[string]interface{}{ RepoAllowlistFlag: "github.com/runatlantis/atlantis", RepoConfigFlag: "", RepoConfigJSONFlag: "", - SilenceNoProjectsFlag: false, - SilenceVCSStatusNoProjectsFlag: false, - SilenceForkPRErrorsFlag: true, - SilenceAllowlistErrorsFlag: true, - SilenceVCSStatusNoPlans: true, - SkipCloneNoChanges: true, - SlackTokenFlag: "slack-token", - SSLCertFileFlag: "cert-file", - SSLKeyFileFlag: "key-file", - RestrictFileList: false, - TFDownloadFlag: true, - TFDownloadURLFlag: "https://my-hostname.com", - TFEHostnameFlag: "my-hostname", - TFELocalExecutionModeFlag: true, - TFETokenFlag: "my-token", - UseTFPluginCache: true, - VarFileAllowlistFlag: "/path", - VCSStatusName: "my-status", - WebBasicAuthFlag: false, - WebPasswordFlag: "atlantis", - WebUsernameFlag: "atlantis", - WebsocketCheckOrigin: false, - WriteGitCredsFlag: true, - DisableAutoplanFlag: true, - DisableAutoplanLabelFlag: "no-auto-plan", - DisableUnlockLabelFlag: "do-not-unlock", - EnablePolicyChecksFlag: false, - EnableRegExpCmdFlag: false, - EnableDiffMarkdownFormat: false, + SetAtlantisApplyCheckSuccessfulIfNoChangesFlag: true, + SilenceNoProjectsFlag: false, + SilenceVCSStatusNoProjectsFlag: false, + SilenceForkPRErrorsFlag: true, + SilenceAllowlistErrorsFlag: true, + SilenceVCSStatusNoPlans: true, + SkipCloneNoChanges: true, + SlackTokenFlag: "slack-token", + SSLCertFileFlag: "cert-file", + SSLKeyFileFlag: "key-file", + RestrictFileList: false, + TFDownloadFlag: true, + TFDownloadURLFlag: "https://my-hostname.com", + TFEHostnameFlag: "my-hostname", + TFELocalExecutionModeFlag: true, + TFETokenFlag: "my-token", + UseTFPluginCache: true, + VarFileAllowlistFlag: "/path", + VCSStatusName: "my-status", + WebBasicAuthFlag: false, + WebPasswordFlag: "atlantis", + WebUsernameFlag: "atlantis", + WebsocketCheckOrigin: false, + WriteGitCredsFlag: true, + DisableAutoplanFlag: true, + DisableAutoplanLabelFlag: "no-auto-plan", + DisableUnlockLabelFlag: "do-not-unlock", + EnablePolicyChecksFlag: false, + EnableRegExpCmdFlag: false, + EnableDiffMarkdownFormat: false, } func TestExecute_Defaults(t *testing.T) { diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 1fd17cbfdc..5d8b5d41d3 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -1127,6 +1127,16 @@ This is useful when you have many projects and want to keep the pull request cle like `atlantis plan -p .*` will still work if used. normal commands will stil be blocked if necessary. Defaults to `false`. +### `--set-atlantis-apply-check-successful-if-no-changes` + ```bash + atlantis server --set-atlantis-apply-check-successful-if-no-changes + # or + ATLANTIS_SET_ATLANTIS_APPLY_CHECK_SUCCESSFUL_IF_NO_CHANGES=false + ``` +`--set-atlantis-apply-check-successful-if-no-changes` will set the `atlantis/apply` status check to "passing" on a VCS pull request if the `atlantis plan` command results in "No Changes". +This is useful, for example, when enabling auto-merge for pull requests that do not involve resource changes, such as automatic dependency updates. +Defaults to `true`. + ### `--silence-allowlist-errors` ```bash diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 7f63f191f5..722c1261fd 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1524,6 +1524,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers lockingClient, discardApprovalOnPlan, e2ePullReqStatusFetcher, + true, ) applyCommandRunner := events.NewApplyCommandRunner( @@ -1541,6 +1542,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers silenceNoProjects, false, e2ePullReqStatusFetcher, + true, ) approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner( diff --git a/server/events/apply_command_runner.go b/server/events/apply_command_runner.go index ee6bf8ab1f..246e4d8114 100644 --- a/server/events/apply_command_runner.go +++ b/server/events/apply_command_runner.go @@ -22,6 +22,7 @@ func NewApplyCommandRunner( SilenceNoProjects bool, silenceVCSStatusNoProjects bool, pullReqStatusFetcher vcs.PullReqStatusFetcher, + SetAtlantisApplyCheckSuccessfulIfNoChanges bool, ) *ApplyCommandRunner { return &ApplyCommandRunner{ vcsClient: vcsClient, @@ -38,22 +39,24 @@ func NewApplyCommandRunner( SilenceNoProjects: SilenceNoProjects, silenceVCSStatusNoProjects: silenceVCSStatusNoProjects, pullReqStatusFetcher: pullReqStatusFetcher, + SetAtlantisApplyCheckSuccessfulIfNoChanges: SetAtlantisApplyCheckSuccessfulIfNoChanges, } } type ApplyCommandRunner struct { - DisableApplyAll bool - Backend locking.Backend - locker locking.ApplyLockChecker - vcsClient vcs.Client - commitStatusUpdater CommitStatusUpdater - prjCmdBuilder ProjectApplyCommandBuilder - prjCmdRunner ProjectApplyCommandRunner - autoMerger *AutoMerger - pullUpdater *PullUpdater - dbUpdater *DBUpdater - parallelPoolSize int - pullReqStatusFetcher vcs.PullReqStatusFetcher + DisableApplyAll bool + Backend locking.Backend + locker locking.ApplyLockChecker + vcsClient vcs.Client + commitStatusUpdater CommitStatusUpdater + prjCmdBuilder ProjectApplyCommandBuilder + prjCmdRunner ProjectApplyCommandRunner + autoMerger *AutoMerger + pullUpdater *PullUpdater + dbUpdater *DBUpdater + parallelPoolSize int + pullReqStatusFetcher vcs.PullReqStatusFetcher + SetAtlantisApplyCheckSuccessfulIfNoChanges bool // SilenceNoProjects is whether Atlantis should respond to PRs if no projects // are found SilenceNoProjects bool @@ -200,7 +203,10 @@ func (a *ApplyCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus var numErrored int status := models.SuccessCommitStatus - numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus) + numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + if a.SetAtlantisApplyCheckSuccessfulIfNoChanges { + numSuccess += pullStatus.StatusCount(models.PlannedNoChangesPlanStatus) + } numErrored = pullStatus.StatusCount(models.ErroredApplyStatus) if numErrored > 0 { diff --git a/server/events/command_runner_internal_test.go b/server/events/command_runner_internal_test.go index 02a54c3870..97cf71ac60 100644 --- a/server/events/command_runner_internal_test.go +++ b/server/events/command_runner_internal_test.go @@ -11,14 +11,16 @@ import ( func TestApplyUpdateCommitStatus(t *testing.T) { cases := map[string]struct { - cmd command.Name - pullStatus models.PullStatus - expStatus models.CommitStatus - expNumSuccess int - expNumTotal int + cmd command.Name + SetAtlantisApplyCheckSuccessfulIfNoChanges bool + pullStatus models.PullStatus + expStatus models.CommitStatus + expNumSuccess int + expNumTotal int }{ "apply, one pending": { cmd: command.Apply, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, pullStatus: models.PullStatus{ Projects: []models.ProjectStatus{ { @@ -35,6 +37,7 @@ func TestApplyUpdateCommitStatus(t *testing.T) { }, "apply, all successful": { cmd: command.Apply, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, pullStatus: models.PullStatus{ Projects: []models.ProjectStatus{ { @@ -51,6 +54,7 @@ func TestApplyUpdateCommitStatus(t *testing.T) { }, "apply, one errored, one pending": { cmd: command.Apply, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, pullStatus: models.PullStatus{ Projects: []models.ProjectStatus{ { @@ -70,6 +74,24 @@ func TestApplyUpdateCommitStatus(t *testing.T) { }, "apply, one planned no changes": { cmd: command.Apply, + SetAtlantisApplyCheckSuccessfulIfNoChanges: false, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.AppliedPlanStatus, + }, + { + Status: models.PlannedNoChangesPlanStatus, + }, + }, + }, + expStatus: models.PendingCommitStatus, + expNumSuccess: 1, + expNumTotal: 2, + }, + "apply, one planned no changes, skip apply when no changes": { + cmd: command.Apply, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, pullStatus: models.PullStatus{ Projects: []models.ProjectStatus{ { @@ -90,7 +112,8 @@ func TestApplyUpdateCommitStatus(t *testing.T) { t.Run(name, func(t *testing.T) { csu := &MockCSU{} cr := &ApplyCommandRunner{ - commitStatusUpdater: csu, + commitStatusUpdater: csu, + SetAtlantisApplyCheckSuccessfulIfNoChanges: c.SetAtlantisApplyCheckSuccessfulIfNoChanges, } cr.updateCommitStatus(&command.Context{}, c.pullStatus) Equals(t, models.Repo{}, csu.CalledRepo) diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 8acea27b98..8475706dd5 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -70,14 +70,15 @@ var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner var postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner type TestConfig struct { - parallelPoolSize int - SilenceNoProjects bool - silenceVCSStatusNoPlans bool - silenceVCSStatusNoProjects bool - StatusName string - discardApprovalOnPlan bool - backend locking.Backend - DisableUnlockLabel string + parallelPoolSize int + SilenceNoProjects bool + silenceVCSStatusNoPlans bool + silenceVCSStatusNoProjects bool + StatusName string + discardApprovalOnPlan bool + backend locking.Backend + SetAtlantisApplyCheckSuccessfulIfNoChanges bool + DisableUnlockLabel string } func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.MockClient { @@ -94,7 +95,8 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock StatusName: "atlantis-test", discardApprovalOnPlan: false, backend: defaultBoltDB, - DisableUnlockLabel: "do-not-unlock", + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, + DisableUnlockLabel: "do-not-unlock", } for _, op := range options { @@ -163,6 +165,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock lockingLocker, testConfig.discardApprovalOnPlan, pullReqStatusFetcher, + testConfig.SetAtlantisApplyCheckSuccessfulIfNoChanges, ) applyCommandRunner = events.NewApplyCommandRunner( @@ -180,6 +183,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock testConfig.SilenceNoProjects, testConfig.silenceVCSStatusNoProjects, pullReqStatusFetcher, + testConfig.SetAtlantisApplyCheckSuccessfulIfNoChanges, ) approvePoliciesCommandRunner = events.NewApprovePoliciesCommandRunner( diff --git a/server/events/plan_command_runner.go b/server/events/plan_command_runner.go index c2b6b7a107..d76e53559f 100644 --- a/server/events/plan_command_runner.go +++ b/server/events/plan_command_runner.go @@ -26,6 +26,7 @@ func NewPlanCommandRunner( lockingLocker locking.Locker, discardApprovalOnPlan bool, pullReqStatusFetcher vcs.PullReqStatusFetcher, + SetAtlantisApplyCheckSuccessfulIfNoChanges bool, ) *PlanCommandRunner { return &PlanCommandRunner{ silenceVCSStatusNoPlans: silenceVCSStatusNoPlans, @@ -46,6 +47,7 @@ func NewPlanCommandRunner( lockingLocker: lockingLocker, DiscardApprovalOnPlan: discardApprovalOnPlan, pullReqStatusFetcher: pullReqStatusFetcher, + SetAtlantisApplyCheckSuccessfulIfNoChanges: SetAtlantisApplyCheckSuccessfulIfNoChanges, } } @@ -74,9 +76,10 @@ type PlanCommandRunner struct { lockingLocker locking.Locker // DiscardApprovalOnPlan controls if all already existing approvals should be removed/dismissed before executing // a plan. - DiscardApprovalOnPlan bool - pullReqStatusFetcher vcs.PullReqStatusFetcher - SilencePRComments []string + DiscardApprovalOnPlan bool + pullReqStatusFetcher vcs.PullReqStatusFetcher + SilencePRComments []string + SetAtlantisApplyCheckSuccessfulIfNoChanges bool } func (p *PlanCommandRunner) runAutoplan(ctx *command.Context) { @@ -150,7 +153,9 @@ func (p *PlanCommandRunner) runAutoplan(ctx *command.Context) { } p.updateCommitStatus(ctx, pullStatus, command.Plan) - p.updateCommitStatus(ctx, pullStatus, command.Apply) + if p.SetAtlantisApplyCheckSuccessfulIfNoChanges { + p.updateCommitStatus(ctx, pullStatus, command.Apply) + } // Check if there are any planned projects and if there are any errors or if plans are being deleted if len(policyCheckCmds) > 0 && @@ -281,7 +286,9 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) { } p.updateCommitStatus(ctx, pullStatus, command.Plan) - p.updateCommitStatus(ctx, pullStatus, command.Apply) + if p.SetAtlantisApplyCheckSuccessfulIfNoChanges { + p.updateCommitStatus(ctx, pullStatus, command.Apply) + } // Runs policy checks step after all plans are successful. // This step does not approve any policies that require approval. diff --git a/server/events/plan_command_runner_test.go b/server/events/plan_command_runner_test.go index 57f8c5bfce..ff275905df 100644 --- a/server/events/plan_command_runner_test.go +++ b/server/events/plan_command_runner_test.go @@ -516,14 +516,39 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { RegisterMockTestingT(t) cases := []struct { - Description string - ProjectContexts []command.ProjectContext - ProjectResults []command.ProjectResult - PrevPlanStored bool // stores a previous "No changes" plan in the backend - DoNotUpdateApply bool // certain circumtances we want to skip the call to update apply - ExpVCSApplyStatusTotal int - ExpVCSApplyStatusSucc int + Description string + ProjectContexts []command.ProjectContext + ProjectResults []command.ProjectResult + PrevPlanStored bool // stores a previous "No changes" plan in the backend + DoNotUpdateApply bool // certain circumstances we want to skip the call to update apply + ExpVCSApplyStatusTotal int + ExpVCSApplyStatusSucc int + SetAtlantisApplyCheckSuccessfulIfNoChanges bool }{ + { + Description: "When planning without the flag, don't set the atlantis/apply VCS status", + SetAtlantisApplyCheckSuccessfulIfNoChanges: false, + DoNotUpdateApply: true, + }, + { + Description: "When planning with the flag, set the atlantis/apply VCS status to 0/0", + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, + ExpVCSApplyStatusTotal: 0, + ExpVCSApplyStatusSucc: 0, + }, + { + Description: "When planning with the previous plan that results in No Changes and without the flag, don't set the atlantis/apply VCS status", + PrevPlanStored: true, + SetAtlantisApplyCheckSuccessfulIfNoChanges: false, + DoNotUpdateApply: true, + }, + { + Description: "When planning with the previous plan that results in No Changes and setting the flag, set the atlantis/apply VCS status to 1/1", + PrevPlanStored: true, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, + ExpVCSApplyStatusTotal: 1, + ExpVCSApplyStatusSucc: 1, + }, { Description: "When planning with changes, do not change the apply status", ProjectContexts: []command.ProjectContext{ @@ -544,7 +569,29 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { DoNotUpdateApply: true, }, { - Description: "When planning with no changes, set the 1/1 apply status", + Description: "When planning with no changes with the flag, set the 1/1 apply status", + ProjectContexts: []command.ProjectContext{ + { + CommandName: command.Plan, + RepoRelDir: "mydir", + }, + }, + ProjectResults: []command.ProjectResult{ + { + RepoRelDir: "mydir", + Command: command.Plan, + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "No changes. Infrastructure is up-to-date.", + }, + }, + }, + DoNotUpdateApply: false, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, + ExpVCSApplyStatusTotal: 1, + ExpVCSApplyStatusSucc: 1, + }, + { + Description: "When planning with no changes without the flag, set the 0/0 apply status", ProjectContexts: []command.ProjectContext{ { CommandName: command.Plan, @@ -560,8 +607,10 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { }, }, }, - ExpVCSApplyStatusTotal: 1, - ExpVCSApplyStatusSucc: 1, + DoNotUpdateApply: true, + SetAtlantisApplyCheckSuccessfulIfNoChanges: false, + ExpVCSApplyStatusTotal: 0, + ExpVCSApplyStatusSucc: 0, }, { Description: "When planning with no changes and previous plan with no changes do not set the apply status", @@ -600,6 +649,7 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { }, }, }, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, PrevPlanStored: true, ExpVCSApplyStatusTotal: 2, ExpVCSApplyStatusSucc: 2, @@ -689,6 +739,7 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { }, }, }, + SetAtlantisApplyCheckSuccessfulIfNoChanges: true, PrevPlanStored: true, ExpVCSApplyStatusTotal: 2, ExpVCSApplyStatusSucc: 2, @@ -703,6 +754,7 @@ func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) { Ok(t, err) vcsClient := setup(t, func(tc *TestConfig) { + tc.SetAtlantisApplyCheckSuccessfulIfNoChanges = c.SetAtlantisApplyCheckSuccessfulIfNoChanges tc.backend = db }) diff --git a/server/server.go b/server/server.go index 4a0e5aa738..4ae04d0e7d 100644 --- a/server/server.go +++ b/server/server.go @@ -737,6 +737,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { lockingClient, userConfig.DiscardApprovalOnPlanFlag, pullReqStatusFetcher, + userConfig.SetAtlantisApplyCheckSuccessfulIfNoChanges, ) applyCommandRunner := events.NewApplyCommandRunner( @@ -754,6 +755,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.SilenceNoProjects, userConfig.SilenceVCSStatusNoProjects, pullReqStatusFetcher, + userConfig.SetAtlantisApplyCheckSuccessfulIfNoChanges, ) approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner( diff --git a/server/user_config.go b/server/user_config.go index 91ed090ac6..0844ef7196 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -92,6 +92,7 @@ type UserConfig struct { RepoConfigJSON string `mapstructure:"repo-config-json"` RepoAllowlist string `mapstructure:"repo-allowlist"` + SetAtlantisApplyCheckSuccessfulIfNoChanges bool `mapstructure:"set-atlantis-apply-check-successful-if-no-changes"` // SilenceNoProjects is whether Atlantis should respond to a PR if no projects are found. SilenceNoProjects bool `mapstructure:"silence-no-projects"` SilenceForkPRErrors bool `mapstructure:"silence-fork-pr-errors"`