Skip to content

Commit

Permalink
feat(automerge): implement GitHub --auto-merge-method flag for apply …
Browse files Browse the repository at this point in the history
…command (runatlantis#4895)

Signed-off-by: a1k0u <alko7enko@gmail.com>
Signed-off-by: X-Guardian <simonheather99@gmail.com>
  • Loading branch information
a1k0u committed Nov 6, 2024
1 parent 0a8551a commit e95c2d3
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 55 deletions.
13 changes: 8 additions & 5 deletions runatlantis.io/docs/automerging.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,23 @@ Automerging can be enabled either by:
If automerge is enabled, you can disable it for a single `atlantis apply`
command with the `--auto-merge-disabled` option.

## How to set merge method for automerge
## How to set the merge method for automerge

If automerge is enabled, you can use `--merge-method` option
for `atlantis apply` command to specify which merge method use.
If automerge is enabled, you can use the `--auto-merge-method` option
for the `atlantis apply` command to specify which merge method use.

```shell
atlantis apply --merge-method squash
atlantis apply --auto-merge-method <method>
```

Implemented only for GitHub. You can choose one of them:
The `method` must be one of:

- merge
- rebase
- squash

This is currently only implemented for the GitHub VCS.

## Requirements

### All Plans Must Succeed
Expand Down
2 changes: 1 addition & 1 deletion runatlantis.io/docs/using-atlantis.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ atlantis apply -w staging
* `-p project` Apply the plan for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.md). Cannot be used at same time as `-d` or `-w`.
* `-w workspace` Apply the plan for this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused.
* `--auto-merge-disabled` Disable [automerge](automerging.md) for this apply command.
* `--merge-method method` Specify which [merge method](automerging.md#how-to-set-merge-method-for-automerge) use for apply command if [automerge](automerging.md) is enabled. Implemented only for GitHub.
* `--auto-merge-method method` Specify which [merge method](automerging.md#how-to-set-merge-method-for-automerge) use for the apply command if [automerge](automerging.md) is enabled. Implemented only for GitHub.
* `--verbose` Append Atlantis log to comment.

### Additional Terraform flags
Expand Down
2 changes: 1 addition & 1 deletion server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ type MergedProjectCfg struct {
Name string
AutoplanEnabled bool
AutoMergeDisabled bool
MergeMethod string
AutoMergeMethod string
TerraformVersion *version.Version
RepoCfgVersion int
PolicySets PolicySets
Expand Down
2 changes: 1 addition & 1 deletion server/events/apply_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
a.updateCommitStatus(ctx, pullStatus)

if a.autoMerger.automergeEnabled(projectCmds) && !cmd.AutoMergeDisabled {
a.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds), cmd.MergeMethod)
a.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds), cmd.AutoMergeMethod)
}
}

Expand Down
28 changes: 14 additions & 14 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const (
policySetFlagShort = ""
autoMergeDisabledFlagLong = "auto-merge-disabled"
autoMergeDisabledFlagShort = ""
mergeMethodFlagLong = "merge-method"
mergeMethodFlagShort = ""
autoMergeMethodFlagLong = "auto-merge-method"
autoMergeMethodFlagShort = ""
verboseFlagLong = "verbose"
verboseFlagShort = ""
clearPolicyApprovalFlagLong = "clear-policy-approval"
Expand Down Expand Up @@ -72,7 +72,7 @@ type CommentBuilder interface {
// BuildPlanComment builds a plan comment for the specified args.
BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string
// BuildApplyComment builds an apply comment for the specified args.
BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, mergeMethod string) string
BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string
// BuildApprovePoliciesComment builds an approve_policies comment for the specified args.
BuildApprovePoliciesComment(repoRelDir string, workspace string, project string) string
}
Expand Down Expand Up @@ -230,7 +230,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
var clearPolicyApproval bool
var verbose bool
var autoMergeDisabled bool
var mergeMethod string
var autoMergeMethod string
var flagSet *pflag.FlagSet
var name command.Name

Expand All @@ -252,7 +252,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", "Apply the plan for this project. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.")
flagSet.BoolVarP(&autoMergeDisabled, autoMergeDisabledFlagLong, autoMergeDisabledFlagShort, false, "Disable automerge after apply.")
flagSet.StringVarP(&mergeMethod, mergeMethodFlagLong, mergeMethodFlagShort, "", "Specifies merge method for the VCS if automerge is enabled.")
flagSet.StringVarP(&autoMergeMethod, autoMergeMethodFlagLong, autoMergeMethodFlagShort, "", "Specifies the merge method for the VCS if automerge is enabled. (Currently only implemented for GitHub)")
flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.")
case command.ApprovePolicies.String():
name = command.ApprovePolicies
Expand Down Expand Up @@ -322,20 +322,20 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
return CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}
}

