Skip to content

Commit

Permalink
Add automerge feature.
Browse files Browse the repository at this point in the history
Automerging merges pull requests automatically if all plans have been
successfully applied.

* Save status of PR's to BoltDB so after each apply, we can check if
there are pending plans.
* Add new feature where we delete successful plans *unless* all plans
have succeeded *if* automerge is enabled. This was requested by users
because when automerge is enabled, they want to enforce that a pull
request's changes have been fully applied. They asked that plans not be
allowed to be applied "piecemeal" and instead, all plans must be
generated successfully prior to allowing any plans to be applied.
  • Loading branch information
lkysow committed Feb 6, 2019
1 parent 2225da9 commit 74e9bbb
Show file tree
Hide file tree
Showing 64 changed files with 2,452 additions and 590 deletions.
7 changes: 4 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ jobs:
# We use dockerize -wait here to wait until the server is up.
- run: |
dockerize -wait tcp://localhost:8080 -- \
muffet -e 'https://github\\.com/runatlantis/atlantis/edit/master/.*' \
-e 'https://github.com/helm/charts/tree/master/stable/atlantis#customization' \
http://localhost:8080/
muffet \
-e 'https://github\.com/runatlantis/atlantis/edit/master/.*' \
-e 'https://github.com/helm/charts/tree/master/stable/atlantis#customization' \
http://localhost:8080/
# Build and push 'latest' Docker tag.
docker_master:
Expand Down
6 changes: 6 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
AllowForkPRsFlag = "allow-fork-prs"
AllowRepoConfigFlag = "allow-repo-config"
AtlantisURLFlag = "atlantis-url"
AutomergeFlag = "automerge"
BitbucketBaseURLFlag = "bitbucket-base-url"
BitbucketTokenFlag = "bitbucket-token"
BitbucketUserFlag = "bitbucket-user"
Expand Down Expand Up @@ -205,6 +206,11 @@ var boolFlags = []boolFlag{
" on the Atlantis server.",
defaultValue: false,
},
{
name: AutomergeFlag,
description: "Automatically merge pull requests when all plans are successfully applied.",
defaultValue: false,
},
{
name: RequireApprovalFlag,
description: "Require pull requests to be \"Approved\" before allowing the apply command to be run.",
Expand Down
14 changes: 14 additions & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ func TestExecute_Defaults(t *testing.T) {
Equals(t, "http://"+hostname+":4141", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.AllowRepoConfig)
Equals(t, false, passedConfig.Automerge)

// Get our home dir since that's what gets defaulted to
dataDir, err := homedir.Expand("~/.atlantis")
Expand Down Expand Up @@ -438,6 +439,7 @@ func TestExecute_Flags(t *testing.T) {
cmd.AtlantisURLFlag: "url",
cmd.AllowForkPRsFlag: true,
cmd.AllowRepoConfigFlag: true,
cmd.AutomergeFlag: true,
cmd.BitbucketBaseURLFlag: "https://bitbucket-base-url.com",
cmd.BitbucketTokenFlag: "bitbucket-token",
cmd.BitbucketUserFlag: "bitbucket-user",
Expand Down Expand Up @@ -468,6 +470,7 @@ func TestExecute_Flags(t *testing.T) {
Equals(t, "url", passedConfig.AtlantisURL)
Equals(t, true, passedConfig.AllowForkPRs)
Equals(t, true, passedConfig.AllowRepoConfig)
Equals(t, true, passedConfig.Automerge)
Equals(t, "https://bitbucket-base-url.com", passedConfig.BitbucketBaseURL)
Equals(t, "bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "bitbucket-user", passedConfig.BitbucketUser)
Expand Down Expand Up @@ -499,6 +502,7 @@ func TestExecute_ConfigFile(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
automerge: true
bitbucket-base-url: "https://mydomain.com"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
Expand Down Expand Up @@ -533,6 +537,7 @@ tfe-token: my-token
Equals(t, "url", passedConfig.AtlantisURL)
Equals(t, true, passedConfig.AllowForkPRs)
Equals(t, true, passedConfig.AllowRepoConfig)
Equals(t, true, passedConfig.Automerge)
Equals(t, "https://mydomain.com", passedConfig.BitbucketBaseURL)
Equals(t, "bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "bitbucket-user", passedConfig.BitbucketUser)
Expand Down Expand Up @@ -564,6 +569,7 @@ func TestExecute_EnvironmentOverride(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
automerge: true
bitbucket-base-url: "https://mydomain.com"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
Expand Down Expand Up @@ -594,6 +600,7 @@ tfe-token: my-token
"ATLANTIS_URL": "override-url",
"ALLOW_FORK_PRS": "false",
"ALLOW_REPO_CONFIG": "false",
"AUTOMERGE": "false",
"BITBUCKET_BASE_URL": "https://override-bitbucket-base-url",
"BITBUCKET_TOKEN": "override-bitbucket-token",
"BITBUCKET_USER": "override-bitbucket-user",
Expand Down Expand Up @@ -628,6 +635,7 @@ tfe-token: my-token
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.AllowRepoConfig)
Equals(t, false, passedConfig.Automerge)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
Expand Down Expand Up @@ -659,6 +667,7 @@ func TestExecute_FlagConfigOverride(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
automerge: true
bitbucket-base-url: "https://bitbucket-base-url"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
Expand Down Expand Up @@ -689,6 +698,7 @@ tfe-token: my-token
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.AutomergeFlag: false,
cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url",
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
Expand Down Expand Up @@ -717,6 +727,7 @@ tfe-token: my-token
Ok(t, err)
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.Automerge)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
Expand Down Expand Up @@ -750,6 +761,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
"ATLANTIS_URL": "url",
"ALLOW_FORK_PRS": "true",
"ALLOW_REPO_CONFIG": "true",
"AUTOMERGE": "true",
"BITBUCKET_BASE_URL": "https://bitbucket-base-url",
"BITBUCKET_TOKEN": "bitbucket-token",
"BITBUCKET_USER": "bitbucket-user",
Expand Down Expand Up @@ -788,6 +800,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.AutomergeFlag: false,
cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url",
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
Expand Down Expand Up @@ -818,6 +831,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.AllowRepoConfig)
Equals(t, false, passedConfig.Automerge)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
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 @@ -81,6 +81,7 @@ module.exports = {
['how-atlantis-works', 'Overview'],
'locking',
'autoplanning',
'automerging',
'checkout-strategy',
'security'
]
Expand Down
13 changes: 8 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: true
projects:
- name: my-project-name
dir: .
Expand Down Expand Up @@ -67,14 +68,16 @@ It should be noted that `atlantis apply` itself could be exploited if run on a m
### Top-Level Keys
```yaml
version:
automerge:
projects:
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 |
| Key | Type | Default | Required | Description |
| --------- | ---------------------------------------------------------------- | ------- | -------- | ----------------------------------------------------------- |
| version | int | none | yes | This key is required and must be set to `2` |
| automerge | bool | false | no | Automatically merge pull request when all plans are applied |
| 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 |

### Project
```yaml
Expand Down
44 changes: 44 additions & 0 deletions runatlantis.io/docs/automerging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Automerging
Atlantis can be configured to automatically merge a pull request after all plans have
been successfully applied.


![Automerge](./images/automerge.png)

## How To Enable
Automerging can be enabled either by:
1. Passing the `--automerge` flag to `atlantis server`. This will cause all
pull requests to be automerged and any repo config will be ignored.
1. Setting `automerge: true` in the repo's `atlantis.yaml` file:
```yaml
version: 2
automerge: true
projects:
- dir: .
```
:::tip NOTE
If a repo has an `atlantis.yaml` file, then each project in the repo needs
to be configured under the `projects` key.
:::

## All Plans Must Succeed
When automerge is enabled, **all plans** in a pull request **must succeed** before
**any** plans can be applied.

For example, imagine this scenario:
1. I open a pull request that makes changes to two Terraform projects, in `dir1/`
and `dir2/`.
1. The plan for `dir2/` fails because my Terraform syntax is wrong.

In this scenario, I can't run
```
atlantis apply -d dir1
```
Even though that plan succeeded, because **all** plans must succeed for **any** plans
to be saved.
Once I fix the issue in `dir2`, I can push a new commit which will trigger an
autoplan. Then I will be able to apply both plans.
## Permissions
The Atlantis VCS user must have the ability to merge pull requests.
Binary file added runatlantis.io/docs/images/automerge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 21 additions & 1 deletion server/events/command_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,29 @@

package events

import "github.com/runatlantis/atlantis/server/events/models"

// CommandResult is the result of running a Command.
type CommandResult struct {
Error error
Failure string
ProjectResults []ProjectResult
ProjectResults []models.ProjectResult
// PlansDeleted is true if all plans created during this command were
// deleted. This happens if automerging is enabled and one project has an
// error since automerging requires all plans to succeed.
PlansDeleted bool
}

// HasErrors returns true if there were any errors during the execution,
// even if it was only in one project.
func (c CommandResult) HasErrors() bool {
if c.Error != nil || c.Failure != "" {
return true
}
for _, r := range c.ProjectResults {
if !r.IsSuccessful() {
return true
}
}
return false
}
108 changes: 108 additions & 0 deletions server/events/command_result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package events_test

import (
"errors"
"testing"

"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/models"
. "github.com/runatlantis/atlantis/testing"
)

func TestCommandResult_HasErrors(t *testing.T) {
cases := map[string]struct {
cr events.CommandResult
exp bool
}{
"error": {
cr: events.CommandResult{
Error: errors.New("err"),
},
exp: true,
},
"failure": {
cr: events.CommandResult{
Failure: "failure",
},
exp: true,
},
"empty results list": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{},
},
exp: false,
},
"successful plan": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
PlanSuccess: &models.PlanSuccess{},
},
},
},
exp: false,
},
"successful apply": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
ApplySuccess: "success",
},
},
},
exp: false,
},
"single errored project": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
Error: errors.New("err"),
},
},
},
exp: true,
},
"single failed project": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
Failure: "failure",
},
},
},
exp: true,
},
"two successful projects": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
PlanSuccess: &models.PlanSuccess{},
},
{
ApplySuccess: "success",
},
},
},
exp: false,
},
"one successful, one failed project": {
cr: events.CommandResult{
ProjectResults: []models.ProjectResult{
{
PlanSuccess: &models.PlanSuccess{},
},
{
Failure: "failed",
},
},
},
exp: true,
},
}

for descrip, c := range cases {
t.Run(descrip, func(t *testing.T) {
Equals(t, c.exp, c.cr.HasErrors())
})
}
}
Loading

0 comments on commit 74e9bbb

Please sign in to comment.