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

feat(automerge): implement GitHub --auto-merge-method flag for apply command #4895

Merged
merged 6 commits into from
Nov 2, 2024
Merged
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
17 changes: 17 additions & 0 deletions runatlantis.io/docs/automerging.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +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 the merge method for automerge

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 --auto-merge-method <method>
```

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
1 change: 1 addition & 0 deletions runatlantis.io/docs/using-atlantis.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +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.
* `--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
1 change: 1 addition & 0 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type MergedProjectCfg struct {
Name string
AutoplanEnabled bool
AutoMergeDisabled bool
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))
a.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds), cmd.AutoMergeMethod)
}
}

Expand Down
3 changes: 2 additions & 1 deletion server/events/automerger.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type AutoMerger struct {
GlobalAutomerge bool
}

func (c *AutoMerger) automerge(ctx *command.Context, pullStatus models.PullStatus, deleteSourceBranchOnMerge bool) {
func (c *AutoMerger) automerge(ctx *command.Context, pullStatus models.PullStatus, deleteSourceBranchOnMerge bool, mergeMethod string) {
// We only automerge if all projects have been successfully applied.
for _, p := range pullStatus.Projects {
if p.Status != models.AppliedPlanStatus {
Expand All @@ -32,6 +32,7 @@ func (c *AutoMerger) automerge(ctx *command.Context, pullStatus models.PullStatu
ctx.Log.Info("automerging pull request")
var pullOptions models.PullRequestOptions
pullOptions.DeleteSourceBranchOnMerge = deleteSourceBranchOnMerge
pullOptions.MergeMethod = mergeMethod
err := c.VCSClient.MergePull(ctx.Log, ctx.Pull, pullOptions)

if err != nil {
Expand Down
36 changes: 28 additions & 8 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
policySetFlagShort = ""
autoMergeDisabledFlagLong = "auto-merge-disabled"
autoMergeDisabledFlagShort = ""
autoMergeMethodFlagLong = "auto-merge-method"
autoMergeMethodFlagShort = ""
verboseFlagLong = "verbose"
verboseFlagShort = ""
clearPolicyApprovalFlagLong = "clear-policy-approval"
Expand Down Expand Up @@ -70,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) 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 @@ -226,7 +228,9 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
var project string
var policySet string
var clearPolicyApproval bool
var verbose, autoMergeDisabled bool
var verbose bool
var autoMergeDisabled bool
var autoMergeMethod string
var flagSet *pflag.FlagSet
var name command.Name

Expand All @@ -248,6 +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(&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 @@ -317,8 +322,20 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
return CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}
}

if autoMergeMethod != "" {
if autoMergeDisabled {
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 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, workspace, project, policySet, clearPolicyApproval),
Command: NewCommentCommand(dir, extraArgs, name, subName, verbose, autoMergeDisabled, autoMergeMethod, workspace, project, policySet, clearPolicyApproval),
}
}

Expand Down Expand Up @@ -387,7 +404,7 @@ func (e *CommentParser) parseArgs(name command.Name, args []string, flagSet *pfl

// BuildPlanComment builds a plan comment for the specified args.
func (e *CommentParser) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string {
flags := e.buildFlags(repoRelDir, workspace, project, false)
flags := e.buildFlags(repoRelDir, workspace, project, false, "")
commentFlags := ""
if len(commentArgs) > 0 {
var flagsWithoutQuotes []string
Expand All @@ -402,18 +419,18 @@ 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) string {
flags := e.buildFlags(repoRelDir, workspace, project, autoMergeDisabled)
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)
}

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

func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool) 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 @@ -441,6 +458,9 @@ func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project
if autoMergeDisabled {
flags = fmt.Sprintf("%s --%s", flags, autoMergeDisabledFlagLong)
}
if autoMergeMethod != "" {
flags = fmt.Sprintf("%s --%s %s", flags, autoMergeMethodFlagLong, autoMergeMethod)
}
return flags
}

Expand Down
33 changes: 24 additions & 9 deletions server/events/comment_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ func TestBuildPlanApplyVersionComment(t *testing.T) {
workspace string
project string
autoMergeDisabled bool
autoMergeMethod string
commentArgs []string
expPlanFlags string
expApplyFlags string
Expand Down Expand Up @@ -824,6 +825,16 @@ func TestBuildPlanApplyVersionComment(t *testing.T) {
expApplyFlags: "-d dir -w workspace --auto-merge-disabled",
expVersionFlags: "-d dir -w workspace",
},
{
repoRelDir: "dir",
workspace: "workspace",
project: "",
autoMergeMethod: "squash",
commentArgs: []string{`"arg1"`, `"arg2"`, `arg3`},
expPlanFlags: "-d dir -w workspace -- arg1 arg2 arg3",
expApplyFlags: "-d dir -w workspace --auto-merge-method squash",
expVersionFlags: "-d dir -w workspace",
},
}

for _, c := range cases {
Expand All @@ -834,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)
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 @@ -1020,14 +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'.
-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
7 changes: 5 additions & 2 deletions server/events/event_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ type CommentCommand struct {
SubName string
// AutoMergeDisabled is true if the command should not automerge after apply.
AutoMergeDisabled bool
// 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 @@ -177,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, clear-policy-approval=%t, flags=%q", c.Name.String(), c.Verbose, c.RepoRelDir, c.Workspace, c.ProjectName, c.PolicySet, 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, 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 @@ -198,6 +200,7 @@ func NewCommentCommand(repoRelDir string, flags []string, name command.Name, sub
Verbose: verbose,
Workspace: workspace,
AutoMergeDisabled: autoMergeDisabled,
AutoMergeMethod: autoMergeMethod,
ProjectName: project,
PolicySet: policySet,
ClearPolicyApproval: clearPolicyApproval,
Expand Down
8 changes: 4 additions & 4 deletions server/events/event_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,14 +750,14 @@ func TestNewCommand_CleansDir(t *testing.T) {

for _, c := range cases {
t.Run(c.RepoRelDir, func(t *testing.T) {
cmd := events.NewCommentCommand(c.RepoRelDir, nil, command.Plan, "", false, false, "workspace", "", "", false)
cmd := events.NewCommentCommand(c.RepoRelDir, nil, command.Plan, "", false, false, "", "workspace", "", "", false)
Equals(t, c.ExpDir, cmd.RepoRelDir)
})
}
}

func TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) {
cmd := events.NewCommentCommand("", nil, command.Plan, "", false, false, "", "", "", false)
cmd := events.NewCommentCommand("", nil, command.Plan, "", false, false, "", "", "", "", false)
Equals(t, events.CommentCommand{
RepoRelDir: "",
Flags: nil,
Expand All @@ -769,7 +769,7 @@ func TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) {
}

func TestNewCommand_AllFieldsSet(t *testing.T) {
cmd := events.NewCommentCommand("dir", []string{"a", "b"}, command.Plan, "", true, false, "workspace", "project", "policyset", false)
cmd := events.NewCommentCommand("dir", []string{"a", "b"}, command.Plan, "", true, false, "", "workspace", "project", "policyset", false)
Equals(t, events.CommentCommand{
Workspace: "workspace",
RepoRelDir: "dir",
Expand Down 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="", 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
Loading
Loading