if mergeMethod != "" {
if autoMergeMethod != "" {
if autoMergeDisabled {
err := fmt.Sprintf("cannot use --%s at same time with --%s", mergeMethodFlagLong, autoMergeDisabledFlagLong)
err := fmt.Sprintf("cannot use --%s at the same time as --%s", autoMergeMethodFlagLong, autoMergeDisabledFlagLong)
return CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}
}

if vcsHost != models.Github {
err := fmt.Sprintf("--%s not implemeted for %s", mergeMethodFlagLong, vcsHost.String())
err := fmt.Sprintf("--%s is not currently implemented for %s", autoMergeMethodFlagLong, vcsHost.String())
return CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}
}
}

return CommentParseResult{
Command: NewCommentCommand(dir, extraArgs, name, subName, verbose, autoMergeDisabled, mergeMethod, workspace, project, policySet, clearPolicyApproval),
Command: NewCommentCommand(dir, extraArgs, name, subName, verbose, autoMergeDisabled, autoMergeMethod, workspace, project, policySet, clearPolicyApproval),
}
}

Expand Down Expand Up @@ -419,8 +419,8 @@ func (e *CommentParser) BuildPlanComment(repoRelDir string, workspace string, pr
}

// BuildApplyComment builds an apply comment for the specified args.
func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, mergeMethod string) string {
flags := e.buildFlags(repoRelDir, workspace, project, autoMergeDisabled, mergeMethod)
func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string {
flags := e.buildFlags(repoRelDir, workspace, project, autoMergeDisabled, autoMergeMethod)
return fmt.Sprintf("%s %s%s", e.ExecutableName, command.Apply.String(), flags)
}

Expand All @@ -430,7 +430,7 @@ func (e *CommentParser) BuildApprovePoliciesComment(repoRelDir string, workspace
return fmt.Sprintf("%s %s%s", e.ExecutableName, command.ApprovePolicies.String(), flags)
}

func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool, mergeMethod string) string {
func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string {
// Add quotes if dir has spaces.
if strings.Contains(repoRelDir, " ") {
repoRelDir = fmt.Sprintf("%q", repoRelDir)
Expand Down Expand Up @@ -458,8 +458,8 @@ func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project
if autoMergeDisabled {
flags = fmt.Sprintf("%s --%s", flags, autoMergeDisabledFlagLong)
}
if mergeMethod != "" {
flags = fmt.Sprintf("%s --%s %s", flags, mergeMethodFlagLong, mergeMethod)
if autoMergeMethod != "" {
flags = fmt.Sprintf("%s --%s %s", flags, autoMergeMethodFlagLong, autoMergeMethod)
}
return flags
}
Expand Down
29 changes: 16 additions & 13 deletions server/events/comment_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ func TestBuildPlanApplyVersionComment(t *testing.T) {
workspace string
project string
autoMergeDisabled bool
mergeMethod string
autoMergeMethod string
commentArgs []string
expPlanFlags string
expApplyFlags string
Expand Down Expand Up @@ -829,10 +829,10 @@ func TestBuildPlanApplyVersionComment(t *testing.T) {
repoRelDir: "dir",
workspace: "workspace",
project: "",
mergeMethod: "squash",
autoMergeMethod: "squash",
commentArgs: []string{`"arg1"`, `"arg2"`, `arg3`},
expPlanFlags: "-d dir -w workspace -- arg1 arg2 arg3",
expApplyFlags: "-d dir -w workspace --merge-method squash",
expApplyFlags: "-d dir -w workspace --auto-merge-method squash",
expVersionFlags: "-d dir -w workspace",
},
}
Expand All @@ -845,7 +845,7 @@ func TestBuildPlanApplyVersionComment(t *testing.T) {
actComment := commentParser.BuildPlanComment(c.repoRelDir, c.workspace, c.project, c.commentArgs)
Equals(t, fmt.Sprintf("atlantis plan %s", c.expPlanFlags), actComment)
case command.Apply:
actComment := commentParser.BuildApplyComment(c.repoRelDir, c.workspace, c.project, c.autoMergeDisabled, c.mergeMethod)
actComment := commentParser.BuildApplyComment(c.repoRelDir, c.workspace, c.project, c.autoMergeDisabled, c.autoMergeMethod)
Equals(t, fmt.Sprintf("atlantis apply %s", c.expApplyFlags), actComment)
}
}
Expand Down Expand Up @@ -1031,15 +1031,18 @@ var PlanUsage = `Usage of plan:
`

var ApplyUsage = `Usage of apply:
--auto-merge-disabled Disable automerge after apply.
-d, --dir string Apply the plan for this directory, relative to root of
repo, ex. 'child/dir'.
--merge-method string Specifies merge method for the VCS if automerge is enabled.
-p, --project string Apply the plan for this project. Refers to the name of
the project configured in a repo config file. Cannot
be used at same time as workspace or dir flags.
--verbose Append Atlantis log to comment.
-w, --workspace string Apply the plan for this Terraform workspace.
--auto-merge-disabled Disable automerge after apply.
--auto-merge-method string Specifies the merge method for the VCS if
automerge is enabled. (Currently only implemented
for GitHub)
-d, --dir string Apply the plan for this directory, relative to
root of repo, ex. 'child/dir'.
-p, --project string Apply the plan for this project. Refers to the
name of the project configured in a repo config
file. Cannot be used at same time as workspace or
dir flags.
--verbose Append Atlantis log to comment.
-w, --workspace string Apply the plan for this Terraform workspace.
`

var ApprovePolicyUsage = `Usage of approve_policies:
Expand Down
10 changes: 5 additions & 5 deletions server/events/event_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ type CommentCommand struct {
SubName string
// AutoMergeDisabled is true if the command should not automerge after apply.
AutoMergeDisabled bool
// MergeMethod specified the merge method for the VCS if automerge enabled.
MergeMethod string
// AutoMergeMethod specified the merge method for the VCS if automerge enabled.
AutoMergeMethod string
// Verbose is true if the command should output verbosely.
Verbose bool
// Workspace is the name of the Terraform workspace to run the command in.
Expand Down Expand Up @@ -179,11 +179,11 @@ func (c CommentCommand) IsAutoplan() bool {

// String returns a string representation of the command.
func (c CommentCommand) String() string {
return fmt.Sprintf("command=%q, verbose=%t, dir=%q, workspace=%q, project=%q, policyset=%q, auto-merge-disabled=%t, merge-method=%s, clear-policy-approval=%t, flags=%q", c.Name.String(), c.Verbose, c.RepoRelDir, c.Workspace, c.ProjectName, c.PolicySet, c.AutoMergeDisabled, c.MergeMethod, c.ClearPolicyApproval, strings.Join(c.Flags, ","))
return fmt.Sprintf("command=%q, verbose=%t, dir=%q, workspace=%q, project=%q, policyset=%q, auto-merge-disabled=%t, auto-merge-method=%s, clear-policy-approval=%t, flags=%q", c.Name.String(), c.Verbose, c.RepoRelDir, c.Workspace, c.ProjectName, c.PolicySet, c.AutoMergeDisabled, c.AutoMergeMethod, c.ClearPolicyApproval, strings.Join(c.Flags, ","))
}

// NewCommentCommand constructs a CommentCommand, setting all missing fields to defaults.
func NewCommentCommand(repoRelDir string, flags []string, name command.Name, subName string, verbose, autoMergeDisabled bool, mergeMethod string, workspace string, project string, policySet string, clearPolicyApproval bool) *CommentCommand {
func NewCommentCommand(repoRelDir string, flags []string, name command.Name, subName string, verbose, autoMergeDisabled bool, autoMergeMethod string, workspace string, project string, policySet string, clearPolicyApproval bool) *CommentCommand {
// If repoRelDir was empty we want to keep it that way to indicate that it
// wasn't specified in the comment.
if repoRelDir != "" {
Expand All @@ -200,7 +200,7 @@ func NewCommentCommand(repoRelDir string, flags []string, name command.Name, sub
Verbose: verbose,
Workspace: workspace,
AutoMergeDisabled: autoMergeDisabled,
MergeMethod: mergeMethod,
AutoMergeMethod: autoMergeMethod,
ProjectName: project,
PolicySet: policySet,
ClearPolicyApproval: clearPolicyApproval,
Expand Down
2 changes: 1 addition & 1 deletion server/events/event_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ func TestCommentCommand_IsAutoplan(t *testing.T) {
}

func TestCommentCommand_String(t *testing.T) {
exp := `command="plan", verbose=true, dir="mydir", workspace="myworkspace", project="myproject", policyset="", auto-merge-disabled=false, merge-method=, clear-policy-approval=false, flags="flag1,flag2"`
exp := `command="plan", verbose=true, dir="mydir", workspace="myworkspace", project="myproject", policyset="", auto-merge-disabled=false, auto-merge-method=, clear-policy-approval=false, flags="flag1,flag2"`
Equals(t, exp, (events.CommentCommand{
RepoRelDir: "mydir",
Flags: []string{"flag1", "flag2"},
Expand Down
4 changes: 2 additions & 2 deletions server/events/project_command_context_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
projectCmdContext := newProjectCommandContext(
ctx,
cmdName,
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.MergeMethod),
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.AutoMergeMethod),
cb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),
cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),
prjCfg,
Expand Down Expand Up @@ -202,7 +202,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
projectCmds = append(projectCmds, newProjectCommandContext(
ctx,
command.PolicyCheck,
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.MergeMethod),
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.AutoMergeMethod),
cb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),
cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),
prjCfg,
Expand Down
12 changes: 8 additions & 4 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"maps"
"net/http"
"slices"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -649,23 +650,26 @@ func (g *GithubClient) MergePull(logger logging.SimpleLogging, pull models.PullR
squashMergeMethod = "squash"
)

mergeMethods := map[string]func() bool{
mergeMethodsAllow := map[string]func() bool{
defaultMergeMethod: repo.GetAllowMergeCommit,
rebaseMergeMethod: repo.GetAllowRebaseMerge,
squashMergeMethod: repo.GetAllowSquashMerge,
}

mergeMethodsName := slices.Collect(maps.Keys(mergeMethodsAllow))
sort.Strings(mergeMethodsName)

var method string
if pullOptions.MergeMethod != "" {
method = pullOptions.MergeMethod

isMethodAllowed, isMethodExist := mergeMethods[method]
isMethodAllowed, isMethodExist := mergeMethodsAllow[method]
if !isMethodExist {
return fmt.Errorf("%s method is unknown for GitHub, use one of them: %v", method, slices.Collect(maps.Keys(mergeMethods)))
return fmt.Errorf("Merge method '%s' is unknown. Specify one of the valid values: '%s'", method, strings.Join(mergeMethodsName, ", "))
}

if !isMethodAllowed() {
return fmt.Errorf("%s method is not allowed by repository settings", method)
return fmt.Errorf("Merge method '%s' is not allowed by the repository Pull Request settings", method)
}
} else {
method = defaultMergeMethod
Expand Down
16 changes: 8 additions & 8 deletions server/events/vcs/github_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,37 +1059,37 @@ func TestGithubClient_MergePullCorrectMethod(t *testing.T) {
mergeMethodOption: "squash",
expMethod: "squash",
},
"merge with merge: overrided by command: merge not allowed": {
"merge with merge: overridden by command: merge not allowed": {
allowMerge: false,
allowRebase: true,
allowSquash: true,
mergeMethodOption: "merge",
expMethod: "",
expErr: "merge method is not allowed by repository settings",
expErr: "Merge method 'merge' is not allowed by the repository Pull Request settings",
},
"merge with rebase: overrided by command: rebase not allowed": {
"merge with rebase: overridden by command: rebase not allowed": {
allowMerge: true,
allowRebase: false,
allowSquash: true,
mergeMethodOption: "rebase",
expMethod: "",
expErr: "rebase method is not allowed by repository settings",
expErr: "Merge method 'rebase' is not allowed by the repository Pull Request settings",
},
"merge with squash: overrided by command: squash not allowed": {
"merge with squash: overridden by command: squash not allowed": {
allowMerge: true,
allowRebase: true,
allowSquash: false,
mergeMethodOption: "squash",
expMethod: "",
expErr: "squash method is not allowed by repository settings",
expErr: "Merge method 'squash' is not allowed by the repository Pull Request settings",
},
"merge with unknown: overrided by command: unknown not exists": {
"merge with unknown: overridden by command: unknown doesn't exist": {
allowMerge: true,
allowRebase: true,
allowSquash: true,
mergeMethodOption: "unknown",
expMethod: "",
expErr: "unknown method is unknown for GitHub, use one of them: [merge rebase squash]",
expErr: "Merge method 'unknown' is unknown. Specify one of the valid values: 'merge, rebase, squash'",
},
}

Expand Down

0 comments on commit e95c2d3

Please sign in to comment.