Repository: mc1arke/sonarqube-community-branch-plugin (2026-03-12T08:07:04Z)

Issue:
Azure DevOps PR status only works if disabled "Reset status whenever there are new changes"
**Describe the bug**

Azure DevOps PR status only works if disabled "Reset status whenever there are new changes".

**To Reproduce**

In Azure DevOps when adding a status policy for SonarQube and enabling "Reset status whenever there are new changes", would leave the status check in the PR wait forever since the request we then need to send should be different than the current implementation.

<img width="349" height="60" alt="Image" src="https://github.com/user-attachments/assets/0ad7a9fd-8c18-47ad-9c6d-38e76688d45c" />

The current request this plugin sends to Azure DevOps results in a 403:
```
{"$id":"1","innerException":null,"message":"Pull request status rejected by status policy. Policy configuration requires status to be posted on iteration","typeName":"Microsoft.TeamFoundation.Git.Server.GitPullRequestStatusRejectedByPolicyException, Microsoft.TeamFoundation.Git.Server","typeKey":"GitPullRequestStatusRejectedByPolicyException","errorCode":0,"eventId":3000}
```

**Expected behavior**

Send a request that works both when "Reset status whenever there are new changes" is enabled and when "Reset status whenever there are new changes" is disabled.

**Software Versions**
 - SonarQube Version: v26.2.0.119303
 - Plugin Version: 26.2.0



Discussion:
Release in 26.3.0 of the plugin



