Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const (
GHOrganizationFlag = "gh-org"
GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec
GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec
GHMergeQueueEnabledFlag = "gh-merge-queue-enabled"
GiteaBaseURLFlag = "gitea-base-url"
GiteaTokenFlag = "gitea-token"
GiteaUserFlag = "gitea-user"
Expand Down Expand Up @@ -560,6 +561,10 @@ var boolFlags = map[string]boolFlag{
description: "Feature flag to enable functionality to allow mergeable check to ignore apply required check",
defaultValue: false,
},
GHMergeQueueEnabledFlag: {
description: "Enable handling of GitHub merge queue (merge_group) events. When enabled, Atlantis posts success for plan/apply/policy_check on merge group commits so the merge queue can proceed.",
defaultValue: false,
},
GitlabStatusRetryEnabledFlag: {
description: "Enable enhanced retry logic for GitLab pipeline status updates with exponential backoff.",
defaultValue: false,
Expand Down
12 changes: 12 additions & 0 deletions runatlantis.io/docs/automerging.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,15 @@ then they should all be applied before Atlantis automatically merges the PR.
## Permissions

The Atlantis VCS user must have the ability to merge pull requests.

## GitHub Merge Queue

If the PR's base branch requires a [merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue), Atlantis cannot merge the PR directly via the REST API (GitHub returns `405 Method Not Allowed`). Instead, when automerge fires Atlantis enables auto-merge on the PR via GraphQL, which adds the PR to the merge queue. GitHub then runs the queue's required checks against the merge candidate commit and merges the PR once they pass.

For this to work end-to-end:

- Set [`--gh-merge-queue-enabled`](server-configuration.md#--gh-merge-queue-enabled) so Atlantis posts `success` for `plan`/`apply`/`policy_check` on `merge_group` events; otherwise the queue stalls waiting for those checks.
- Subscribe the GitHub webhook to the `merge_group` event (see [Configuring Webhooks](configuring-webhooks.md#github)).
- The Atlantis VCS user / GitHub App must have permission to enable auto-merge on the repo.

The `--auto-merge-method` setting is honored for non-queue branches; for branches enforcing a merge queue GitHub uses the queue's configured method and ignores any per-PR override.
1 change: 1 addition & 0 deletions runatlantis.io/docs/configuring-webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ If installing on a single repository, navigate to the repository home page and c
* **Pushes**
* **Issue comments**
* **Pull requests**
* **Merge groups** (only required if you use [GitHub merge queue](server-configuration.md#--gh-merge-queue-enabled) and have set `--gh-merge-queue-enabled`)
* leave **Active** checked
* click **Add webhook**
* See [Next Steps](#next-steps)
Expand Down
16 changes: 16 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,22 @@ ATLANTIS_GH_HOSTNAME="my.github.enterprise.com"
Hostname of your GitHub Enterprise installation. If using [GitHub.com](https://github.com),
don't set. Defaults to `github.com`.

### `--gh-merge-queue-enabled` <Badge text="v0.43.0+" type="info"/>

```bash
atlantis server --gh-merge-queue-enabled
# or
ATLANTIS_GH_MERGE_QUEUE_ENABLED=true
```

Enable handling of [GitHub merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue) (`merge_group`) webhook events. When enabled, on a `merge_group` `checks_requested` event Atlantis posts `success` for the `<status-name>/plan`, `<status-name>/apply`, and `<status-name>/policy_check` commit statuses on the merge group's head SHA so the merge queue can proceed.

Atlantis does not re-run `terraform plan`/`apply` against the merge ref — the PR was already validated before joining the queue, so posting `success` is sufficient to unblock the queue. `destroyed` actions are ignored.

To use this, ensure the `merge_group` event is enabled on the GitHub webhook (or for the GitHub App).

Defaults to `false`.

### `--gh-org` <Badge text="v0.1.3+" type="info"/>

```bash
Expand Down
102 changes: 101 additions & 1 deletion server/controllers/events/events_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/google/go-github/v83/github"
"github.com/microcosm-cc/bluemonday"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
Expand Down Expand Up @@ -87,7 +88,12 @@ type VCSEventsController struct {
// startup to support.
SupportedVCSHosts []models.VCSHostType `validate:"required"`
VCSClient vcs.Client `validate:"required"`
TestingMode bool
// CommitStatusUpdater is used to set commit statuses outside of the regular
// command runner flow, e.g. when handling GitHub merge queue events.
CommitStatusUpdater events.CommitStatusUpdater `validate:"required"`
// GithubMergeQueueEnabled toggles handling of GitHub merge_group events.
GithubMergeQueueEnabled bool
TestingMode bool
// BitbucketWebhookSecret is the secret added to this webhook via the Bitbucket
// UI that identifies this call as coming from Bitbucket. If empty, no
// request validation is done.
Expand Down Expand Up @@ -201,6 +207,10 @@ func (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Re
resp = e.HandleGithubPullRequestEvent(logger, event, githubReqID)
scope = scope.SubScope(fmt.Sprintf("pr_%s", *event.Action))
scope = common.SetGitScopeTags(scope, event.GetRepo().GetFullName(), event.GetNumber())
case *github.MergeGroupEvent:
resp = e.HandleGithubMergeGroupEvent(logger, event, githubReqID)
scope = scope.SubScope(fmt.Sprintf("merge_group_%s", event.GetAction()))
scope = common.SetGitScopeTags(scope, event.GetRepo().GetFullName(), 0)
default:
resp = HTTPResponse{
body: fmt.Sprintf("Ignoring unsupported event %s", githubReqID),
Expand Down Expand Up @@ -562,6 +572,96 @@ func (e *VCSEventsController) HandleGithubPullRequestEvent(logger logging.Simple
return e.handlePullRequestEvent(logger, baseRepo, headRepo, pull, user, pullEventType)
}

// HandleGithubMergeGroupEvent handles GitHub merge_group webhook events. When
// a merge group's checks are requested, Atlantis posts success commit statuses
// for plan, apply, and policy_check on the merge group's head SHA so that the
// merge queue can proceed. The PR was already validated by Atlantis before it
// joined the queue, so re-running plans here is unnecessary.
func (e *VCSEventsController) HandleGithubMergeGroupEvent(logger logging.SimpleLogging, event *github.MergeGroupEvent, githubReqID string) HTTPResponse {
if !e.GithubMergeQueueEnabled {
return HTTPResponse{
body: fmt.Sprintf("Ignoring merge_group event since gh-merge-queue-enabled is false %s", githubReqID),
}
}

action := event.GetAction()
if action != "checks_requested" {
return HTTPResponse{
body: fmt.Sprintf("Ignoring merge_group event with action %q %s", action, githubReqID),
}
}

baseRepo, err := e.Parser.ParseGithubRepo(event.GetRepo())
if err != nil {
wrapped := fmt.Errorf("parsing merge_group event: %s: %w", githubReqID, err)
return HTTPResponse{
body: wrapped.Error(),
err: HTTPError{
code: http.StatusBadRequest,
err: wrapped,
isSilenced: false,
},
}
}

logger = logger.With("repo", baseRepo.FullName)

if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
err := fmt.Errorf("merge_group event from non-allowlisted repo '%s/%s'", baseRepo.VCSHost.Hostname, baseRepo.FullName)
return HTTPResponse{
body: err.Error(),
err: HTTPError{
code: http.StatusForbidden,
err: err,
isSilenced: e.SilenceAllowlistErrors,
},
}
}

headSHA := event.GetMergeGroup().GetHeadSHA()
if headSHA == "" {
err := fmt.Errorf("merge_group event missing head SHA %s", githubReqID)
return HTTPResponse{
body: err.Error(),
err: HTTPError{
code: http.StatusBadRequest,
err: err,
isSilenced: false,
},
}
}

logger.Info("Handling GitHub Merge Group 'checks_requested' event for %s", headSHA)

pull := models.PullRequest{
HeadCommit: headSHA,
BaseRepo: baseRepo,
}

cmds := []command.Name{command.Plan, command.Apply, command.PolicyCheck}
var errs []error
for _, cmd := range cmds {
if err := e.CommitStatusUpdater.UpdateCombined(logger, baseRepo, pull, models.SuccessCommitStatus, cmd); err != nil {
errs = append(errs, fmt.Errorf("updating %s status: %w", cmd.String(), err))
}
}
if len(errs) > 0 {
joined := errors.Join(errs...)
return HTTPResponse{
body: joined.Error(),
err: HTTPError{
code: http.StatusInternalServerError,
err: joined,
isSilenced: false,
},
}
}

return HTTPResponse{
body: "Merge group checks marked as successful",
}
}

func (e *VCSEventsController) handlePullRequestEvent(logger logging.SimpleLogging, baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User, eventType models.PullRequestEventType) HTTPResponse {
if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
// If the repo isn't allowlisted and we receive an opened pull request
Expand Down
Loading
Loading