Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an automerge feature. #389

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the service account used to post comments etc. require write permissions for this to work?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The account would indeed require write permissions. Is there something in the doc which contradicts that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing that contradicts it, but it doesn't mention that requirement explicitly anywhere :)

Copy link
Author

@brndnmtthws brndnmtthws Jan 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required for Atlantis to work correctly (I think?), so I don't see why it would be explicitly called out in this section of the doc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, cool. Nvm then 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually does require increased permissions depending on your setup. For example, my user token can comment on public pull requests but it can't merge unless I'm a collaborator.

The token needs to be from a user with those permissions.

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