diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 6c3ad7b44d..4b41068e82 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -46,11 +46,13 @@ need to be defined. ```yaml version: 3 automerge: true +delete_source_branch_on_merge: true projects: - name: my-project-name dir: . workspace: default terraform_version: v0.11.0 + delete_source_branch_on_merge: true autoplan: when_modified: ["*.tf", "../modules/**.tf"] enabled: true @@ -189,13 +191,15 @@ See [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.html#cus ```yaml version: automerge: +delete_source_branch_on_merge: projects: workflows: ``` | Key | Type | Default | Required | Description | |-------------------------------|----------------------------------------------------------|---------|----------|-------------------------------------------------------------| | version | int | none | **yes** | This key is required and must be set to `3` | -| automerge | bool | `false` | no | Automatically merge pull request when all plans are applied | +| automerge | bool | `false` | no | Automatically merges pull request when all plans are applied| +| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge | | projects | array[[Project](repo-level-atlantis-yaml.html#project)] | `[]` | no | Lists the projects in this repo | | workflows
*(restricted)* | map[string: [Workflow](custom-workflows.html#reference)] | `{}` | no | Custom workflows | @@ -204,6 +208,7 @@ workflows: name: myname dir: mydir workspace: myworkspace +delete_source_branch_on_merge: autoplan: terraform_version: 0.11.0 apply_requirements: ["approved"] @@ -216,6 +221,7 @@ workflow: myworkflow | dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. | | workspace | string | `"default"` | no | The [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | | autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | +| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge | | terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | | apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. | | workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 9dbb4e93ac..06b644cf1e 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -42,7 +42,7 @@ repos: # allowed_overrides specifies which keys can be overridden by this repo in # its atlantis.yaml file. - allowed_overrides: [apply_requirements, workflow] + allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge] # allowed_workflows specifies which workflows the repos that match # are allowed to select. @@ -52,6 +52,10 @@ repos: # workflows. If false (default), the repo can only use server-side defined # workflows. allow_custom_workflows: true + + # delete_source_branch_on_merge defines whether the source branch would be deleted on merge + # If false (default), the source branch won't be deleted on merge + delete_source_branch_on_merge: true # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution. pre_workflow_hooks: @@ -366,14 +370,15 @@ If you set a workflow with the key `default`, it will override this. ::: ### Repo -| Key | Type | Default | Required | Description | -|------------------------|----------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | string | none | yes | Value can be a regular expression when specified as /<regex>/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. | -| workflow | string | none | no | A custom workflow. | -| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. | -| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements` and `workflow` | -| allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. | -| allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.html). | +| Key | Type | Default | Required | Description | +|-------------------------------|----------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | none | yes | Value can be a regular expression when specified as /<regex>/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. | +| workflow | string | none | no | A custom workflow. | +| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. | +| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow` and `delete_source_branch_on_merge` | +| allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. | +| allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.html). | +| delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge (only AzureDevOps and GitLab support) | :::tip Notes diff --git a/server/events/apply_command_runner.go b/server/events/apply_command_runner.go index b54ea99a4e..318bc43ba5 100644 --- a/server/events/apply_command_runner.go +++ b/server/events/apply_command_runner.go @@ -158,7 +158,7 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) { a.updateCommitStatus(ctx, pullStatus) if a.autoMerger.automergeEnabled(projectCmds) { - a.autoMerger.automerge(ctx, pullStatus) + a.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds)) } } diff --git a/server/events/automerger.go b/server/events/automerger.go index decd35b8fe..4ce7e3d2d8 100644 --- a/server/events/automerger.go +++ b/server/events/automerger.go @@ -12,7 +12,7 @@ type AutoMerger struct { GlobalAutomerge bool } -func (c *AutoMerger) automerge(ctx *CommandContext, pullStatus models.PullStatus) { +func (c *AutoMerger) automerge(ctx *CommandContext, pullStatus models.PullStatus, deleteSourceBranchOnMerge bool) { // We only automerge if all projects have been successfully applied. for _, p := range pullStatus.Projects { if p.Status != models.AppliedPlanStatus { @@ -29,7 +29,9 @@ func (c *AutoMerger) automerge(ctx *CommandContext, pullStatus models.PullStatus // Make the API call to perform the merge. ctx.Log.Info("automerging pull request") - err := c.VCSClient.MergePull(ctx.Pull) + var pullOptions models.PullRequestOptions + pullOptions.DeleteSourceBranchOnMerge = deleteSourceBranchOnMerge + err := c.VCSClient.MergePull(ctx.Pull, pullOptions) if err != nil { ctx.Log.Err("automerging failed: %s", err) @@ -48,3 +50,9 @@ func (c *AutoMerger) automergeEnabled(projectCmds []models.ProjectCommandContext // Otherwise we check if this repo is configured for automerging. (len(projectCmds) > 0 && projectCmds[0].AutomergeEnabled) } + +// deleteSourceBranchOnMergeEnabled returns true if we should delete the source branch on merge in this context. +func (c *AutoMerger) deleteSourceBranchOnMergeEnabled(projectCmds []models.ProjectCommandContext) bool { + //check if this repo is configured for automerging. + return (len(projectCmds) > 0 && projectCmds[0].DeleteSourceBranchOnMerge) +} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 1167b85c71..24c717a01d 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -339,6 +339,4 @@ func (c *DefaultCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logg } } -// automergeComment is the comment that gets posted when Atlantis automatically -// merges the PR. var automergeComment = `Automatically merging because all plans have been successfully applied.` diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 7ef503e968..1254e96d03 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -649,8 +649,12 @@ func TestApplyWithAutoMerge_VSCMerge(t *testing.T) { autoMerger.GlobalAutomerge = true defer func() { autoMerger.GlobalAutomerge = false }() + pullOptions := models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, + } + ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand}) - vcsClient.VerifyWasCalledOnce().MergePull(modelPull) + vcsClient.VerifyWasCalledOnce().MergePull(modelPull, pullOptions) } func TestRunApply_DiscardedProjects(t *testing.T) { @@ -688,7 +692,8 @@ func TestRunApply_DiscardedProjects(t *testing.T) { When(workingDir.GetPullDir(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())). ThenReturn(tmp, nil) ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, &pull, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand}) - vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest()) + + vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest(), matchers.AnyModelsPullRequestOptions()) } func TestRunCommentCommand_DrainOngoing(t *testing.T) { diff --git a/server/events/mocks/matchers/models_pullrequestoptions.go b/server/events/mocks/matchers/models_pullrequestoptions.go new file mode 100644 index 0000000000..b9d5e471a2 --- /dev/null +++ b/server/events/mocks/matchers/models_pullrequestoptions.go @@ -0,0 +1,21 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsPullRequestOptions() models.PullRequestOptions { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.PullRequestOptions))(nil)).Elem())) + var nullValue models.PullRequestOptions + return nullValue +} + +func EqModelsPullRequestOptions(value models.PullRequestOptions) models.PullRequestOptions { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.PullRequestOptions + return nullValue +} diff --git a/server/events/models/models.go b/server/events/models/models.go index bb3236d67d..817d4d1f90 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -171,6 +171,13 @@ type PullRequest struct { BaseRepo Repo } +// PullRequestOptions is used to set optional paralmeters for PullRequest +type PullRequestOptions struct { + // When DeleteSourceBranchOnMerge flag is set to true VCS deletes the source branch after the PR is merged + // Applied by GitLab & AzureDevops + DeleteSourceBranchOnMerge bool +} + type PullRequestState int const ( @@ -391,6 +398,8 @@ type ProjectCommandContext struct { // PolicySets represent the policies that are run on the plan as part of the // policy check stage PolicySets valid.PolicySets + // DeleteSourceBranchOnMerge will attempt to allow a branch to be deleted when merged (AzureDevOps & GitLab Support Only) + DeleteSourceBranchOnMerge bool } // GetShowResultFileName returns the filename (not the path) to store the tf show result diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 4da9ccc17c..06dcf4c3c0 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -25,6 +25,8 @@ const ( DefaultParallelApplyEnabled = false // DefaultParallelPlanEnabled is the default for the parallel plan setting. DefaultParallelPlanEnabled = false + // DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge + DefaultDeleteSourceBranchOnMerge = false ) func NewProjectCommandBuilder( @@ -235,6 +237,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags, repoDir, repoCfg.Automerge, + mergedCfg.DeleteSourceBranchOnMerge, repoCfg.ParallelApply, repoCfg.ParallelPlan, verbose, @@ -261,6 +264,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags, repoDir, DefaultAutomergeEnabled, + pCfg.DeleteSourceBranchOnMerge, DefaultParallelApplyEnabled, DefaultParallelPlanEnabled, verbose, @@ -451,10 +455,12 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex var projCtxs []models.ProjectCommandContext var projCfg valid.MergedProjectCfg automerge := DefaultAutomergeEnabled + deleteBranchOnMerge := DefaultDeleteSourceBranchOnMerge parallelApply := DefaultParallelApplyEnabled parallelPlan := DefaultParallelPlanEnabled if repoCfgPtr != nil { automerge = repoCfgPtr.Automerge + deleteBranchOnMerge = projCfg.DeleteSourceBranchOnMerge parallelApply = repoCfgPtr.ParallelApply parallelPlan = repoCfgPtr.ParallelPlan } @@ -477,8 +483,9 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex commentFlags, repoDir, automerge, - parallelApply, + deleteBranchOnMerge, parallelPlan, + parallelApply, verbose, )...) } @@ -492,8 +499,9 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex commentFlags, repoDir, automerge, - parallelApply, + deleteBranchOnMerge, parallelPlan, + parallelApply, verbose, )...) } @@ -503,7 +511,6 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex } return projCtxs, nil - } // validateWorkspaceAllowed returns an error if repoCfg defines projects in diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index 13b43c9a19..f681ec4df7 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -33,7 +33,7 @@ type ProjectCommandContextBuilder interface { prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, parallelPlan, parallelApply, verbose bool, + automerge, deleteSourceBranchOnMerge, parallelPlan, parallelApply, verbose bool, ) []models.ProjectCommandContext } @@ -47,7 +47,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, parallelApply, parallelPlan, verbose bool, + automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, ) (projectCmds []models.ProjectCommandContext) { ctx.Log.Debug("Building project command context for %s", cmdName) @@ -75,6 +75,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( prjCfg.PolicySets, escapeArgs(commentFlags), automerge, + deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -94,7 +95,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, parallelApply, parallelPlan, verbose bool, + automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, ) (projectCmds []models.ProjectCommandContext) { ctx.Log.Debug("PolicyChecks are enabled") projectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext( @@ -104,6 +105,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( commentFlags, repoDir, automerge, + deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -123,6 +125,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( prjCfg.PolicySets, escapeArgs(commentFlags), automerge, + deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -143,6 +146,7 @@ func newProjectCommandContext(ctx *CommandContext, policySets valid.PolicySets, escapedCommentArgs []string, automergeEnabled bool, + deleteSourceBranchOnMerge, parallelApplyEnabled bool, parallelPlanEnabled bool, verbose bool, @@ -167,30 +171,31 @@ func newProjectCommandContext(ctx *CommandContext, } return models.ProjectCommandContext{ - CommandName: cmd, - ApplyCmd: applyCmd, - BaseRepo: ctx.Pull.BaseRepo, - EscapedCommentArgs: escapedCommentArgs, - AutomergeEnabled: automergeEnabled, - ParallelApplyEnabled: parallelApplyEnabled, - ParallelPlanEnabled: parallelPlanEnabled, - AutoplanEnabled: projCfg.AutoplanEnabled, - Steps: steps, - HeadRepo: ctx.HeadRepo, - Log: ctx.Log, - PullMergeable: ctx.PullMergeable, - ProjectPlanStatus: projectPlanStatus, - Pull: ctx.Pull, - ProjectName: projCfg.Name, - ApplyRequirements: projCfg.ApplyRequirements, - RePlanCmd: planCmd, - RepoRelDir: projCfg.RepoRelDir, - RepoConfigVersion: projCfg.RepoCfgVersion, - TerraformVersion: projCfg.TerraformVersion, - User: ctx.User, - Verbose: verbose, - Workspace: projCfg.Workspace, - PolicySets: policySets, + CommandName: cmd, + ApplyCmd: applyCmd, + BaseRepo: ctx.Pull.BaseRepo, + EscapedCommentArgs: escapedCommentArgs, + AutomergeEnabled: automergeEnabled, + DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, + ParallelApplyEnabled: parallelApplyEnabled, + ParallelPlanEnabled: parallelPlanEnabled, + AutoplanEnabled: projCfg.AutoplanEnabled, + Steps: steps, + HeadRepo: ctx.HeadRepo, + Log: ctx.Log, + PullMergeable: ctx.PullMergeable, + ProjectPlanStatus: projectPlanStatus, + Pull: ctx.Pull, + ProjectName: projCfg.Name, + ApplyRequirements: projCfg.ApplyRequirements, + RePlanCmd: planCmd, + RepoRelDir: projCfg.RepoRelDir, + RepoConfigVersion: projCfg.RepoCfgVersion, + TerraformVersion: projCfg.TerraformVersion, + User: ctx.User, + Verbose: verbose, + Workspace: projCfg.Workspace, + PolicySets: policySets, } } diff --git a/server/events/project_command_context_builder_test.go b/server/events/project_command_context_builder_test.go index c53408650d..d0a3de9bdd 100644 --- a/server/events/project_command_context_builder_test.go +++ b/server/events/project_command_context_builder_test.go @@ -57,7 +57,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { }, } - result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false) + result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false, false) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) }) @@ -77,7 +77,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { }, } - result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false) + result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false, false) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) }) diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index ee96fdc046..beaf847921 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -288,7 +288,7 @@ func (g *AzureDevopsClient) UpdateStatus(repo models.Repo, pull models.PullReque // If the user has set a branch policy that disallows no fast-forward, the merge will fail // until we handle branch policies // https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops -func (g *AzureDevopsClient) MergePull(pull models.PullRequest) error { +func (g *AzureDevopsClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { owner, project, repoName := SplitAzureDevopsRepoFullName(pull.BaseRepo.FullName) descriptor := "Atlantis Terraform Pull Request Automation" @@ -313,7 +313,7 @@ func (g *AzureDevopsClient) MergePull(pull models.PullRequest) error { completionOpts := azuredevops.GitPullRequestCompletionOptions{ BypassPolicy: new(bool), BypassReason: azuredevops.String(""), - DeleteSourceBranch: new(bool), + DeleteSourceBranch: &pullOptions.DeleteSourceBranchOnMerge, MergeCommitMessage: azuredevops.String(common.AutomergeCommitMsg), MergeStrategy: &mcm, SquashMerge: new(bool), diff --git a/server/events/vcs/azuredevops_client_test.go b/server/events/vcs/azuredevops_client_test.go index 5d4e4c4153..b127d71829 100644 --- a/server/events/vcs/azuredevops_client_test.go +++ b/server/events/vcs/azuredevops_client_test.go @@ -127,6 +127,8 @@ func TestAzureDevopsClient_MergePull(t *testing.T) { Owner: "owner", Name: "repo", }, + }, models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, }) if c.expErr == "" { Ok(t, err) diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index ff60faf6c6..edf4786926 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -194,7 +194,7 @@ func (b *Client) UpdateStatus(repo models.Repo, pull models.PullRequest, status } // MergePull merges the pull request. -func (b *Client) MergePull(pull models.PullRequest) error { +func (b *Client) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/merge", b.BaseURL, pull.BaseRepo.FullName, pull.Num) _, err := b.makeRequest("POST", path, nil) return err diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index adb6eba0d8..a30ccd6f70 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -244,7 +244,7 @@ func (b *Client) UpdateStatus(repo models.Repo, pull models.PullRequest, status } // MergePull merges the pull request. -func (b *Client) MergePull(pull models.PullRequest) error { +func (b *Client) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { projectKey, err := b.GetProjectKey(pull.BaseRepo.Name, pull.BaseRepo.SanitizedCloneURL) if err != nil { return err diff --git a/server/events/vcs/bitbucketserver/client_test.go b/server/events/vcs/bitbucketserver/client_test.go index 6ff44f83af..016db6a344 100644 --- a/server/events/vcs/bitbucketserver/client_test.go +++ b/server/events/vcs/bitbucketserver/client_test.go @@ -177,6 +177,8 @@ func TestClient_MergePull(t *testing.T) { Hostname: "bitbucket.org", }, }, + }, models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, }) Ok(t, err) } diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index dad6fa1901..e5902aafbd 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -36,7 +36,7 @@ type Client interface { // url is an optional link that users should click on for more information // about this status. UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error - MergePull(pull models.PullRequest) error + MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error MarkdownPullLink(pull models.PullRequest) (string, error) // DownloadRepoConfigFile return `atlantis.yaml` content from VCS (which support fetch a single file from repository) diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 20d3ab1ae9..9dd8180b07 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -329,7 +329,7 @@ func (g *GithubClient) UpdateStatus(repo models.Repo, pull models.PullRequest, s } // MergePull merges the pull request. -func (g *GithubClient) MergePull(pull models.PullRequest) error { +func (g *GithubClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { // Users can set their repo to disallow certain types of merging. // We detect which types aren't allowed and use the type that is. g.logger.Debug("GET /repos/%v/%v", pull.BaseRepo.Owner, pull.BaseRepo.Name) diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go index 70d719704e..0987a95cfd 100644 --- a/server/events/vcs/github_client_test.go +++ b/server/events/vcs/github_client_test.go @@ -640,6 +640,8 @@ func TestGithubClient_MergePullHandlesError(t *testing.T) { }, }, Num: 1, + }, models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, }) if c.expErr == "" { @@ -761,7 +763,10 @@ func TestGithubClient_MergePullCorrectMethod(t *testing.T) { }, }, Num: 1, + }, models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, }) + Ok(t, err) }) } diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index 9cb320f4b7..9c3a2a4716 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -214,13 +214,14 @@ func (g *GitlabClient) GetMergeRequest(repoFullName string, pullNum int) (*gitla } // MergePull merges the merge request. -func (g *GitlabClient) MergePull(pull models.PullRequest) error { +func (g *GitlabClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { commitMsg := common.AutomergeCommitMsg _, _, err := g.Client.MergeRequests.AcceptMergeRequest( pull.BaseRepo.FullName, pull.Num, &gitlab.AcceptMergeRequestOptions{ - MergeCommitMessage: &commitMsg, + MergeCommitMessage: &commitMsg, + ShouldRemoveSourceBranch: &pullOptions.DeleteSourceBranchOnMerge, }) return errors.Wrap(err, "unable to merge merge request, it may not be in a mergeable state") } diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go index bb0002381c..d4c0b47583 100644 --- a/server/events/vcs/gitlab_client_test.go +++ b/server/events/vcs/gitlab_client_test.go @@ -164,6 +164,8 @@ func TestGitlabClient_MergePull(t *testing.T) { Owner: "runatlantis", Name: "atlantis", }, + }, models.PullRequestOptions{ + DeleteSourceBranchOnMerge: false, }) if c.expErr == "" { Ok(t, err) diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 0b20b49b63..ad7eeef197 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -4,10 +4,11 @@ package mocks import ( - pegomock "github.com/petergtz/pegomock" - models "github.com/runatlantis/atlantis/server/events/models" "reflect" "time" + + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" ) type MockClient struct { @@ -127,11 +128,11 @@ func (mock *MockClient) UpdateStatus(repo models.Repo, pull models.PullRequest, return ret0 } -func (mock *MockClient) MergePull(pull models.PullRequest) error { +func (mock *MockClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - params := []pegomock.Param{pull} + params := []pegomock.Param{pull, pullOptions} result := pegomock.GetGenericMockFrom(mock).Invoke("MergePull", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var ret0 error if len(result) != 0 { @@ -206,14 +207,14 @@ func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient { } } -func (mock *MockClient) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockClient { +func (mock *MockClient) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockClient { return &VerifierMockClient{ mock: mock, invocationCountMatcher: invocationCountMatcher, } } -func (mock *MockClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClient { +func (mock *MockClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClient { return &VerifierMockClient{ mock: mock, invocationCountMatcher: invocationCountMatcher, @@ -221,7 +222,7 @@ func (mock *MockClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.M } } -func (mock *MockClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockClient { +func (mock *MockClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockClient { return &VerifierMockClient{ mock: mock, invocationCountMatcher: invocationCountMatcher, @@ -231,7 +232,7 @@ func (mock *MockClient) VerifyWasCalledEventually(invocationCountMatcher pegomoc type VerifierMockClient struct { mock *MockClient - invocationCountMatcher pegomock.Matcher + invocationCountMatcher pegomock.InvocationCountMatcher inOrderContext *pegomock.InOrderContext timeout time.Duration } @@ -446,8 +447,8 @@ func (c *MockClient_UpdateStatus_OngoingVerification) GetAllCapturedArguments() return } -func (verifier *VerifierMockClient) MergePull(pull models.PullRequest) *MockClient_MergePull_OngoingVerification { - params := []pegomock.Param{pull} +func (verifier *VerifierMockClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) *MockClient_MergePull_OngoingVerification { + params := []pegomock.Param{pull, pullOptions} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MergePull", params, verifier.timeout) return &MockClient_MergePull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -457,18 +458,22 @@ type MockClient_MergePull_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_MergePull_OngoingVerification) GetCapturedArguments() models.PullRequest { - pull := c.GetAllCapturedArguments() - return pull[len(pull)-1] +func (c *MockClient_MergePull_OngoingVerification) GetCapturedArguments() (models.PullRequest, models.PullRequestOptions) { + pull, pullOptions := c.GetAllCapturedArguments() + return pull[len(pull)-1], pullOptions[len(pullOptions)-1] } -func (c *MockClient_MergePull_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) { +func (c *MockClient_MergePull_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest, _param1 []models.PullRequestOptions) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.PullRequest, len(c.methodInvocations)) for u, param := range params[0] { _param0[u] = param.(models.PullRequest) } + _param1 = make([]models.PullRequestOptions, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(models.PullRequestOptions) + } } return } diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 7ce68a3a03..c73a6a22e2 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -44,7 +44,7 @@ func (a *NotConfiguredVCSClient) PullIsMergeable(repo models.Repo, pull models.P func (a *NotConfiguredVCSClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error { return a.err() } -func (a *NotConfiguredVCSClient) MergePull(pull models.PullRequest) error { +func (a *NotConfiguredVCSClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { return a.err() } func (a *NotConfiguredVCSClient) MarkdownPullLink(pull models.PullRequest) (string, error) { diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index a583d70cc0..a49895c0e2 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -76,8 +76,8 @@ func (d *ClientProxy) UpdateStatus(repo models.Repo, pull models.PullRequest, st return d.clients[repo.VCSHost.Type].UpdateStatus(repo, pull, state, src, description, url) } -func (d *ClientProxy) MergePull(pull models.PullRequest) error { - return d.clients[pull.BaseRepo.VCSHost.Type].MergePull(pull) +func (d *ClientProxy) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error { + return d.clients[pull.BaseRepo.VCSHost.Type].MergePull(pull, pullOptions) } func (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) { diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index 21ffe793de..b8c73183f0 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -1035,7 +1035,7 @@ func TestParseGlobalCfg(t *testing.T) { input: `repos: - id: /.*/ allowed_overrides: [invalid]`, - expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\" and \"workflow\" are supported.).).", + expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\", \"workflow\" and \"delete_source_branch_on_merge\" are supported.).).", }, "invalid apply_requirement": { input: `repos: @@ -1118,7 +1118,7 @@ repos: pre_workflow_hooks: - run: custom workflow command workflow: custom1 - allowed_overrides: [apply_requirements, workflow] + allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge] allow_custom_workflows: true - id: /.*/ branch: /(master|main)/ @@ -1157,7 +1157,7 @@ policies: ApplyRequirements: []string{"approved", "mergeable"}, PreWorkflowHooks: preWorkflowHooks, Workflow: &customWorkflow1, - AllowedOverrides: []string{"apply_requirements", "workflow"}, + AllowedOverrides: []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"}, AllowCustomWorkflows: Bool(true), }, { @@ -1253,9 +1253,10 @@ workflows: }, }, }, - AllowedWorkflows: []string{}, - AllowedOverrides: []string{}, - AllowCustomWorkflows: Bool(false), + AllowedWorkflows: []string{}, + AllowedOverrides: []string{}, + AllowCustomWorkflows: Bool(false), + DeleteSourceBranchOnMerge: Bool(false), }, }, Workflows: map[string]valid.Workflow{ diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index f0fefe84e8..c64112e107 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -19,14 +19,15 @@ type GlobalCfg struct { // Repo is the raw schema for repos in the server-side repo config. type Repo struct { - ID string `yaml:"id" json:"id"` - Branch string `yaml:"branch" json:"branch"` - ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` - PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` - Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` - AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` - AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` - AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` + ID string `yaml:"id" json:"id"` + Branch string `yaml:"branch" json:"branch"` + ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` + Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` + AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` + AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` + AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` + DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty" json:"delete_source_branch_on_merge,omitempty"` } func (g GlobalCfg) Validate() error { @@ -163,8 +164,8 @@ func (r Repo) Validate() error { overridesValid := func(value interface{}) error { overrides := value.([]string) for _, o := range overrides { - if o != valid.ApplyRequirementsKey && o != valid.WorkflowKey { - return fmt.Errorf("%q is not a valid override, only %q and %q are supported", o, valid.ApplyRequirementsKey, valid.WorkflowKey) + if o != valid.ApplyRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey { + return fmt.Errorf("%q is not a valid override, only %q, %q and %q are supported", o, valid.ApplyRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey) } } return nil @@ -176,12 +177,18 @@ func (r Repo) Validate() error { return nil } + deleteSourceBranchOnMergeValid := func(value interface{}) error { + //TOBE IMPLEMENTED + return nil + } + return validation.ValidateStruct(&r, validation.Field(&r.ID, validation.Required, validation.By(idValid)), validation.Field(&r.Branch, validation.By(branchValid)), validation.Field(&r.AllowedOverrides, validation.By(overridesValid)), validation.Field(&r.ApplyRequirements, validation.By(validApplyReq)), validation.Field(&r.Workflow, validation.By(workflowExists)), + validation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)), ) } @@ -234,14 +241,15 @@ OUTER: } return valid.Repo{ - ID: id, - IDRegex: idRegex, - BranchRegex: branchRegex, - ApplyRequirements: mergedApplyReqs, - PreWorkflowHooks: preWorkflowHooks, - Workflow: workflow, - AllowedWorkflows: r.AllowedWorkflows, - AllowedOverrides: r.AllowedOverrides, - AllowCustomWorkflows: r.AllowCustomWorkflows, + ID: id, + IDRegex: idRegex, + BranchRegex: branchRegex, + ApplyRequirements: mergedApplyReqs, + PreWorkflowHooks: preWorkflowHooks, + Workflow: workflow, + AllowedWorkflows: r.AllowedWorkflows, + AllowedOverrides: r.AllowedOverrides, + AllowCustomWorkflows: r.AllowCustomWorkflows, + DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge, } } diff --git a/server/events/yaml/raw/project.go b/server/events/yaml/raw/project.go index 8573fcf7ac..14858f1117 100644 --- a/server/events/yaml/raw/project.go +++ b/server/events/yaml/raw/project.go @@ -19,13 +19,14 @@ const ( ) type Project struct { - Name *string `yaml:"name,omitempty"` - Dir *string `yaml:"dir,omitempty"` - Workspace *string `yaml:"workspace,omitempty"` - Workflow *string `yaml:"workflow,omitempty"` - TerraformVersion *string `yaml:"terraform_version,omitempty"` - Autoplan *Autoplan `yaml:"autoplan,omitempty"` - ApplyRequirements []string `yaml:"apply_requirements,omitempty"` + Name *string `yaml:"name,omitempty"` + Dir *string `yaml:"dir,omitempty"` + Workspace *string `yaml:"workspace,omitempty"` + Workflow *string `yaml:"workflow,omitempty"` + TerraformVersion *string `yaml:"terraform_version,omitempty"` + Autoplan *Autoplan `yaml:"autoplan,omitempty"` + ApplyRequirements []string `yaml:"apply_requirements,omitempty"` + DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"` } func (p Project) Validate() error { @@ -86,6 +87,10 @@ func (p Project) ToValid() valid.Project { v.Name = p.Name + if p.DeleteSourceBranchOnMerge != nil { + v.DeleteSourceBranchOnMerge = p.DeleteSourceBranchOnMerge + } + return v } diff --git a/server/events/yaml/raw/repo_cfg.go b/server/events/yaml/raw/repo_cfg.go index c8851e853f..3f90803bd5 100644 --- a/server/events/yaml/raw/repo_cfg.go +++ b/server/events/yaml/raw/repo_cfg.go @@ -19,15 +19,19 @@ const DefaultParallelPlan = false // DefaultParallelPolicyCheck is the default setting for parallel plan const DefaultParallelPolicyCheck = false +// DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge +const DefaultDeleteSourceBranchOnMerge = false + // RepoCfg is the raw schema for repo-level atlantis.yaml config. type RepoCfg struct { - Version *int `yaml:"version,omitempty"` - Projects []Project `yaml:"projects,omitempty"` - Workflows map[string]Workflow `yaml:"workflows,omitempty"` - PolicySets PolicySets `yaml:"policies,omitempty"` - Automerge *bool `yaml:"automerge,omitempty"` - ParallelApply *bool `yaml:"parallel_apply,omitempty"` - ParallelPlan *bool `yaml:"parallel_plan,omitempty"` + Version *int `yaml:"version,omitempty"` + Projects []Project `yaml:"projects,omitempty"` + Workflows map[string]Workflow `yaml:"workflows,omitempty"` + PolicySets PolicySets `yaml:"policies,omitempty"` + Automerge *bool `yaml:"automerge,omitempty"` + ParallelApply *bool `yaml:"parallel_apply,omitempty"` + ParallelPlan *bool `yaml:"parallel_plan,omitempty"` + DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"` } func (r RepoCfg) Validate() error { @@ -75,12 +79,13 @@ func (r RepoCfg) ToValid() valid.RepoCfg { } return valid.RepoCfg{ - Version: *r.Version, - Projects: validProjects, - Workflows: validWorkflows, - Automerge: automerge, - ParallelApply: parallelApply, - ParallelPlan: parallelPlan, - ParallelPolicyCheck: parallelPlan, + Version: *r.Version, + Projects: validProjects, + Workflows: validWorkflows, + Automerge: automerge, + ParallelApply: parallelApply, + ParallelPlan: parallelPlan, + ParallelPolicyCheck: parallelPlan, + DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge, } } diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index f91ac4671d..dc8f1dcc89 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -19,6 +19,7 @@ const AllowedWorkflowsKey = "allowed_workflows" const AllowedOverridesKey = "allowed_overrides" const AllowCustomWorkflowsKey = "allow_custom_workflows" const DefaultWorkflowName = "default" +const DeleteSourceBranchOnMergeKey = "delete_source_branch_on_merge" // NonOverrideableApplyReqs will get applied across all "repos" in the server side config. // If repo config is allowed overrides, they can override this. @@ -41,27 +42,29 @@ type Repo struct { ID string // IDRegex is the regex match for this config. // If ID is set then this will be nil. - IDRegex *regexp.Regexp - BranchRegex *regexp.Regexp - ApplyRequirements []string - PreWorkflowHooks []*PreWorkflowHook - Workflow *Workflow - AllowedWorkflows []string - AllowedOverrides []string - AllowCustomWorkflows *bool + IDRegex *regexp.Regexp + BranchRegex *regexp.Regexp + ApplyRequirements []string + PreWorkflowHooks []*PreWorkflowHook + Workflow *Workflow + AllowedWorkflows []string + AllowedOverrides []string + AllowCustomWorkflows *bool + DeleteSourceBranchOnMerge *bool } type MergedProjectCfg struct { - ApplyRequirements []string - Workflow Workflow - AllowedWorkflows []string - RepoRelDir string - Workspace string - Name string - AutoplanEnabled bool - TerraformVersion *version.Version - RepoCfgVersion int - PolicySets PolicySets + ApplyRequirements []string + Workflow Workflow + AllowedWorkflows []string + RepoRelDir string + Workspace string + Name string + AutoplanEnabled bool + TerraformVersion *version.Version + RepoCfgVersion int + PolicySets PolicySets + DeleteSourceBranchOnMerge bool } // PreWorkflowHook is a map of custom run commands to run before workflows. @@ -160,22 +163,24 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { } allowCustomWorkflows := false + deleteSourceBranchOnMerge := false if args.AllowRepoCfg { - allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey} + allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey} allowCustomWorkflows = true } return GlobalCfg{ Repos: []Repo{ { - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile(".*"), - ApplyRequirements: applyReqs, - PreWorkflowHooks: args.PreWorkflowHooks, - Workflow: &defaultWorkflow, - AllowedWorkflows: allowedWorkflows, - AllowedOverrides: allowedOverrides, - AllowCustomWorkflows: &allowCustomWorkflows, + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + ApplyRequirements: applyReqs, + PreWorkflowHooks: args.PreWorkflowHooks, + Workflow: &defaultWorkflow, + AllowedWorkflows: allowedWorkflows, + AllowedOverrides: allowedOverrides, + AllowCustomWorkflows: &allowCustomWorkflows, + DeleteSourceBranchOnMerge: &deleteSourceBranchOnMerge, }, }, Workflows: map[string]Workflow{ @@ -211,7 +216,8 @@ func (r Repo) IDString() string { // MergeProjectCfg merges proj and rCfg with the global config to return a // final config. It assumes that all configs have been validated. func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg { - applyReqs, workflow, allowedOverrides, allowCustomWorkflows := g.getMatchingCfg(log, repoID) + log.Debug("MergeProjectCfg started") + applyReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID) // If repos are allowed to override certain keys then override them. for _, key := range allowedOverrides { @@ -243,22 +249,38 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro } log.Debug("overriding server-defined %s with repo-specified workflow: %q", WorkflowKey, workflow.Name) } + case DeleteSourceBranchOnMergeKey: + //We check whether the server configured value and repo-root level + //config is different. If it is then we change to the more granular. + if rCfg.DeleteSourceBranchOnMerge != nil && deleteSourceBranchOnMerge != *rCfg.DeleteSourceBranchOnMerge { + log.Debug("overriding server-defined %s with repo settings: [%t]", DeleteSourceBranchOnMergeKey, rCfg.DeleteSourceBranchOnMerge) + deleteSourceBranchOnMerge = *rCfg.DeleteSourceBranchOnMerge + } + //Then we check whether the more granular project based config is + //different. If it is then we set it. + if proj.DeleteSourceBranchOnMerge != nil && deleteSourceBranchOnMerge != *proj.DeleteSourceBranchOnMerge { + log.Debug("overriding repo-root-defined %s with repo settings: [%t]", DeleteSourceBranchOnMergeKey, *proj.DeleteSourceBranchOnMerge) + deleteSourceBranchOnMerge = *proj.DeleteSourceBranchOnMerge + } + log.Debug("merged deleteSourceBranchOnMerge: [%t]", deleteSourceBranchOnMerge) } + log.Debug("MergeProjectCfg completed") } log.Debug("final settings: %s: [%s], %s: %s", ApplyRequirementsKey, strings.Join(applyReqs, ","), WorkflowKey, workflow.Name) return MergedProjectCfg{ - ApplyRequirements: applyReqs, - Workflow: workflow, - RepoRelDir: proj.Dir, - Workspace: proj.Workspace, - Name: proj.GetName(), - AutoplanEnabled: proj.Autoplan.Enabled, - TerraformVersion: proj.TerraformVersion, - RepoCfgVersion: rCfg.Version, - PolicySets: g.PolicySets, + ApplyRequirements: applyReqs, + Workflow: workflow, + RepoRelDir: proj.Dir, + Workspace: proj.Workspace, + Name: proj.GetName(), + AutoplanEnabled: proj.Autoplan.Enabled, + TerraformVersion: proj.TerraformVersion, + RepoCfgVersion: rCfg.Version, + PolicySets: g.PolicySets, + DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, } } @@ -266,16 +288,17 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro // repo with id repoID. It is used when there is no repo config. func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg { log.Debug("building config based on server-side config") - applyReqs, workflow, _, _ := g.getMatchingCfg(log, repoID) + applyReqs, workflow, _, _, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID) return MergedProjectCfg{ - ApplyRequirements: applyReqs, - Workflow: workflow, - RepoRelDir: repoRelDir, - Workspace: workspace, - Name: "", - AutoplanEnabled: DefaultAutoPlanEnabled, - TerraformVersion: nil, - PolicySets: g.PolicySets, + ApplyRequirements: applyReqs, + Workflow: workflow, + RepoRelDir: repoRelDir, + Workspace: workspace, + Name: "", + AutoplanEnabled: DefaultAutoPlanEnabled, + TerraformVersion: nil, + PolicySets: g.PolicySets, + DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, } } @@ -316,6 +339,9 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { if p.ApplyRequirements != nil && !sliceContainsF(allowedOverrides, ApplyRequirementsKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", ApplyRequirementsKey, AllowedOverridesKey, ApplyRequirementsKey) } + if p.DeleteSourceBranchOnMerge != nil && !sliceContainsF(allowedOverrides, DeleteSourceBranchOnMergeKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", DeleteSourceBranchOnMergeKey, AllowedOverridesKey, DeleteSourceBranchOnMergeKey) + } } // Check custom workflows. @@ -374,7 +400,7 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { } // getMatchingCfg returns the key settings for repoID. -func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (applyReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool) { +func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (applyReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool) { toLog := make(map[string]string) traceF := func(repoIdx int, repoID string, key string, val interface{}) string { from := "default server config" @@ -396,7 +422,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, PreWorkflowHooksKey} { + for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { @@ -420,6 +446,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app toLog[AllowCustomWorkflowsKey] = traceF(i, repo.IDString(), AllowCustomWorkflowsKey, *repo.AllowCustomWorkflows) allowCustomWorkflows = *repo.AllowCustomWorkflows } + case DeleteSourceBranchOnMergeKey: + if repo.DeleteSourceBranchOnMerge != nil { + toLog[DeleteSourceBranchOnMergeKey] = traceF(i, repo.IDString(), DeleteSourceBranchOnMergeKey, *repo.DeleteSourceBranchOnMerge) + deleteSourceBranchOnMerge = *repo.DeleteSourceBranchOnMerge + } } } } diff --git a/server/events/yaml/valid/global_cfg_test.go b/server/events/yaml/valid/global_cfg_test.go index 66628de9bb..64807e402f 100644 --- a/server/events/yaml/valid/global_cfg_test.go +++ b/server/events/yaml/valid/global_cfg_test.go @@ -49,13 +49,14 @@ func TestNewGlobalCfg(t *testing.T) { baseCfg := valid.GlobalCfg{ Repos: []valid.Repo{ { - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile(".*"), - ApplyRequirements: []string{}, - Workflow: &expDefaultWorkflow, - AllowedWorkflows: []string{}, - AllowedOverrides: []string{}, - AllowCustomWorkflows: Bool(false), + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + ApplyRequirements: []string{}, + Workflow: &expDefaultWorkflow, + AllowedWorkflows: []string{}, + AllowedOverrides: []string{}, + AllowCustomWorkflows: Bool(false), + DeleteSourceBranchOnMerge: Bool(false), }, }, Workflows: map[string]valid.Workflow{ @@ -113,7 +114,7 @@ func TestNewGlobalCfg(t *testing.T) { if c.allowRepoCfg { exp.Repos[0].AllowCustomWorkflows = Bool(true) - exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "workflow"} + exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"} } if c.mergeableReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "mergeable") diff --git a/server/events/yaml/valid/repo_cfg.go b/server/events/yaml/valid/repo_cfg.go index 6af60141f7..b107c06b41 100644 --- a/server/events/yaml/valid/repo_cfg.go +++ b/server/events/yaml/valid/repo_cfg.go @@ -13,14 +13,15 @@ import ( // RepoCfg is the atlantis.yaml config after it's been parsed and validated. type RepoCfg struct { // Version is the version of the atlantis YAML file. - Version int - Projects []Project - Workflows map[string]Workflow - PolicySets PolicySets - Automerge bool - ParallelApply bool - ParallelPlan bool - ParallelPolicyCheck bool + Version int + Projects []Project + Workflows map[string]Workflow + PolicySets PolicySets + Automerge bool + ParallelApply bool + ParallelPlan bool + ParallelPolicyCheck bool + DeleteSourceBranchOnMerge *bool } func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project { @@ -98,13 +99,14 @@ func (r RepoCfg) ValidateWorkspaceAllowed(repoRelDir string, workspace string) e } type Project struct { - Dir string - Workspace string - Name *string - WorkflowName *string - TerraformVersion *version.Version - Autoplan Autoplan - ApplyRequirements []string + Dir string + Workspace string + Name *string + WorkflowName *string + TerraformVersion *version.Version + Autoplan Autoplan + ApplyRequirements []string + DeleteSourceBranchOnMerge *bool } // GetName returns the name of the project or an empty string if there is no diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index e72829abd9..b6c262329b 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -445,9 +445,9 @@ func TestGitHubWorkflow(t *testing.T) { if c.ExpAutomerge { // Verify that the merge API call was made. - vcsClient.VerifyWasCalledOnce().MergePull(matchers.AnyModelsPullRequest()) + vcsClient.VerifyWasCalledOnce().MergePull(matchers.AnyModelsPullRequest(), matchers.AnyModelsPullRequestOptions()) } else { - vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest()) + vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest(), matchers.AnyModelsPullRequestOptions()) } }) } @@ -617,9 +617,9 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) { if c.ExpAutomerge { // Verify that the merge API call was made. - vcsClient.VerifyWasCalledOnce().MergePull(matchers.AnyModelsPullRequest()) + vcsClient.VerifyWasCalledOnce().MergePull(matchers.AnyModelsPullRequest(), matchers.AnyModelsPullRequestOptions()) } else { - vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest()) + vcsClient.VerifyWasCalled(Never()).MergePull(matchers.AnyModelsPullRequest(), matchers.AnyModelsPullRequestOptions()) } }) }