Skip to content

Commit

Permalink
Implement an automerge feature.
Browse files Browse the repository at this point in the history
Similar to autoplan, automerge is a feature whereby Atlantis will merge
a PR/MR once all plans have successfully been applied.

This addresses issue runatlantis#186.
  • Loading branch information
Brenden Matthews committed Jan 17, 2019
1 parent 56b09b6 commit 88909dd
Show file tree
Hide file tree
Showing 57 changed files with 1,377 additions and 26 deletions.
6 changes: 6 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
TFETokenFlag = "tfe-token"
AutomergeFlag = "automerge"

// Flag defaults.
DefaultCheckoutStrategy = "branch"
Expand Down Expand Up @@ -215,6 +216,11 @@ var boolFlags = []boolFlag{
description: "Silences the posting of whitelist error comments.",
defaultValue: false,
},
{
name: AutomergeFlag,
description: "Automatically merge a pull/merge request when all plans are successfully applied.",
defaultValue: false,
},
}
var intFlags = []intFlag{
{
Expand Down
5 changes: 5 additions & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ func TestExecute_Flags(t *testing.T) {
cmd.SSLCertFileFlag: "cert-file",
cmd.SSLKeyFileFlag: "key-file",
cmd.TFETokenFlag: "my-token",
cmd.AutomergeFlag: true,
})
err := c.Execute()
Ok(t, err)
Expand Down Expand Up @@ -610,6 +611,7 @@ ssl-key-file: my-token
"SSL_CERT_FILE": "override-cert-file",
"SSL_KEY_FILE": "override-key-file",
"TFE_TOKEN": "override-my-token",
"AUTOMERGE": "false",
} {
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
}
Expand Down Expand Up @@ -760,6 +762,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
"SSL_CERT_FILE": "cert-file",
"SSL_KEY_FILE": "key-file",
"TFE_TOKEN": "my-token",
"AUTOMERGE": "true",
}
for name, value := range envVars {
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
Expand Down Expand Up @@ -797,6 +800,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
cmd.SSLCertFileFlag: "override-cert-file",
cmd.SSLKeyFileFlag: "override-key-file",
cmd.TFETokenFlag: "override-my-token",
cmd.AutomergeFlag: true,
})
err := c.Execute()
Ok(t, err)
Expand Down Expand Up @@ -826,6 +830,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
Equals(t, "override-my-token", passedConfig.TFEToken)
Equals(t, true, passedConfig.Automerge)
}

