Repository: runatlantis/atlantis (language: go)

Issue:
Gitlab: duplicate pipelines in gitlab merge requests
<!--- 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 --->

---

<!---
When filing a bug, please include the following headings if possible.
Any example text in this template can be deleted.
--->

### Overview of the Issue

We updated our Atlantis from v0.20.1 to v0.23.5 and since then we detect that the Atlantis steps are running in a different pipeline than the project, so we now have double pipelines.

<!---
Please describe the issue you are having and how you encountered the problem.
--->


### Reproduction Steps

<!--- 
In order to effectively and quickly resolve the issue, please provide exact steps that allow us the reproduce the problem. If no steps are provided, then it will likely take longer to get the issue resolved.
--->


### Logs

<!---
Provide log files from Atlantis server

logs can be retrieved from the deployment or from atlantis comments by adding `--debug` such as `atlantis plan --debug`

<details>
  <summary>Logs</summary>

```
03/05/2023, 15:09:59 | {"level":"debug","ts":"2023-05-03T14:09:59.457Z","caller":"server/middleware.go:45","msg":"GET / – from 172.31.60.62:43432","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis
03/05/2023, 15:09:59 | {"level":"info","ts":"2023-05-03T14:09:59.457Z","caller":"server/server.go:1040","msg":"Apply Lock: {false 0001-01-01 00:00:00 +0000 UTC }","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis
03/05/2023, 15:09:59 | {"level":"debug","ts":"2023-05-03T14:09:59.457Z","caller":"server/middleware.go:72","msg":"GET / – respond HTTP 200","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis
03/05/2023, 15:09:59 | {"level":"debug","ts":"2023-05-03T14:09:59.351Z","caller":"server/middleware.go:45","msg":"GET / – from 172.31.60.25:4318","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis
03/05/2023, 15:09:59 | {"level":"info","ts":"2023-05-03T14:09:59.351Z","caller":"server/server.go:1040","msg":"Apply Lock: {false 0001-01-01 00:00:00 +0000 UTC }","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis
03/05/2023, 15:09:59 | {"level":"debug","ts":"2023-05-03T14:09:59.351Z","caller":"server/middleware.go:72","msg":"GET / – respond HTTP 200","json":{}} | fba962f24ecb405f8cf5bf359a96c130 | atlantis

03/05/2023, 15:09:59	{"level":"debug","ts":"2023-05-03T14:09:59.457Z","caller":"server/middleware.go:45","msg":"GET / – from 172.31.60.62:43432","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
03/05/2023, 15:09:59	{"level":"info","ts":"2023-05-03T14:09:59.457Z","caller":"server/server.go:1040","msg":"Apply Lock: {false 0001-01-01 00:00:00 +0000 UTC }","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
03/05/2023, 15:09:59	{"level":"debug","ts":"2023-05-03T14:09:59.457Z","caller":"server/middleware.go:72","msg":"GET / – respond HTTP 200","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
03/05/2023, 15:09:59	{"level":"debug","ts":"2023-05-03T14:09:59.351Z","caller":"server/middleware.go:45","msg":"GET / – from 172.31.60.25:4318","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
03/05/2023, 15:09:59	{"level":"info","ts":"2023-05-03T14:09:59.351Z","caller":"server/server.go:1040","msg":"Apply Lock: {false 0001-01-01 00:00:00 +0000 UTC }","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
03/05/2023, 15:09:59	{"level":"debug","ts":"2023-05-03T14:09:59.351Z","caller":"server/middleware.go:72","msg":"GET / – respond HTTP 200","json":{}}	[fba962f24ecb405f8cf5bf359a96c130](https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/atlantis/services/atlantis/configuration/fba962f24ecb405f8cf5bf359a96c130?region=eu-west-1)	atlantis
```

</details>
--->


### Environment details

- Atlantis version: v0.23.5
- Deployment method: ecs
- Atlantis flags:

