Repository: runatlantis/atlantis (language: go)

Issue:
--gh-allow-mergeable-bypass-apply for multiple servers
<!--- Please keep this note for the community --->

### Community Note

- Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
- Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
- If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---

- [ ] I'd be willing to implement this feature ([contributing guide](https://github.com/runatlantis/atlantis/blob/main/CONTRIBUTING.md))

**Describe the user story**
Our setup:
- multiple atlantis servers (v0.21.0) against one repo
- different `vcs-status-name` for each of them (to avoid overwriting status checks)
- `mergeable` requirements set for all projects
- all respective `apply` status checks marked as **required**
- `gh-allow-mergeable-bypass-apply` flag set on all servers

When testing for mergeability, the function [here](https://github.com/runatlantis/atlantis/blob/main/server/events/vcs/github_client.go#L400) only checks its own apply status but ignores those of other servers, causing all of them to report `mergeable` as failed.

Desired behavior: the servers ignore the `apply` status checks of all the servers and pass the `mergeable` check.

**Describe the solution you'd like**
You could allow deepening the hierarchy of the status check names (instead of `<vcs_status_name/<cmd_name>:` use `<vcs_status_name>/<cmd_name> [<vcs_server_name>]:`, eg. `atlantis/apply [serverA]:` and then you can keep using the prefix match that's present in the code right now.

This would involve a new configuration flag like `vcs-server-name` or similar that defaults to empty string and isn't used if not specified (the check name prefix would default to `<vcs_status_name/<cmd_name>:`).

**Describe the drawbacks of your solution**
This collides somewhat with the original concept of `vcs-status-name` which was supposed to set instances apart and could be confusing going forwards. The solution assumes multiple servers sharing `vcs-status-name` and being set apart by a different flag.

**Describe alternatives you've considered**

Add a configuration flag specifying a list of `vcs_status_name`s to ignore. Eg. a configuration flag `--gh-mergeable-bypass-names` that can be set to something like `["atlantis", "atlantis-A"]` on all the servers. It's not particularly pretty or scalable but it would do the job.

Other ways of dealing with the flag are possible, eg. a flag like `vcs_status_group_name` which could be an optional flag that changes the status check prefix or adds a needle in it to look for. Eg. when set to `mygroup` it turns the status check names into `mygroup/<vcs_status_name>/apply` and the prefix check is altered to take this into consideration.

Maybe even a change in the status check naming, changing it to eg. `atlantis/<CMD> [vcs-status-name]` but that's backwards incompatible and could break people's branch protection settings.

--gh-allow-mergeable-bypass-apply for multiple servers
<!--- Please keep this note for the community --->

### Community Note

- Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
- Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
- If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---

- [ ] I'd be willing to implement this feature ([contributing guide](https://github.com/runatlantis/atlantis/blob/main/CONTRIBUTING.md))

**Describe the user story**
Our setup:
- multiple atlantis servers (v0.21.0) against one repo
- different `vcs-status-name` for each of them (to avoid overwriting status checks)
- `mergeable` requirements set for all projects
- all respective `apply` status checks marked as **required**
- `gh-allow-mergeable-bypass-apply` flag set on all servers

When testing for mergeability, the function [here](https://github.com/runatlantis/atlantis/blob/main/server/events/vcs/github_client.go#L400) only checks its own apply status but ignores those of other servers, causing all of them to report `mergeable` as failed.

Desired behavior: the servers ignore the `apply` status checks of all the servers and pass the `mergeable` check.

**Describe the solution you'd like**
You could allow deepening the hierarchy of the status check names (instead of `<vcs_status_name/<cmd_name>:` use `<vcs_status_name>/<cmd_name> [<vcs_server_name>]:`, eg. `atlantis/apply [serverA]:` and then you can keep using the prefix match that's present in the code right now.

This would involve a new configuration flag like `vcs-server-name` or similar that defaults to empty string and isn't used if not specified (the check name prefix would default to `<vcs_status_name/<cmd_name>:`).

**Describe the drawbacks of your solution**
This collides somewhat with the original concept of `vcs-status-name` which was supposed to set instances apart and could be confusing going forwards. The solution assumes multiple servers sharing `vcs-status-name` and being set apart by a different flag.

**Describe alternatives you've considered**

Add a configuration flag specifying a list of `vcs_status_name`s to ignore. Eg. a configuration flag `--gh-mergeable-bypass-names` that can be set to something like `["atlantis", "atlantis-A"]` on all the servers. It's not particularly pretty or scalable but it would do the job.

Other ways of dealing with the flag are possible, eg. a flag like `vcs_status_group_name` which could be an optional flag that changes the status check prefix or adds a needle in it to look for. Eg. when set to `mygroup` it turns the status check names into `mygroup/<vcs_status_name>/apply` and the prefix check is altered to take this into consideration.

Maybe even a change in the status check naming, changing it to eg. `atlantis/<CMD> [vcs-status-name]` but that's backwards incompatible and could break people's branch protection settings.



Discussion / hints:
this might be a tricky implementation.

Let me cc others for their thoughts

- @krrrr38 since they implemented pr https://github.com/runatlantis/atlantis/pull/2805
- @rayterrill due to implementing https://github.com/runatlantis/atlantis/pull/2436
@Dawnflash does using `--executable-name` from 0.22.3 reduce or resolve this problem?
It sidesteps the problem, trades it for having to execute actions twice in some cases. I don't mind but we're all about self-service and this would confuse the heck out of devs.
I don't think separate executable names are an option for us. Yet, it's a great alternative for people who require `mergeable`. We just downgraded to `approved` for the time being.
+1 to this, any workaround that would allow us to use mergeable requirement ?
Note that this issue is more scary than it looks because Atlantis does not respect CODEOWNERS. So anyone can approve a PR and trigger an apply.
this might be a tricky implementation.

Let me cc others for their thoughts

- @krrrr38 since they implemented pr https://github.com/runatlantis/atlantis/pull/2805
- @rayterrill due to implementing https://github.com/runatlantis/atlantis/pull/2436
@Dawnflash does using `--executable-name` from 0.22.3 reduce or resolve this problem?
It sidesteps the problem, trades it for having to execute actions twice in some cases. I don't mind but we're all about self-service and this would confuse the heck out of devs.
I don't think separate executable names are an option for us. Yet, it's a great alternative for people who require `mergeable`. We just downgraded to `approved` for the time being.
+1 to this, any workaround that would allow us to use mergeable requirement ?
Note that this issue is more scary than it looks because Atlantis does not respect CODEOWNERS. So anyone can approve a PR and trigger an apply.

Fix patch:
diff --git a/cmd/server.go b/cmd/server.go
index e53ca20418..84ff1c23e9 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -146,6 +146,7 @@ const (
 	UseTFPluginCache                 = "use-tf-plugin-cache"
 	VarFileAllowlistFlag             = "var-file-allowlist"
 	VCSStatusName                    = "vcs-status-name"
+	IgnoreVCSStatusNames             = "ignore-vcs-status-names"
 	TFEHostnameFlag                  = "tfe-hostname"
 	TFELocalExecutionModeFlag        = "tfe-local-execution-mode"
 	TFETokenFlag                     = "tfe-token"
@@ -175,6 +176,7 @@ const (
 	DefaultGitlabHostname               = "gitlab.com"
 	DefaultLockingDBType                = "boltdb"
 	DefaultLogLevel                     = "info"
+	DefaultIgnoreVCSStatusNames         = ""
 	DefaultMaxCommentsPerCommand        = 100
 	DefaultParallelPoolSize             = 15
 	DefaultStatsNamespace               = "atlantis"
@@ -439,6 +441,12 @@ var stringFlags = map[string]stringFlag{
 		description: "Comma-separated list of additional paths where variable definition files can be read from." +
 			" If this argument is not provided, it defaults to Atlantis' data directory, determined by the --data-dir argument.",
 	},
+	IgnoreVCSStatusNames: {
+		description: "Comma separated list of VCS status names from other atlantis services." +
+			" When `gh-allow-mergeable-bypass-apply` is true, will ignore status checks (e.g. `status1/plan`, `status1/apply`, `status2/plan`, `status2/apply`) from other Atlantis services when checking if the PR is mergeable." +
+			" Currently only implemented for GitHub.",
+		defaultValue: DefaultIgnoreVCSStatusNames,
+	},
 	VCSStatusName: {
 		description:  "Name used to identify Atlantis for pull request statuses.",
 		defaultValue: DefaultVCSStatusName,
@@ -918,6 +926,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) {
 	if c.VCSStatusName == "" {
 		c.VCSStatusName = DefaultVCSStatusName
 	}
+	if c.IgnoreVCSStatusNames == "" {
+		c.IgnoreVCSStatusNames = DefaultIgnoreVCSStatusNames
+	}
 	if c.TFEHostname == "" {
 		c.TFEHostname = DefaultTFEHostname
 	}
diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md
index 610838f262..3c062ae853 100644
--- a/runatlantis.io/docs/server-configuration.md
+++ b/runatlantis.io/docs/server-configuration.md
@@ -857,6 +857,20 @@ This is useful when you have many projects and want to keep the pull request cle
   Used for example with CDKTF pre-workflow hooks that dynamically generate
   Terraform files.
 
+### `--ignore-vcs-status-names`
+
+   ```bash
+  atlantis server --ignore-vcs-status-names="status1,status2"
+  # or
+  ATLANTIS_IGNORE_VCS_STATUS_NAMES=status1,status2
+  ```
+
+   Comma separated list of VCS status names from other atlantis services.
+   When `gh-allow-mergeable-bypass-apply` is true, will ignore status checks
+   (e.g. `status1/plan`, `status1/apply`, `status2/plan`, `status2/apply`)
+   from other Atlantis services when checking if the PR is mergeable.
+   Currently only implemented for GitHub.
+
 ### `--locking-db-type`
 
   ```bash
diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go
index 1d115d28e9..63344d85e7 100644
--- a/server/events/vcs/azuredevops_client.go
+++ b/server/events/vcs/azuredevops_client.go
@@ -177,7 +177,7 @@ func (g *AzureDevopsClient) DiscardReviews(repo models.Repo, pull models.PullReq
 }
 
 // PullIsMergeable returns true if the merge request can be merged.
-func (g *AzureDevopsClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) { //nolint: revive
+func (g *AzureDevopsClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (bool, error) { //nolint: revive
 	owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)
 
 	opts := azuredevops.PullRequestGetOptions{IncludeWorkItemRefs: true}
diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go
index a0dcfd7161..c9a88f0245 100644
--- a/server/events/vcs/bitbucketcloud/client.go
+++ b/server/events/vcs/bitbucketcloud/client.go
@@ -139,7 +139,7 @@ func (b *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo,
 }
 
 // PullIsMergeable returns true if the merge request has no conflicts and can be merged.
-func (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string) (bool, error) {
+func (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (bool, error) {
 	nextPageURL := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/diffstat", b.BaseURL, repo.FullName, pull.Num)
 	// We'll only loop 1000 times as a safety measure.
 	maxLoops := 1000
diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go
index 69dbbde518..83956db629 100644
--- a/server/events/vcs/bitbucketserver/client.go
+++ b/server/events/vcs/bitbucketserver/client.go
@@ -203,7 +203,7 @@ func (b *Client) DiscardReviews(_ models.Repo, _ models.PullRequest) error {
 }
 
 // PullIsMergeable returns true if the merge request has no conflicts and can be merged.
-func (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string) (bool, error) {
+func (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (bool, error) {
 	projectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)
 	if err != nil {
 		return false, err
diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go
index b6ad7cb9cd..9e32981a82 100644
--- a/server/events/vcs/client.go
+++ b/server/events/vcs/client.go
@@ -30,7 +30,7 @@ type Client interface {
 	ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error
 	HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error
 	PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error)
-	PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error)
+	PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error)
 	// UpdateStatus updates the commit status to state for pull. src is the
 	// source of this status. This should be relatively static across runs,
 	// ex. atlantis/plan or atlantis/apply.
diff --git a/server/events/vcs/gitea/client.go b/server/events/vcs/gitea/client.go
index f9deb2cb74..9867d4c8f9 100644
--- a/server/events/vcs/gitea/client.go
+++ b/server/events/vcs/gitea/client.go
@@ -283,7 +283,7 @@ func (c *GiteaClient) PullIsApproved(logger logging.SimpleLogging, repo models.R
 }
 
 // PullIsMergeable returns true if the pull request is mergeable
-func (c *GiteaClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
+func (c *GiteaClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (bool, error) {
 	logger.Debug("Checking if Gitea pull request %d is mergeable", pull.Num)
 
 	pullRequest, _, err := c.giteaClient.GetPullRequest(repo.Owner, repo.Name, int64(pull.Num))
diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go
index 698d4dc999..d9c3d90541 100644
--- a/server/events/vcs/github_client.go
+++ b/server/events/vcs/github_client.go
@@ -751,7 +751,7 @@ func (g *GithubClient) ExpectedWorkflowPassed(expectedWorkflow WorkflowFileRefer
 }
 
 // IsMergeableMinusApply checks review decision (which takes into account CODEOWNERS) and required checks for PR (excluding the atlantis apply check).
-func (g *GithubClient) IsMergeableMinusApply(logger logging.SimpleLogging, repo models.Repo, pull *github.PullRequest, vcsstatusname string) (bool, error) {
+func (g *GithubClient) IsMergeableMinusApply(logger logging.SimpleLogging, repo models.Repo, pull *github.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {
 	if pull.Number == nil {
 		return false, errors.New("pull request number is nil")
 	}
@@ -778,8 +778,8 @@ func (g *GithubClient) IsMergeableMinusApply(logger logging.SimpleLogging, repo
 			// Ignore atlantis apply check(s)
 			continue
 		}
-		if !ExpectedCheckPassed(requiredCheck, checkRuns, statusContexts, vcsstatusname) {
-			logger.Debug("%s: Expected Required Check: %s", notMergeablePrefix, requiredCheck)
+		if !slices.Contains(ignoreVCSStatusNames, GetVCSStatusNameFromRequiredCheck(requiredCheck)) && !ExpectedCheckPassed(requiredCheck, checkRuns, statusContexts, vcsstatusname) {
+			logger.Debug("%s: Expected Required Check: %s VCS Status Name: %s Ignore VCS Status Names: %s", notMergeablePrefix, requiredCheck, vcsstatusname, ignoreVCSStatusNames)
 			return false, nil
 		}
 	}
@@ -797,8 +797,12 @@ func (g *GithubClient) IsMergeableMinusApply(logger logging.SimpleLogging, repo
 	return true, nil
 }
 
+func GetVCSStatusNameFromRequiredCheck(requiredCheck githubv4.String) string {
+	return strings.Split(string(requiredCheck), "/")[0]
+}
+
 // PullIsMergeable returns true if the pull request is mergeable.
-func (g *GithubClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
+func (g *GithubClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {
 	logger.Debug("Checking if GitHub pull request %d is mergeable", pull.Num)
 	githubPR, err := g.GetPullRequest(logger, repo, pull.Num)
 	if err != nil {
@@ -820,7 +824,7 @@ func (g *GithubClient) PullIsMergeable(logger logging.SimpleLogging, repo models
 	case "blocked":
 		if g.config.AllowMergeableBypassApply {
 			logger.Debug("AllowMergeableBypassApply feature flag is enabled - attempting to bypass apply from mergeable requirements")
-			isMergeableMinusApply, err := g.IsMergeableMinusApply(logger, repo, githubPR, vcsstatusname)
+			isMergeableMinusApply, err := g.IsMergeableMinusApply(logger, repo, githubPR, vcsstatusname, ignoreVCSStatusNames)
 			if err != nil {
 				return false, errors.Wrap(err, "getting pull request status")
 			}
diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go
index 289b1a0d8a..b78c82b271 100644
--- a/server/events/vcs/gitlab_client.go
+++ b/server/events/vcs/gitlab_client.go
@@ -300,7 +300,7 @@ func (g *GitlabClient) PullIsApproved(logger logging.SimpleLogging, repo models.
 // See:
 // - https://gitlab.com/gitlab-org/gitlab-ee/issues/3169
 // - https://gitlab.com/gitlab-org/gitlab-ce/issues/42344
-func (g *GitlabClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
+func (g *GitlabClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, _ []string) (bool, error) {
 	logger.Debug("Checking if GitLab merge request %d is mergeable", pull.Num)
 	mr, resp, err := g.Client.MergeRequests.GetMergeRequest(repo.FullName, pull.Num, nil)
 	if resp != nil {
diff --git a/server/events/vcs/instrumented_client.go b/server/events/vcs/instrumented_client.go
index 942a4823e1..32022438f1 100644
--- a/server/events/vcs/instrumented_client.go
+++ b/server/events/vcs/instrumented_client.go
@@ -183,7 +183,7 @@ func (c *InstrumentedClient) PullIsApproved(logger logging.SimpleLogging, repo m
 	return approved, err
 }
 
-func (c *InstrumentedClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
+func (c *InstrumentedClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {
 	scope := c.StatsScope.SubScope("pull_is_mergeable")
 	scope = SetGitScopeTags(scope, repo.FullName, pull.Num)
 
@@ -193,7 +193,7 @@ func (c *InstrumentedClient) PullIsMergeable(logger logging.SimpleLogging, repo
 	executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
 	executionError := scope.Counter(metrics.ExecutionErrorMetric)
 
-	mergeable, err := c.Client.PullIsMergeable(logger, repo, pull, vcsstatusname)
+	mergeable, err := c.Client.PullIsMergeable(logger, repo, pull, vcsstatusname, ignoreVCSStatusNames)
 
 	if err != nil {
 		executionError.Inc(1)
diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go
index ffa37fe8cb..f51036b87a 100644
--- a/server/events/vcs/mocks/mock_client.go
+++ b/server/events/vcs/mocks/mock_client.go
@@ -223,11 +223,11 @@ func (mock *MockClient) PullIsApproved(logger logging.SimpleLogging, repo models
 	return ret0, ret1
 }
 
-func (mock *MockClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
+func (mock *MockClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {
 	if mock == nil {
 		panic("mock must not be nil. Use myMock := NewMockClient().")
 	}
-	params := []pegomock.Param{logger, repo, pull, vcsstatusname}
+	params := []pegomock.Param{logger, repo, pull, vcsstatusname, ignoreVCSStatusNames}
 	result := pegomock.GetGenericMockFrom(mock).Invoke("PullIsMergeable", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
 	var ret0 bool
 	var ret1 error
diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go
index 6fd7a731cb..41b14ad2c6 100644
--- a/server/events/vcs/not_configured_vcs_client.go
+++ b/server/events/vcs/not_configured_vcs_client.go
@@ -45,7 +45,7 @@ func (a *NotConfiguredVCSClient) PullIsApproved(_ logging.SimpleLogging, _ model
 func (a *NotConfiguredVCSClient) DiscardReviews(_ models.Repo, _ models.PullRequest) error {
 	return nil
 }
-func (a *NotConfiguredVCSClient) PullIsMergeable(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ string) (bool, error) {
+func (a *NotConfiguredVCSClient) PullIsMergeable(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ string, _ []string) (bool, error) {
 	return false, a.err()
 }
 func (a *NotConfiguredVCSClient) UpdateStatus(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ models.CommitStatus, _ string, _ string, _ string) error {
diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go
index cd67b84c90..68aa45bf58 100644
--- a/server/events/vcs/proxy.go
+++ b/server/events/vcs/proxy.go
@@ -81,8 +81,8 @@ func (d *ClientProxy) DiscardReviews(repo models.Repo, pull models.PullRequest)
 	return d.clients[repo.VCSHost.Type].DiscardReviews(repo, pull)
 }
 
-func (d *ClientProxy) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
-	return d.clients[repo.VCSHost.Type].PullIsMergeable(logger, repo, pull, vcsstatusname)
+func (d *ClientProxy) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {
+	return d.clients[repo.VCSHost.Type].PullIsMergeable(logger, repo, pull, vcsstatusname, ignoreVCSStatusNames)
 }
 
 func (d *ClientProxy) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {
diff --git a/server/events/vcs/pull_status_fetcher.go b/server/events/vcs/pull_status_fetcher.go
index b5b8a46383..b96d69011a 100644
--- a/server/events/vcs/pull_status_fetcher.go
+++ b/server/events/vcs/pull_status_fetcher.go
@@ -13,14 +13,16 @@ type PullReqStatusFetcher interface {
 }
 
 type pullReqStatusFetcher struct {
-	client        Client
-	vcsStatusName string
+	client               Client
+	vcsStatusName        string
+	ignoreVCSStatusNames []string
 }
 
-func NewPullReqStatusFetcher(client Client, vcsStatusName string) PullReqStatusFetcher {
+func NewPullReqStatusFetcher(client Client, vcsStatusName string, ignoreVCSStatusNames []string) PullReqStatusFetcher {
 	return &pullReqStatusFetcher{
-		client:        client,
-		vcsStatusName: vcsStatusName,
+		client:               client,
+		vcsStatusName:        vcsStatusName,
+		ignoreVCSStatusNames: ignoreVCSStatusNames,
 	}
 }
 
@@ -30,7 +32,7 @@ func (f *pullReqStatusFetcher) FetchPullStatus(logger logging.SimpleLogging, pul
 		return pullStatus, errors.Wrapf(err, "fetching pull approval status for repo: %s, and pull number: %d", pull.BaseRepo.FullName, pull.Num)
 	}
 
-	mergeable, err := f.client.PullIsMergeable(logger, pull.BaseRepo, pull, f.vcsStatusName)
+	mergeable, err := f.client.PullIsMergeable(logger, pull.BaseRepo, pull, f.vcsStatusName, f.ignoreVCSStatusNames)
 	if err != nil {
 		return pullStatus, errors.Wrapf(err, "fetching mergeability status for repo: %s, and pull number: %d", pull.BaseRepo.FullName, pull.Num)
 	}
diff --git a/server/server.go b/server/server.go
index af940f3787..f2d93b5a49 100644
--- a/server/server.go
+++ b/server/server.go
@@ -722,7 +722,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
 		userConfig.QuietPolicyChecks,
 	)
 
-	pullReqStatusFetcher := vcs.NewPullReqStatusFetcher(vcsClient, userConfig.VCSStatusName)
+	pullReqStatusFetcher := vcs.NewPullReqStatusFetcher(vcsClient, userConfig.VCSStatusName, strings.Split(userConfig.IgnoreVCSStatusNames, ","))
 	planCommandRunner := events.NewPlanCommandRunner(
 		userConfig.SilenceVCSStatusNoPlans,
 		userConfig.SilenceVCSStatusNoProjects,
diff --git a/server/user_config.go b/server/user_config.go
index 9a6247ae09..8693a55549 100644
--- a/server/user_config.go
+++ b/server/user_config.go
@@ -75,6 +75,7 @@ type UserConfig struct {
 	LogLevel                        string `mapstructure:"log-level"`
 	MarkdownTemplateOverridesDir    string `mapstructure:"markdown-template-overrides-dir"`
 	MaxCommentsPerCommand           int    `mapstructure:"max-comments-per-command"`
+	IgnoreVCSStatusNames            string `mapstructure:"ignore-vcs-status-names"`
 	ParallelPoolSize                int    `mapstructure:"parallel-pool-size"`
 	ParallelPlan                    bool   `mapstructure:"parallel-plan"`
 	ParallelApply                   bool   `mapstructure:"parallel-apply"`


Test patch:
diff --git a/cmd/server_test.go b/cmd/server_test.go
index cccf9cc055..2c9034af48 100644
--- a/cmd/server_test.go
+++ b/cmd/server_test.go
@@ -145,6 +145,7 @@ var testFlags = map[string]interface{}{
 	UseTFPluginCache:                 true,
 	VarFileAllowlistFlag:             "/path",
 	VCSStatusName:                    "my-status",
+	IgnoreVCSStatusNames:             "",
 	WebBasicAuthFlag:                 false,
 	WebPasswordFlag:                  "atlantis",
 	WebUsernameFlag:                  "atlantis",
diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go
index 7a80b13b07..6b985fd9b8 100644
--- a/server/controllers/events/events_controller_e2e_test.go
+++ b/server/controllers/events/events_controller_e2e_test.go
@@ -1191,7 +1191,7 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
 			// Setup test dependencies.
 			w := httptest.NewRecorder()
 			When(vcsClient.PullIsMergeable(
-				Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq("atlantis-test"))).ThenReturn(true, nil)
+				Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq("atlantis-test"), Eq([]string{}))).ThenReturn(true, nil)
 			When(vcsClient.PullIsApproved(
 				Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(models.ApprovalStatus{
 				IsApproved: true,
@@ -1505,7 +1505,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
 		userConfig.QuietPolicyChecks,
 	)
 
-	e2ePullReqStatusFetcher := vcs.NewPullReqStatusFetcher(e2eVCSClient, "atlantis-test")
+	e2ePullReqStatusFetcher := vcs.NewPullReqStatusFetcher(e2eVCSClient, "atlantis-test", []string{})
 
 	planCommandRunner := events.NewPlanCommandRunner(
 		false,
diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go
index c4d41b441a..fa2dee091c 100644
--- a/server/events/command_runner_test.go
+++ b/server/events/command_runner_test.go
@@ -1148,7 +1148,7 @@ func TestApplyMergeablityWhenPolicyCheckFails(t *testing.T) {
 		},
 	})
 
-	When(ch.VCSClient.PullIsMergeable(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull), Eq("atlantis-test"))).ThenReturn(true, nil)
+	When(ch.VCSClient.PullIsMergeable(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull), Eq("atlantis-test"), Eq([]string{}))).ThenReturn(true, nil)
 
 	When(projectCommandBuilder.BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]())).Then(func(args []Param) ReturnValues {
 		return ReturnValues{
diff --git a/server/events/vcs/azuredevops_client_test.go b/server/events/vcs/azuredevops_client_test.go
index 1e428023b8..a7095262d2 100644
--- a/server/events/vcs/azuredevops_client_test.go
+++ b/server/events/vcs/azuredevops_client_test.go
@@ -428,7 +428,7 @@ func TestAzureDevopsClient_PullIsMergeable(t *testing.T) {
 					},
 				}, models.PullRequest{
 					Num: 1,
-				}, "atlantis-test")
+				}, "atlantis-test", []string{})
 			Ok(t, err)
 			Equals(t, c.expMergeable, actMergeable)
 		})
diff --git a/server/events/vcs/bitbucketcloud/client_test.go b/server/events/vcs/bitbucketcloud/client_test.go
index 0a4bd48db6..59108a14f3 100644
--- a/server/events/vcs/bitbucketcloud/client_test.go
+++ b/server/events/vcs/bitbucketcloud/client_test.go
@@ -352,7 +352,7 @@ func TestClient_PullIsMergeable(t *testing.T) {
 					},
 				}, models.PullRequest{
 					Num: 1,
-				}, "atlantis-test")
+				}, "atlantis-test", []string{})
 			Ok(t, err)
 			Equals(t, c.ExpMergeable, actMergeable)
 		})
diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go
index b668f9e919..05f1ff939b 100644
--- a/server/events/vcs/github_client_test.go
+++ b/server/events/vcs/github_client_test.go
@@ -609,7 +609,7 @@ func TestGithubClient_PullIsMergeable(t *testing.T) {
 					},
 				}, models.PullRequest{
 					Num: 1,
-				}, vcsStatusName)
+				}, vcsStatusName, []string{})
 			Ok(t, err)
 			Equals(t, c.expMergeable, actMergeable)
 		})
@@ -619,6 +619,7 @@ func TestGithubClient_PullIsMergeable(t *testing.T) {
 func TestGithubClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T) {
 	logger := logging.NewNoopLogger(t)
 	vcsStatusName := "atlantis"
+	ignoreVCSStatusNames := []string{"other-atlantis"}
 	cases := []struct {
 		state                     string
 		statusCheckRollupFilePath string
@@ -715,6 +716,12 @@ func TestGithubClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T)
 			`"APPROVED"`,
 			false,
 		},
+		{
+			"blocked",
+			"ruleset-check-pending-other-atlantis.json",
+			`"APPROVED"`,
+			true,
+		},
 		{
 			"blocked",
 			"ruleset-check-skipped.json",
@@ -763,6 +770,12 @@ func TestGithubClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T)
 			`"APPROVED"`,
 			false,
 		},
+		{
+			"blocked",
+			"ruleset-check-failed-other-atlantis.json",
+			`"APPROVED"`,
+			true,
+		},
 		{
 			"blocked",
 			"ruleset-check-passed.json",
@@ -879,7 +892,7 @@ func TestGithubClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T)
 					},
 				}, models.PullRequest{
 					Num: 1,
-				}, vcsStatusName)
+				}, vcsStatusName, ignoreVCSStatusNames)
 			Ok(t, err)
 			Equals(t, c.expMergeable, actMergeable)
 		})
diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go
index cef424bcb0..98de58287f 100644
--- a/server/events/vcs/gitlab_client_test.go
+++ b/server/events/vcs/gitlab_client_test.go
@@ -659,7 +659,7 @@ func TestGitlabClient_PullIsMergeable(t *testing.T) {
 						Num:        c.mrID,
 						BaseRepo:   repo,
 						HeadCommit: "67cb91d3f6198189f433c045154a885784ba6977",
-					}, vcsStatusName)
+					}, vcsStatusName, []string{})
 
 				Ok(t, err)
 				Equals(t, c.expState, mergeable)
diff --git a/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-failed-other-atlantis.json b/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-failed-other-atlantis.json
new file mode 100644
index 0000000000..38d291aa74
--- /dev/null
+++ b/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-failed-other-atlantis.json
@@ -0,0 +1,80 @@
+{
+  "data": {
+    "repository": {
+      "pullRequest": {
+        "reviewDecision": null,
+        "baseRef": {
+          "branchProtectionRule": {
+            "requiredStatusChecks": []
+          },
+          "rules": {
+            "pageInfo": {
+              "endCursor": "QWERTY",
+              "hasNextPage": false
+            },
+            "nodes": [
+              {
+                "type": "REQUIRED_STATUS_CHECKS",
+                "repositoryRuleset": {
+                  "enforcement": "ACTIVE"
+                },
+                "parameters": {
+                  "requiredStatusChecks": [
+                    {
+                      "context": "atlantis/apply"
+                    },
+                    {
+                      "context": "other-atlantis/apply"
+                    }
+                  ]
+                }
+              }
+            ]
+          }
+        },
+        "commits": {
+          "nodes": [
+            {
+              "commit": {
+                "statusCheckRollup": {
+                  "contexts": {
+                    "pageInfo": {
+                      "endCursor": "QWERTY",
+                      "hasNextPage": false
+                    },
+                    "nodes": [
+                      {
+                        "__typename": "StatusContext",
+                        "context": "atlantis/apply",
+                        "state": "PENDING",
+                        "isRequired": true
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "atlantis/plan",
+                        "state": "SUCCESS",
+                        "isRequired": false
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "other-atlantis/apply",
+                        "state": "FAILED",
+                        "isRequired": true
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "other-atlantis/apply",
+                        "state": "SUCCESS",
+                        "isRequired": false
+                      }
+                    ]
+                  }
+                }
+              }
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-pending-other-atlantis.json b/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-pending-other-atlantis.json
new file mode 100644
index 0000000000..f0936309e0
--- /dev/null
+++ b/server/events/vcs/testdata/github-pull-request-mergeability/ruleset-check-pending-other-atlantis.json
@@ -0,0 +1,80 @@
+{
+  "data": {
+    "repository": {
+      "pullRequest": {
+        "reviewDecision": null,
+        "baseRef": {
+          "branchProtectionRule": {
+            "requiredStatusChecks": []
+          },
+          "rules": {
+            "pageInfo": {
+              "endCursor": "QWERTY",
+              "hasNextPage": false
+            },
+            "nodes": [
+              {
+                "type": "REQUIRED_STATUS_CHECKS",
+                "repositoryRuleset": {
+                  "enforcement": "ACTIVE"
+                },
+                "parameters": {
+                  "requiredStatusChecks": [
+                    {
+                      "context": "atlantis/apply"
+                    },
+                    {
+                      "context": "other-atlantis/apply"
+                    }
+                  ]
+                }
+              }
+            ]
+          }
+        },
+        "commits": {
+          "nodes": [
+            {
+              "commit": {
+                "statusCheckRollup": {
+                  "contexts": {
+                    "pageInfo": {
+                      "endCursor": "QWERTY",
+                      "hasNextPage": false
+                    },
+                    "nodes": [
+                      {
+                        "__typename": "StatusContext",
+                        "context": "atlantis/apply",
+                        "state": "PENDING",
+                        "isRequired": true
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "atlantis/plan",
+                        "state": "SUCCESS",
+                        "isRequired": false
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "other-atlantis/apply",
+                        "state": "PENDING",
+                        "isRequired": true
+                      },
+                      {
+                        "__typename": "StatusContext",
+                        "context": "other-atlantis/plan",
+                        "state": "PENDING",
+                        "isRequired": false
+                      }
+                    ]
+                  }
+                }
+              }
+            }
+          ]
+        }
+      }
+    }
+  }
+}