Patch:
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsClient.java
index 6195ab302..a3d696bfa 100644
--- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsClient.java
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021-2024 Michael Clarke
+ * Copyright (C) 2021-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -34,6 +34,8 @@ public interface AzureDevopsClient {
 
     PullRequest retrievePullRequest(String projectName, String repositoryName, int pullRequestId) throws IOException;
 
+    int retrievePullRequestIterationIdForCommit(String projectName, String repositoryName, int pullRequestId, String commitSha) throws IOException;
+
     List<Commit> getPullRequestCommits(String projectName, String repositoryName, int pullRequestId) throws IOException;
 
     List<CommentThread> retrieveThreads(String projectName, String repositoryName, int pullRequestId) throws IOException;
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClient.java
index a7c8db35a..c26b9aa86 100644
--- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClient.java
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021-2024 Michael Clarke
+ * Copyright (C) 2021-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -28,6 +28,8 @@
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.CreateCommentThreadRequest;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.GitPullRequestStatus;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequest;
+import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequestIteration;
+import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequestIterationList;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Repository;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.UpdateCommentThreadStatusRequest;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.enums.CommentThreadStatus;
@@ -119,6 +121,17 @@ public PullRequest retrievePullRequest(String projectId, String repositoryName,
         return execute(url, "get", null, PullRequest.class);
     }
 
+    @Override
+    public int retrievePullRequestIterationIdForCommit(String projectId, String repositoryName, int pullRequestId, String commitSha) throws IOException {
+        String url = String.format("%s/%s/_apis/git/repositories/%s/pullRequests/%s/iterations?includeCommits=true&api-version=%s", apiUrl, encode(projectId), encode(repositoryName), pullRequestId, API_VERSION);
+        PullRequestIterationList iterationList = Objects.requireNonNull(execute(url, "get", null, PullRequestIterationList.class));
+        return iterationList.getValue().stream()
+                .filter(iteration -> iteration.getSourceRefCommit() != null && commitSha.equals(iteration.getSourceRefCommit().getCommitId()))
+                .mapToInt(PullRequestIteration::getId)
+                .findFirst()
+                .orElse(1);
+    }
+
     @Override
     public List<Commit> getPullRequestCommits(String projectId, String repositoryName, int pullRequestId) throws IOException {
         String url = String.format("%s/%s/_apis/git/repositories/%s/pullRequests/%s/commits?api-version=%s", apiUrl, encode(projectId), encode(repositoryName), pullRequestId, API_VERSION);
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/GitPullRequestStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/GitPullRequestStatus.java
index 777fc2eed..f2276b1ac 100644
--- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/GitPullRequestStatus.java
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/GitPullRequestStatus.java
@@ -1,3 +1,21 @@
+/*
+ * Copyright (C) 2020-2026 Michael Clarke, Sebastiaan Speck
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
 package com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
@@ -5,22 +23,25 @@
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.enums.GitStatusState;
 
 public class GitPullRequestStatus {
-    
+
     private final GitStatusState state;
     private final String description;
     private final GitStatusContext context;
     private final String targetUrl;
+    private final int iterationId;
 
     @JsonCreator
     public GitPullRequestStatus(
             @JsonProperty("state") GitStatusState state,
             @JsonProperty("description") String description,
             @JsonProperty("context") GitStatusContext context,
-            @JsonProperty("targetUrl") String targetUrl) {
+            @JsonProperty("targetUrl") String targetUrl,
+            @JsonProperty("iterationId") int iterationId) {
         this.state = state;
         this.description = description;
         this.context = context;
         this.targetUrl = targetUrl;
+        this.iterationId = iterationId;
     }
 
     /**
@@ -29,7 +50,7 @@ public GitPullRequestStatus(
     public GitStatusState getState() {
         return this.state;
     }
-    
+
     /**
      * Description of the status
      */
@@ -50,4 +71,11 @@ public GitStatusContext getContext() {
     public String getTargetUrl() {
         return this.targetUrl;
     }
+
+    /**
+     * ID of the iteration to associate status with. Minimum value is 1.
+     */
+    public int getIterationId() {
+        return this.iterationId;
+    }
 }
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequest.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequest.java
index 817e769e9..994075150 100644
--- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequest.java
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 Michael Clarke
+ * Copyright (C) 2021-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -25,11 +25,14 @@ public class PullRequest {
 
     private final int id;
     private final Repository repository;
+    private final boolean supportsIterations;
 
     @JsonCreator
-    public PullRequest(@JsonProperty("pullRequestId") int id, @JsonProperty("repository") Repository repository) {
+    public PullRequest(@JsonProperty("pullRequestId") int id, @JsonProperty("repository") Repository repository,
+                       @JsonProperty("supportsIterations") boolean supportsIterations) {
         this.id = id;
         this.repository = repository;
+        this.supportsIterations = supportsIterations;
     }
 
     public int getId() {
@@ -39,4 +42,8 @@ public int getId() {
     public Repository getRepository() {
         return repository;
     }
+
+    public boolean doesSupportIterations() {
+        return supportsIterations;
+    }
 }
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIteration.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIteration.java
new file mode 100644
index 000000000..26e41be39
--- /dev/null
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIteration.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2026 Sebastiaan Speck
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+package com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PullRequestIteration {
+
+    private final int id;
+    private final Commit sourceRefCommit;
+
+    @JsonCreator
+    public PullRequestIteration(@JsonProperty("id") int id, @JsonProperty("sourceRefCommit") Commit sourceRefCommit) {
+        this.id = id;
+        this.sourceRefCommit = sourceRefCommit;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public Commit getSourceRefCommit() {
+        return sourceRefCommit;
+    }
+}
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIterationList.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIterationList.java
new file mode 100644
index 000000000..193c9d646
--- /dev/null
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/model/PullRequestIterationList.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2026 Sebastiaan Speck
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+package com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class PullRequestIterationList {
+
+    private final List<PullRequestIteration> value;
+
+    @JsonCreator
+    public PullRequestIterationList(@JsonProperty("value") List<PullRequestIteration> value) {
+        this.value = value;
+    }
+
+    public List<PullRequestIteration> getValue() {
+        return value;
+    }
+}
diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java
index 9f22538cd..c9345db73 100644
--- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java
+++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2026 Markus Heberling, Michael Clarke
+ * Copyright (C) 2020-2026 Markus Heberling, Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -145,16 +145,25 @@ protected List<String> getCommitIdsForPullRequest(AzureDevopsClient client, Pull
     @Override
     protected void submitPipelineStatus(AzureDevopsClient client, PullRequest pullRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary, ProjectAlmSettingDto projectAlmSettingDto) {
         try {
+            String projectName = pullRequest.getRepository().getProject().getName();
+            String repositoryName = pullRequest.getRepository().getName();
+            int pullRequestId = pullRequest.getId();
+
+            int iterationId = pullRequest.doesSupportIterations()
+                    ? client.retrievePullRequestIterationIdForCommit(projectName, repositoryName, pullRequestId, analysis.getCommitSha())
+                    : 1;
+
             String description = analysis.getQualityGateStatus() == QualityGate.Status.OK ? "Quality Gate passed" : "Quality Gate failed";
 
             GitPullRequestStatus gitPullRequestStatus = new GitPullRequestStatus(
                     GitStatusStateMapper.toGitStatusState(analysis.getQualityGateStatus()),
                     description,
                     new GitStatusContext("SonarQube", "quality gate"),
-                    analysisSummary.getDashboardUrl()
+                    analysisSummary.getDashboardUrl(),
+                    iterationId
             );
 
-            client.submitPullRequestStatus(pullRequest.getRepository().getProject().getName(), pullRequest.getRepository().getName(), pullRequest.getId(), gitPullRequestStatus);
+            client.submitPullRequestStatus(projectName, repositoryName, pullRequestId, gitPullRequestStatus);
         } catch (IOException ex) {
             throw new IllegalStateException("Could not update pipeline status in Gitlab", ex);
         }


Test patch:
diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java
index 0a24d814a..4123fafe5 100644
--- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java
+++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021-2024 Michael Clarke
+ * Copyright (C) 2021-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -23,6 +23,8 @@
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.GitPullRequestStatus;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.GitStatusContext;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequest;
+import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequestIteration;
+import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequestIterationList;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.enums.GitStatusState;
 import org.apache.http.StatusLine;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -36,6 +38,7 @@
 import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -88,7 +91,7 @@ void checkSubmitPullRequestStatusSubmitsCorrectContent() throws IOException {
         when(closeableHttpClient.execute(any())).thenReturn(closeableHttpResponse);
         when(objectMapper.writeValueAsString(any())).thenReturn("json");
 
-        underTest.submitPullRequestStatus("project Id With Spaces", "repository Name With Spaces", 123, new GitPullRequestStatus(GitStatusState.SUCCEEDED, "description", new GitStatusContext("name", "genre"), "url"));
+        underTest.submitPullRequestStatus("project Id With Spaces", "repository Name With Spaces", 123, new GitPullRequestStatus(GitStatusState.SUCCEEDED, "description", new GitStatusContext("name", "genre"), "url", 2));
 
         ArgumentCaptor<HttpUriRequest> requestArgumentCaptor = ArgumentCaptor.captor();
         verify(closeableHttpClient).execute(requestArgumentCaptor.capture());
@@ -100,6 +103,55 @@ void checkSubmitPullRequestStatusSubmitsCorrectContent() throws IOException {
         assertThat(request.getEntity().getContent()).hasContent("json");
     }
 
+    @Test
+    void checkGetPullRequestIterationIdForCommitReturnsMatchingIteration() throws IOException {
+        AzureDevopsRestClient underTest = new AzureDevopsRestClient("http://url.test/api", "token", objectMapper, () -> closeableHttpClient);
+
+        CloseableHttpResponse closeableHttpResponse = mock();
+        StatusLine statusLine = mock();
+        when(statusLine.getStatusCode()).thenReturn(200);
+        when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine);
+        when(closeableHttpResponse.getEntity()).thenReturn(new StringEntity("content", StandardCharsets.UTF_8));
+        when(closeableHttpClient.execute(any())).thenReturn(closeableHttpResponse);
+        PullRequestIterationList iterationList = new PullRequestIterationList(List.of(
+                new PullRequestIteration(1, new com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Commit("sha-aaa")),
+                new PullRequestIteration(2, new com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Commit("sha-bbb")),
+                new PullRequestIteration(3, new com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Commit("sha-ccc"))));
+        when(objectMapper.readValue(any(String.class), eq(PullRequestIterationList.class))).thenReturn(iterationList);
+
+        int result = underTest.retrievePullRequestIterationIdForCommit("projectId", "repository Name", 123, "sha-bbb");
+
+        ArgumentCaptor<HttpUriRequest> requestArgumentCaptor = ArgumentCaptor.captor();
+        verify(closeableHttpClient).execute(requestArgumentCaptor.capture());
+
+        RequestBuilder request = RequestBuilder.copy(requestArgumentCaptor.getValue());
+
+        assertThat(request.getMethod()).isEqualTo("GET");
+        assertThat(request.getUri()).isEqualTo(URI.create("http://url.test/api/projectId/_apis/git/repositories/repository%20Name/pullRequests/123/iterations?includeCommits=true&api-version=4.1"));
+        assertThat(request.getEntity()).isNull();
+        assertThat(result).isEqualTo(2);
+    }
+
+    @Test
+    void checkGetPullRequestIterationIdForCommitReturnsOneWhenNoMatchFound() throws IOException {
+        AzureDevopsRestClient underTest = new AzureDevopsRestClient("http://url.test/api", "token", objectMapper, () -> closeableHttpClient);
+
+        CloseableHttpResponse closeableHttpResponse = mock();
+        StatusLine statusLine = mock();
+        when(statusLine.getStatusCode()).thenReturn(200);
+        when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine);
+        when(closeableHttpResponse.getEntity()).thenReturn(new StringEntity("content", StandardCharsets.UTF_8));
+        when(closeableHttpClient.execute(any())).thenReturn(closeableHttpResponse);
+        PullRequestIterationList iterationList = new PullRequestIterationList(List.of(
+                new PullRequestIteration(1, new com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Commit("sha-aaa")),
+                new PullRequestIteration(2, new com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Commit("sha-bbb"))));
+        when(objectMapper.readValue(any(String.class), eq(PullRequestIterationList.class))).thenReturn(iterationList);
+
+        int result = underTest.retrievePullRequestIterationIdForCommit("projectId", "repository Name", 123, "sha-unknown");
+
+        assertThat(result).isEqualTo(1);
+    }
+
     @Test
     void checkAddCommentToThreadSubmitsCorrectContent() throws IOException {
         AzureDevopsRestClient underTest = new AzureDevopsRestClient("http://test.url", "authToken", objectMapper, () -> closeableHttpClient);
diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorIntegrationTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorIntegrationTest.java
index e49e72876..acecd7023 100644
--- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorIntegrationTest.java
+++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorIntegrationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 Michael Clarke
+ * Copyright (C) 2024-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -394,7 +394,8 @@ private void setupStubs() {
                         "\"state\":\"SUCCEEDED\"," +
                         "\"description\":\"Quality Gate passed\"," +
                         "\"context\":{\"genre\":\"SonarQube\",\"name\":\"quality gate\"}," +
-                        "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" +
+                        "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"," +
+                        "\"iterationId\":1" +
                         "}")
                 )
                 .willReturn(ok()));
@@ -532,6 +533,18 @@ private void setupStubs() {
                         "    \"emailAddress\": \"test.user@mail.domain\"" + System.lineSeparator() +
                         "  }" + System.lineSeparator() +
                         "}")));
+
+        wireMockExtension.stubFor(get(urlEqualTo("/azure%20Project/_apis/git/repositories/my%20Repository/pullRequests/" + pullRequestId + "/iterations?includeCommits=true&api-version=4.1"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Authorization", equalTo(authHeader))
+                .willReturn(aResponse().withStatus(200).withBody("{" + System.lineSeparator() +
+                        "    \"value\": [" + System.lineSeparator() +
+                        "        {" + System.lineSeparator() +
+                        "            \"id\": 1" + System.lineSeparator() +
+                        "        }" + System.lineSeparator() +
+                        "    ]," + System.lineSeparator() +
+                        "    \"count\": 1" + System.lineSeparator() +
+                        "}")));
     }
 
     @Test
diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java
index 0e6d4ff5e..e76ae14ad 100644
--- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java
+++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024-2025 Michael Clarke
+ * Copyright (C) 2024-2026 Michael Clarke, Sebastiaan Speck
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -46,6 +46,7 @@
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.CommentThread;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.ConnectionData;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.CreateCommentRequest;
+import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.GitPullRequestStatus;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.IdentityRef;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Project;
 import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequest;
@@ -66,6 +67,20 @@ class AzureDevOpsPullRequestDecoratorTest {
     private final ReportGenerator reportGenerator = mock();
     private final MarkdownFormatterFactory markdownFormatterFactory = mock();
 
+    private PullRequest mockPullRequest(AzureDevopsClient azureDevopsClient, String azureProject, String azureRepository, int pullRequestId) throws IOException {
+        PullRequest pullRequest = mock();
+        when(pullRequest.getId()).thenReturn(pullRequestId);
+        when(pullRequest.doesSupportIterations()).thenReturn(true);
+        when(azureDevopsClient.retrievePullRequest(any(), any(), anyInt())).thenReturn(pullRequest);
+        Repository repository = mock();
+        Project project = mock();
+        when(pullRequest.getRepository()).thenReturn(repository);
+        when(repository.getProject()).thenReturn(project);
+        when(project.getName()).thenReturn(azureProject);
+        when(repository.getRemoteUrl()).thenReturn("https://remote.url/path/to/repo");
+        when(repository.getName()).thenReturn(azureRepository);
+        return pullRequest;
+    }
 
     @Test
     void testDecorateQualityGateRepoSlugException() {
@@ -413,4 +428,101 @@ void shouldNotTryAndCleanupNonSummaryNote() throws IOException {
         verify(azureDevopsClient).retrieveThreads(azureProject, azureRepository, pullRequestId);
     }
 
+    @Test
+    void shouldSubmitPullRequestStatusWithIterationIdForCommit() throws IOException {
+        String azureProject = "azure-project";
+        String azureRepository = "azure-repo";
+        int pullRequestId = 321;
+        int iterationId = 2;
+        String commitSha = "abc123def456";
+
+        when(analysisDetails.getPullRequestId()).thenReturn(Integer.toString(pullRequestId));
+        when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK);
+        when(analysisDetails.getCommitSha()).thenReturn(commitSha);
+        when(projectAlmSettingDto.getAlmSlug()).thenReturn(azureProject);
+        when(projectAlmSettingDto.getAlmRepo()).thenReturn(azureRepository);
+        when(projectAlmSettingDto.getInlineAnnotationsEnabled()).thenReturn(false);
+
+        AnalysisSummary analysisSummary = mock();
+        when(analysisSummary.getDashboardUrl()).thenReturn("http://sonar/dashboard");
+        when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary);
+
+        AzureDevopsClient azureDevopsClient = mock();
+        when(azureDevopsClientFactory.createClient(any(), any())).thenReturn(azureDevopsClient);
+        mockPullRequest(azureDevopsClient, azureProject, azureRepository, pullRequestId);
+
+        when(azureDevopsClient.retrievePullRequestIterationIdForCommit(azureProject, azureRepository, pullRequestId, commitSha)).thenReturn(iterationId);
+        when(azureDevopsClient.createThread(any(), any(), anyInt(), any())).thenReturn(mock());
+
+        AzureDevOpsPullRequestDecorator underTest = new AzureDevOpsPullRequestDecorator(scmInfoRepository, azureDevopsClientFactory, reportGenerator, markdownFormatterFactory);
+        underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto);
+
+        ArgumentCaptor<GitPullRequestStatus> statusCaptor = ArgumentCaptor.captor();
+        verify(azureDevopsClient).submitPullRequestStatus(eq(azureProject), eq(azureRepository), eq(pullRequestId), statusCaptor.capture());
+        assertThat(statusCaptor.getValue().getIterationId()).isEqualTo(iterationId);
+    }
+
+    @Test
+    void shouldThrowIfIterationIdForCommitCallFails() throws IOException {
+        String azureProject = "azure-project";
+        String azureRepository = "azure-repo";
+        int pullRequestId = 321;
+
+        when(analysisDetails.getPullRequestId()).thenReturn(Integer.toString(pullRequestId));
+        when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK);
+        when(analysisDetails.getCommitSha()).thenReturn("abc123");
+        when(projectAlmSettingDto.getAlmSlug()).thenReturn(azureProject);
+        when(projectAlmSettingDto.getAlmRepo()).thenReturn(azureRepository);
+        when(projectAlmSettingDto.getInlineAnnotationsEnabled()).thenReturn(false);
+
+        AnalysisSummary analysisSummary = mock();
+        when(analysisSummary.getDashboardUrl()).thenReturn("http://sonar/dashboard");
+        when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary);
+
+        AzureDevopsClient azureDevopsClient = mock();
+        when(azureDevopsClientFactory.createClient(any(), any())).thenReturn(azureDevopsClient);
+        mockPullRequest(azureDevopsClient, azureProject, azureRepository, pullRequestId);
+
+        when(azureDevopsClient.retrievePullRequestIterationIdForCommit(any(), any(), anyInt(), any())).thenThrow(new IOException("403 Forbidden"));
+        when(azureDevopsClient.createThread(any(), any(), anyInt(), any())).thenReturn(mock());
+
+        AzureDevOpsPullRequestDecorator underTest = new AzureDevOpsPullRequestDecorator(scmInfoRepository, azureDevopsClientFactory, reportGenerator, markdownFormatterFactory);
+
+        assertThatThrownBy(() -> underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto))
+                .isExactlyInstanceOf(IllegalStateException.class)
+                .hasMessage("Could not update pipeline status in Gitlab");
+
+        verify(azureDevopsClient, never()).submitPullRequestStatus(any(), any(), anyInt(), any());
+    }
+
+    @Test
+    void shouldUseIterationIdOneWhenPullRequestDoesNotSupportIterations() throws IOException {
+        String azureProject = "azure-project";
+        String azureRepository = "azure-repo";
+        int pullRequestId = 321;
+
+        when(analysisDetails.getPullRequestId()).thenReturn(Integer.toString(pullRequestId));
+        when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK);
+        when(projectAlmSettingDto.getAlmSlug()).thenReturn(azureProject);
+        when(projectAlmSettingDto.getAlmRepo()).thenReturn(azureRepository);
+        when(projectAlmSettingDto.getInlineAnnotationsEnabled()).thenReturn(false);
+
+        AnalysisSummary analysisSummary = mock();
+        when(analysisSummary.getDashboardUrl()).thenReturn("http://sonar/dashboard");
+        when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary);
+
+        AzureDevopsClient azureDevopsClient = mock();
+        when(azureDevopsClientFactory.createClient(any(), any())).thenReturn(azureDevopsClient);
+        PullRequest pullRequest = mockPullRequest(azureDevopsClient, azureProject, azureRepository, pullRequestId);
+        when(pullRequest.doesSupportIterations()).thenReturn(false);
+        when(azureDevopsClient.createThread(any(), any(), anyInt(), any())).thenReturn(mock());
+
+        AzureDevOpsPullRequestDecorator underTest = new AzureDevOpsPullRequestDecorator(scmInfoRepository, azureDevopsClientFactory, reportGenerator, markdownFormatterFactory);
+        underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto);
+
+        ArgumentCaptor<GitPullRequestStatus> statusCaptor = ArgumentCaptor.captor();
+        verify(azureDevopsClient).submitPullRequestStatus(eq(azureProject), eq(azureRepository), eq(pullRequestId), statusCaptor.capture());
+        assertThat(statusCaptor.getValue().getIterationId()).isEqualTo(1);
+        verify(azureDevopsClient, never()).retrievePullRequestIterationIdForCommit(any(), any(), anyInt(), any());
+    }
 }