Atlantis server-side config file:
```shell
atlantis server --gitlab-hostname="$git_hostname" \
                         --gitlab-token="$git_token" \
                         --gitlab-user="$git_username" \
                         --gitlab-webhook-secret="$git_webhook" \
                         --checkout-strategy='merge' \
                         --enable-diff-markdown-format \
                         --repo-whitelist='*' \
                         --autoplan-file-list='**/*.tf,**/*.tfvars,**/*.hcl,*.tf,*.tfvars,*.hcl' \
                         --repo-config="/atlantis/repos.yaml"
```


```yaml
repos:
- id: /.*/
  allowed_overrides: [apply_requirements, workflow]
  allow_custom_workflows: true
  pre_workflow_hooks:
    - run: python3 /scripts/inject_policy_checker.py
- id: /.*comp\/realestate.*/
  allowed_overrides: [apply_requirements, workflow]
  allow_custom_workflows: true
  pre_workflow_hooks:
    - run: python3 /scripts/inject_policy_checker.py
    - run: python3 /scripts/inject_infracost.py
  post_workflow_hooks:
    - run: |
        if [ ! -d "/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM" ]; then
          exit 0
        fi

        infracost comment gitlab --repo $BASE_REPO_OWNER/$BASE_REPO_NAME \
                                 --merge-request $PULL_NUM \
                                 --path /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM/'*'-infracost.json \
                                 --gitlab-server-url https://git.example.com \
                                 --gitlab-token $ATLANTIS_GITLAB_TOKEN \
                                 --behavior new

        rm -rf /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM
- id: /.*infrastructure\/vv\/.*/
  allowed_overrides: [apply_requirements, workflow]
  allow_custom_workflows: true
  pre_workflow_hooks:
    - run: python3 /scripts/inject_policy_checker.py
    - run: python3 /scripts/inject_infracost.py
  post_workflow_hooks:
    - run: |
        if [ ! -d "/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM" ]; then
          exit 0
        fi

        infracost comment gitlab --repo $BASE_REPO_OWNER/$BASE_REPO_NAME \
                                 --merge-request $PULL_NUM \
                                 --path /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM/'*'-infracost.json \
                                 --gitlab-server-url https://git.example.com \
                                 --gitlab-token $ATLANTIS_GITLAB_TOKEN \
                                 --behavior new

        rm -rf /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM
      description: Sending Gitlab comment with the table
- id: /.*comp\/motors\/.*/
  allowed_overrides: [apply_requirements, workflow]
  allow_custom_workflows: true
  pre_workflow_hooks:
    - run: python3 /scripts/inject_policy_checker.py
    - run: python3 /scripts/inject_infracost.py
  post_workflow_hooks:
    - run: |
        if [ ! -d "/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM" ]; then
          exit 0
        fi

        infracost comment gitlab --repo $BASE_REPO_OWNER/$BASE_REPO_NAME \
                                 --merge-request $PULL_NUM \
                                 --path /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM/'*'-infracost.json \
                                 --gitlab-server-url https://git.example.com \
                                 --gitlab-token $ATLANTIS_GITLAB_TOKEN \
                                 --behavior new

        rm -rf /tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM
```

Repo `atlantis.yaml` file:

```yaml
version: 3
automerge: false
parallel_plan: true
parallel_apply: true
projects:
  - name: production
    dir: infrastructure
    terraform_version: v1.0.7
    workflow: production
    workspace: production
    apply_requirements:
      - approved
    autoplan:
      when_modified: [../**/*]
  - name: staging
    dir: infrastructure
    terraform_version: v1.0.7
    workflow: staging
    workspace: staging
    apply_requirements:
      - approved
    autoplan:
      when_modified: [../**/*]
  - name: terragrunt-example
    dir: infrastructure-terragrunt
    workflow: terragrunt
    terraform_version: v1.0.7
    apply_requirements:
      - approved
    autoplan:
      when_modified: [../**/*]
workflows:
  production:
    plan:
      steps:
        - run: rm -rf .terraform*
        - init:
            extra_args:
              - -backend-config=../backends/production.backend.tfvars
        - plan:
            extra_args:
              - -compact-warnings
              - -var 'region=us-west-1'
              - -var 'account=xxx'
              - -var 'role_name=atlantis'
    apply:
      steps:
        - apply
  staging:
    plan:
      steps:
        - run: rm -rf .terraform*
        - init:
            extra_args:
              - -backend-config=../backends/staging.backend.tfvars
        - plan:
            extra_args:
              - -compact-warnings
              - -var 'region=us-west-1'
              - -var 'account=xxx'
              - -var 'role_name=atlantis'
    apply:
      steps:
        - apply
  terragrunt:
    plan:
      steps:
        - env:
            name: TF_IN_AUTOMATION
            value: true
        - env:
            name: TERRAGRUNT_IAM_ROLE
            value: arn:aws:iam::xxx:role/atlantis
        - run: terragrunt init --terragrunt-source-update --terragrunt-non-interactive
        - run: terragrunt plan -out=$PLANFILE
    apply:
      steps:
        - env:
            name: TF_IN_AUTOMATION
            value: true
        - env:
            name: TERRAGRUNT_IAM_ROLE
            value: arn:aws:iam::xxx:role/atlantis
        - run: terragrunt apply $PLANFILE

```