// If using bitbucket cloud, webhook secrets are not supported.
Expand Down
1 change: 1 addition & 0 deletions runatlantis.io/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ module.exports = {
'locking',
'autoplanning',
'checkout-strategy',
'automerging',
'security'
]
}
Expand Down
14 changes: 9 additions & 5 deletions runatlantis.io/docs/atlantis-yaml-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ to use `atlantis.yaml` files.
## Example Using All Keys
```yaml
version: 2
automerge: false
projects:
- name: my-project-name
dir: .
Expand Down Expand Up @@ -69,12 +70,15 @@ It should be noted that `atlantis apply` itself could be exploited if run on a m
version:
projects:
workflows:
automerge:
```
| Key | Type | Default | Required | Description |
| --------- | ---------------------------------------------------------------- | ------- | -------- | ------------------------------------------- |
| version | int | none | yes | This key is required and must be set to `2` |
| projects | array[[Project](atlantis-yaml-reference.html#project)] | [] | no | Lists the projects in this repo |
| workflows | map[string -> [Workflow](atlantis-yaml-reference.html#workflow)] | {} | no | Custom workflows |
| Key | Type | Default | Required | Description |
| --------- | ---------------------------------------------------------------- | ------- | -------- | ----------------------------------------------- |
| version | int | none | yes | This key is required and must be set to `2` |
| projects | array[[Project](atlantis-yaml-reference.html#project)] | [] | no | Lists the projects in this repo |
| workflows | map[string -> [Workflow](atlantis-yaml-reference.html#workflow)] | {} | no | Custom workflows |
| automerge | boolean | false | no | Enable automatic merging after successful apply |


### Project
```yaml
Expand Down
21 changes: 21 additions & 0 deletions runatlantis.io/docs/automerging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Automerging
Atlantis can be configured to automatically merge a PR after all plans have
been successfully applied. Automerging can be enabled either by passing the
`--automerge` flag to the `atlantis server` command, or it can be specified
using `atlantis.yaml` at the top level:

```yaml
version: 2
automerge: true
projects:
- dir: project1
autoplan:
when_modified: ["../modules/**/*.tf", "*.tf*"]
```
The automerge setting is global, and if specified on the command line it will
override any `atlantis.yaml` settings. You may need to adjust the permissions
for your git provider to enable merging via the API.

When automerge is enabled, the changes will only be merged if all plan and
apply stages have succeeded.
48 changes: 48 additions & 0 deletions server/events/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type DefaultCommandRunner struct {
AllowForkPRsFlag string
ProjectCommandBuilder ProjectCommandBuilder
ProjectCommandRunner ProjectCommandRunner
AutomergeOverride bool
}

// RunAutoplanCommand runs plan when a pull request is opened or updated.
Expand Down Expand Up @@ -176,6 +177,53 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
cmd,
CommandResult{
ProjectResults: results})

if cmd.Name == ApplyCommand {
// Lastly, merge the PR if required
c.mergePullIfRequired(ctx, projectCmds, results)
}
}

func (c *DefaultCommandRunner) mergePullIfRequired(ctx *CommandContext, projectCmds []models.ProjectCommandContext, results []ProjectResult) {
if len(projectCmds) < 1 || len(results) != len(projectCmds) {
ctx.Log.Debug("unexpected lengths of projectCmds and results (got %d and %d", len(projectCmds), len(results))
return
}
// Fetch the global config from any command, if it exists (is there a better way to do this?)
if !c.AutomergeOverride && !(projectCmds[0].GlobalConfig != nil && projectCmds[0].GlobalConfig.Automerge) {
ctx.Log.Debug("automerging disabled")
return
}
// Check to be sure all results did not have errors
for _, result := range results {
if result.Error != nil {
// If there was any error, do not merge
ctx.Log.Debug("automerging canceled due to errors")
return
}
}

// Double check that there are no more plans
if c.ProjectCommandRunner.HasErrors(projectCmds[0]) {
ctx.Log.Debug("automerging canceled because one or more projects have errors")
return
}

// Double check that there are no more plans
if c.ProjectCommandRunner.HasPendingPlans(projectCmds[0]) {
ctx.Log.Debug("automerging canceled because there are pending plans")
return
}

ctx.Log.Debug("automerging PR num=%d", ctx.Pull.Num)

// If we made it here, the PR can be merged automatically
mergeResult, err := c.VCSClient.MergePull(ctx.BaseRepo, ctx.Pull)
if err != nil {
ctx.Log.Err("unable to merge pull: %s", err)
} else if !mergeResult {
ctx.Log.Err("unable to merge pull")
}
}

func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContext, cmdName CommandName) []ProjectResult {
Expand Down
4 changes: 2 additions & 2 deletions server/events/mocks/matchers/go_gitlab_mergecommentevent.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/events/mocks/matchers/go_gitlab_mergeevent.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/events/mocks/matchers/ptr_to_go_gitlab_mergerequest.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions server/events/mocks/mock_project_command_runner.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions server/events/pending_plan_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,39 @@ func (p *PendingPlanFinder) Find(pullDir string) ([]PendingPlan, error) {
}
return plans, nil
}

// FindErrors finds all failed init or plans in pullDir. pullDir should be the
// working directory where Atlantis will operate on this pull request. It's one
// level up from where Atlantis clones the repo for each workspace.
func (p *PendingPlanFinder) FindErrors(pullDir string) ([]PendingPlan, error) {
workspaceDirs, err := ioutil.ReadDir(pullDir)
if err != nil {
return nil, err
}
var plans []PendingPlan
for _, workspaceDir := range workspaceDirs {
workspace := workspaceDir.Name()
repoDir := filepath.Join(pullDir, workspace)

// Any generated plans should be untracked by git since Atlantis created
// them.
lsCmd := exec.Command("git", "ls-files", ".", "--others") // nolint: gosec
lsCmd.Dir = repoDir
lsOut, err := lsCmd.CombinedOutput()
if err != nil {
return nil, errors.Wrapf(err, "running git ls-files . "+
"--others: %s", string(lsOut))
}
for _, file := range strings.Split(string(lsOut), "\n") {
if filepath.Ext(file) == ".tfinit-error" || filepath.Ext(file) == ".tfplan-error" {
repoRelDir := filepath.Dir(file)
plans = append(plans, PendingPlan{
RepoDir: repoDir,
RepoRelDir: repoRelDir,
Workspace: workspace,
})
}
}
}
return plans, nil
}
1 change: 1 addition & 0 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ func (p *DefaultProjectCommandBuilder) buildApplyAllCommands(ctx *CommandContext
}
cmds = append(cmds, cmd)
}

return cmds, nil
}

Expand Down
Loading

0 comments on commit 88909dd

Please sign in to comment.