Discussion / hints:
did you check the other issues? there was a PR that fixed this a while back.
I didn't find anything similar open, but found this PR https://github.com/runatlantis/atlantis/pull/2745 
It is the same behaviour reported here https://github.com/runatlantis/atlantis/issues/2743
@jukie do you have any ideas?
@paulozava Please, could you confirm the GitLab version are you using (gitlab.com or self-hosted)? And the `.gitlab-ci.yaml` config too.
Sure, we are using GitLab self-hosted at version 15.9.4-ee

The `.gitlab-ci.yaml`:

```` yaml
image: registry.corp.com/infrastructure/verticals/linters:latest

stages:
  - Lint
  - Deploy

YAML lint:
  stage: Lint
  script:
    - find . -name "*.yml" -o -name "*.yaml" | xargs -n 1 yamllint -d relaxed --no-warnings
  only:
    - merge_requests
  tags: [k8s-motors]

Kustomize Lint:
  stage: Lint
  variables:
    K8S_INFRASTRUCTURE_FILES_FOLDER: k8s-clusterconfig
  script:
    - kustomize build $K8S_INFRASTRUCTURE_FILES_FOLDER/staging > /dev/null
    - kustomize build $K8S_INFRASTRUCTURE_FILES_FOLDER/production > /dev/null
    - kustomize build argocd-apps/production > /dev/null
    - kustomize build argocd-apps/staging > /dev/null
  only:
    - merge_requests
  tags: [k8s-motors]

```

Hi everyone,

Meanwhile I was able to perform some tests and identify why we get duplicate pipelines and I believe the problem may be the way the implementation of https://github.com/runatlantis/atlantis/pull/2745 was done.

Let me try to explain.

If you have a project which contains [workflows](https://docs.gitlab.com/ee/ci/yaml/workflow.html) to create MR pipelines, the MR 2745 works just fine and Atlantis pipeline will appear in the same MR pipeline (as expected)

If you enable in gitlab MR options the setting "**Enable merged results pipelines**" and then push a commit, the MR pipeline will run in a different pipeline than the Atlantis which will now run a branch pipeline.
This in reality is not a big deal. The problem happens when you revert the setting above

When you revert it, you would expect the normal behavior (both Atlantis and the project pipeline in the same line) but in reality what happens is that the behavior above is still kept (2 separate pipelines)

From what I could understand the issue is with this bit of code which gets the head pipeline from the MR
```
mr, err := g.GetMergeRequest(pull.BaseRepo.FullName, pull.Num)
if err != nil {
	return err
}
```
The issue is that the api call above will return the Atlantis pipeline as being the head pipeline and `mr.Pipeline.Source` will be `external`

From that moment on, unless you trigger a new pipeline for the project, the last pipeline will always be the one from Atlantis which is identified as `external` and the pipelines will never "merge"

I was also able to test that if the function is just
```
func (g *GitlabClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {
	gitlabState := gitlab.Pending
	switch state {
	case models.PendingCommitStatus:
		gitlabState = gitlab.Running
	case models.FailedCommitStatus:
		gitlabState = gitlab.Failed
	case models.SuccessCommitStatus:
		gitlabState = gitlab.Success
	}

	options := &gitlab.SetCommitStatusOptions{
		State:       gitlabState,
		Context:     gitlab.String(src),
		Description: gitlab.String(description),
		TargetURL:   &url,
	}

	_, _, err := g.Client.Commits.SetCommitStatus(repo.FullName, pull.HeadCommit, options)

	return err
}
```
Deliberately not passing `refTarget` seems to work for all cases and I guess Gitlab will always try to see what use-case it fits best? :man_shrugging: 

I don't have any use-case or example of when an event is anything else than a merge_request_event to test and would be cool if someone could present some examples of the pipelines that originally were being duplicated and that got solved with the MR above.

I also noticed that (at least for Gitlab) the supported Webhook events are `Comments` and `Merge Request events` and I am wondering if all the logic from the MR is ever going to be needed.

**Thank you in advance for your time and help**, and happy to propose an MR with any sort of fix but just want to better understand first the motivation for the code above and how we can make it work for all use-cases :smiley: 


We self host 15.11.2-ee and have this same problem.
Pinging @michelmzs (I should've done it sooner) as you were the author of the initial MR
Is anyone currently working on this? This has been a pain point for us for a while. I'd be willing to take a shot at implementing a fix if nobody else has work in flight. 
I thought this was fixed already, have you tested 0.26.0?

On Wed, Oct 18, 2023, 5:37 p.m. WarpRat ***@***.***> wrote:

> Is anyone currently working on this? This has been a pain point for us for
> a while. I'd be willing to take a shot at implementing a fix if nobody else
> has work in flight.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/runatlantis/atlantis/issues/3373#issuecomment-1769710310>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAQ3ERHYRL7I5DTVSBFHDYTYABY55AVCNFSM6AAAAAAXUP3FNGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTONRZG4YTAMZRGA>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>

Yes, we're still seeing this issue on 0.26.0 with merged results pipelines enabled. Based on the description in [this comment](https://github.com/runatlantis/atlantis/issues/3373#issuecomment-1613517907) I'm not sure if it's even possible to have converged merged results pipelines and external status pipelines which would be ideal. However, we still see the behavior described in that comment where even disabling merged results pipelines still results in a merge request pipeline with our jobs and a separate branch pipeline with the Altantis jobs. If we don't use merge request pipelines at all then everything works as expected in the branch pipeline. If we have to stick to branch pipelines only we can do that but it sounds like it would be a minor fix to get things at least working in merge request pipelines again.

We're on Gitlab SaaS which is currently 16.5. 
> Hi everyone,
> 
> Meanwhile I was able to perform some tests and identify why we get duplicate pipelines and I believe the problem may be the way the implementation of #2745 was done.
> 
> Let me try to explain.
> 
> If you have a project which contains [workflows](https://docs.gitlab.com/ee/ci/yaml/workflow.html) to create MR pipelines, the MR 2745 works just fine and Atlantis pipeline will appear in the same MR pipeline (as expected)
> 
> If you enable in gitlab MR options the setting "**Enable merged results pipelines**" and then push a commit, the MR pipeline will run in a different pipeline than the Atlantis which will now run a branch pipeline. This in reality is not a big deal. The problem happens when you revert the setting above
> 
> When you revert it, you would expect the normal behavior (both Atlantis and the project pipeline in the same line) but in reality what happens is that the behavior above is still kept (2 separate pipelines)
> 
> From what I could understand the issue is with this bit of code which gets the head pipeline from the MR
> 
> ```
> mr, err := g.GetMergeRequest(pull.BaseRepo.FullName, pull.Num)
> if err != nil {
> 	return err
> }
> ```
> 
> The issue is that the api call above will return the Atlantis pipeline as being the head pipeline and `mr.Pipeline.Source` will be `external`
> 
> From that moment on, unless you trigger a new pipeline for the project, the last pipeline will always be the one from Atlantis which is identified as `external` and the pipelines will never "merge"
> 
> I was also able to test that if the function is just
> 
> ```
> func (g *GitlabClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {
> 	gitlabState := gitlab.Pending
> 	switch state {
> 	case models.PendingCommitStatus:
> 		gitlabState = gitlab.Running
> 	case models.FailedCommitStatus:
> 		gitlabState = gitlab.Failed
> 	case models.SuccessCommitStatus:
> 		gitlabState = gitlab.Success
> 	}
> 
> 	options := &gitlab.SetCommitStatusOptions{
> 		State:       gitlabState,
> 		Context:     gitlab.String(src),
> 		Description: gitlab.String(description),
> 		TargetURL:   &url,
> 	}
> 
> 	_, _, err := g.Client.Commits.SetCommitStatus(repo.FullName, pull.HeadCommit, options)
> 
> 	return err
> }
> ```
> 
> Deliberately not passing `refTarget` seems to work for all cases and I guess Gitlab will always try to see what use-case it fits best? 🤷‍♂️
> 
> I don't have any use-case or example of when an event is anything else than a merge_request_event to test and would be cool if someone could present some examples of the pipelines that originally were being duplicated and that got solved with the MR above.
> 
> I also noticed that (at least for Gitlab) the supported Webhook events are `Comments` and `Merge Request events` and I am wondering if all the logic from the MR is ever going to be needed.
> 
> **Thank you in advance for your time and help**, and happy to propose an MR with any sort of fix but just want to better understand first the motivation for the code above and how we can make it work for all use-cases 😃

Sorry for the very late response, guys.

Removing the refTarget from setCommitStatus will work for Merge Result pipelines, but the behavior described in https://github.com/runatlantis/atlantis/issues/2484 will occur again. Without the ref from head_pipeline, when a user checkout from a branch, the CommitStatus will be issued to the HEAD branch.

Also, I think that the duplicated pipeline problem not solved by the mentioned PRs is the race condition of GitLab CI Pipeline creation x Atlantis Webhook process, sometimes Atlantis will be faster than GitLab Pipeline creation, so the CommitStatus will be set in a different Pipeline ID.
There is a discussion about it in https://github.com/runatlantis/atlantis/pull/3887
@X-Guardian @lukemassa 
@michelmzs thank you very much for helping understand the issue

We will do some tests and hopefully we will be able to overcome the issue. We also had the oportunity to update our Atlantis to one of the latest versions and try it out

Thanks once again!
I assume this is related to our issue: https://github.com/runatlantis/atlantis/issues/4615

Fix patch:
diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go
index 16ffa38421..289b1a0d8a 100644
--- a/server/events/vcs/gitlab_client.go
+++ b/server/events/vcs/gitlab_client.go
@@ -22,14 +22,14 @@ import (
 	"strings"
 	"time"
 
-	version "github.com/hashicorp/go-version"
+	"github.com/hashicorp/go-version"
 	"github.com/jpillora/backoff"
 	"github.com/pkg/errors"
 	"github.com/runatlantis/atlantis/server/events/command"
 	"github.com/runatlantis/atlantis/server/events/models"
 	"github.com/runatlantis/atlantis/server/events/vcs/common"
 	"github.com/runatlantis/atlantis/server/logging"
-	gitlab "github.com/xanzy/go-gitlab"
+	"github.com/xanzy/go-gitlab"
 )
 
 // gitlabMaxCommentLength is the maximum number of chars allowed by Gitlab in a
@@ -406,17 +406,17 @@ func (g *GitlabClient) UpdateStatus(logger logging.SimpleLogging, repo models.Re
 		gitlabState = gitlab.Success
 	}
 
-	// refTarget is set to the head pipeline of the MR if it exists, or else it is set to the head branch
-	// of the MR. This is needed because the commit status is only shown in the MR if the pipeline is
-	// assigned to an MR reference.
-	// Try to get the MR details a couple of times in case the pipeline is not yet assigned to the MR
-	refTarget := pull.HeadBranch
+	// refTarget is only set to the head branch of the MR if HeadPipeline is not found
+	// when HeadPipeline is found we set the pipelineID for the request instead
+	var refTarget *string
+	var pipelineID *int
 
 	retries := 1
 	delay := 2 * time.Second
 	var mr *gitlab.MergeRequest
 	var err error
 
+	// Try to get the MR details a couple of times in case the pipeline is not yet assigned to the MR
 	for i := 0; i <= retries; i++ {
 		mr, err = g.GetMergeRequest(logger, pull.BaseRepo.FullName, pull.Num)
 		if err != nil {
@@ -425,7 +425,8 @@ func (g *GitlabClient) UpdateStatus(logger logging.SimpleLogging, repo models.Re
 		if mr.HeadPipeline != nil {
 			logger.Debug("Head pipeline found for merge request %d, source '%s'. refTarget '%s'",
 				pull.Num, mr.HeadPipeline.Source, mr.HeadPipeline.Ref)
-			refTarget = mr.HeadPipeline.Ref
+			// set pipeline ID for the req once found
+			pipelineID = gitlab.Ptr(mr.HeadPipeline.ID)
 			break
 		}
 		if i != retries {
@@ -433,6 +434,8 @@ func (g *GitlabClient) UpdateStatus(logger logging.SimpleLogging, repo models.Re
 				pull.Num, delay)
 			time.Sleep(delay)
 		} else {
+			// set the ref target here if the pipeline wasn't found
+			refTarget = gitlab.Ptr(pull.HeadBranch)
 			logger.Debug("Head pipeline not found for merge request %d.",
 				pull.Num)
 		}
@@ -461,7 +464,9 @@ func (g *GitlabClient) UpdateStatus(logger logging.SimpleLogging, repo models.Re
 			Context:     gitlab.Ptr(src),
 			Description: gitlab.Ptr(description),
 			TargetURL:   &url,
-			Ref:         gitlab.Ptr(refTarget),
+			// only one of these should get sent in the request
+			PipelineID: pipelineID,
+			Ref:        refTarget,
 		})
 
 		if resp != nil {


Test patch:
diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go
index a32a75d74a..cef424bcb0 100644
--- a/server/events/vcs/gitlab_client_test.go
+++ b/server/events/vcs/gitlab_client_test.go
@@ -12,17 +12,19 @@ import (
 	"testing"
 	"time"
 
-	version "github.com/hashicorp/go-version"
+	"github.com/hashicorp/go-version"
 	"github.com/runatlantis/atlantis/server/events/command"
 	"github.com/runatlantis/atlantis/server/events/models"
 	"github.com/runatlantis/atlantis/server/logging"
-	gitlab "github.com/xanzy/go-gitlab"
+	"github.com/xanzy/go-gitlab"
 
 	. "github.com/runatlantis/atlantis/testing"
 )
 
 var projectID = 4580910
 
+const gitlabPipelineSuccessMrID = 488598
+
 // Test that the base url gets set properly.
 func TestNewGitlabClient_BaseURL(t *testing.T) {
 	gitlabClientUnderTest = true
@@ -302,7 +304,7 @@ func TestGitlabClient_UpdateStatus(t *testing.T) {
 
 						body, err := io.ReadAll(r.Body)
 						Ok(t, err)
-						exp := fmt.Sprintf(`{"state":"%s","ref":"patch-1-merger","context":"src","target_url":"https://google.com","description":"description"}`, c.expState)
+						exp := fmt.Sprintf(`{"state":"%s","context":"src","target_url":"https://google.com","description":"description","pipeline_id":%d}`, c.expState, gitlabPipelineSuccessMrID)
 						Equals(t, exp, string(body))
 						defer r.Body.Close()  // nolint: errcheck
 						w.Write([]byte("{}")) // nolint: errcheck
@@ -393,7 +395,7 @@ func TestGitlabClient_UpdateStatusRetryable(t *testing.T) {
 
 						body, err := io.ReadAll(r.Body)
 						Ok(t, err)
-						exp := fmt.Sprintf(`{"state":"%s","ref":"patch-1-merger","context":"src","target_url":"https://google.com","description":"description"}`, c.expState)
+						exp := fmt.Sprintf(`{"state":"%s","context":"src","target_url":"https://google.com","description":"description","pipeline_id":%d}`, c.expState, gitlabPipelineSuccessMrID)
 						Equals(t, exp, string(body))
 						defer r.Body.Close() // nolint: errcheck