From 76068f845fd366d0a9a6e40d206c37ad8b19a6d8 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 22 Apr 2026 21:23:19 +0200 Subject: [PATCH 01/13] [devops] Fix PR latest-commit detection Treat PR merge refs as PR builds regardless of the reported build reason, and consider synthetic merge commits current when one of their parents matches the PR head. Also pass the checkout template's isPR flag correctly so the fixed commit hash is used for PR comment handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../automation/scripts/GitHub.Tests.ps1 | 91 ++++++++++++++ tools/devops/automation/scripts/GitHub.psm1 | 119 ++++++++++++++++-- .../automation/templates/common/checkout.yml | 2 +- 3 files changed, 201 insertions(+), 11 deletions(-) diff --git a/tools/devops/automation/scripts/GitHub.Tests.ps1 b/tools/devops/automation/scripts/GitHub.Tests.ps1 index ea6b26f8c2cf..ac74796090d1 100644 --- a/tools/devops/automation/scripts/GitHub.Tests.ps1 +++ b/tools/devops/automation/scripts/GitHub.Tests.ps1 @@ -257,8 +257,17 @@ Describe 'IsCurrentCommitLatestInPR' { "head" = @{ "sha" = "different123hash" } + "base" = @{ + "ref" = "main" + } } } -ModuleName 'GitHub' + Mock Test-GitIsAncestor { + return $false + } -ModuleName 'GitHub' + Mock Get-GitCommitParents { + return @("basebranch123") + } -ModuleName 'GitHub' $result = Get-IsCurrentCommitLatestInPR -Org "testorg" -Repo "testrepo" -Token "test-token" -Hash "abc123def456" -PrIDs @("123") $result | Should -Be $false @@ -276,5 +285,87 @@ Describe 'IsCurrentCommitLatestInPR' { $result = Get-IsCurrentCommitLatestInPR -Org "testorg" -Repo "testrepo" -Token "test-token" -Hash "abc123def456" -PrIDs @() # Empty array means not in PR $result | Should -Be $true } + + It 'returns true when the current commit is a synthetic merge commit for the latest PR head' { + Mock Invoke-Request { + return @{ + "head" = @{ + "sha" = "abc123def456" + } + "base" = @{ + "ref" = "main" + } + } + } -ModuleName 'GitHub' + Mock Test-GitIsAncestor { + return $false + } -ModuleName 'GitHub' + Mock Get-GitCommitParents { + return @("basebranch123", "abc123def456") + } -ModuleName 'GitHub' + + $result = Get-IsCurrentCommitLatestInPR -Org "testorg" -Repo "testrepo" -Token "test-token" -Hash "merge123456" -PrIDs @("123") + $result | Should -Be $true + } + + It 'returns false when commit is in the base branch despite having the PR head as a parent' { + Mock Invoke-Request { + return @{ + "head" = @{ + "sha" = "prhead123" + } + "base" = @{ + "ref" = "main" + } + } + } -ModuleName 'GitHub' + Mock Test-GitIsAncestor { + param ($Commit, $Branch) + # The commit is in the base branch + return $Branch -eq "origin/main" + } -ModuleName 'GitHub' + Mock Get-GitCommitParents { + return @("prhead123", "someothercommit") + } -ModuleName 'GitHub' + + $result = Get-IsCurrentCommitLatestInPR -Org "testorg" -Repo "testrepo" -Token "test-token" -Hash "mergecommit456" -PrIDs @("123") + $result | Should -Be $false + } + + It 'returns false when commit is in the PR branch despite having the PR head as a parent' { + Mock Invoke-Request { + return @{ + "head" = @{ + "sha" = "prhead123" + } + "base" = @{ + "ref" = "main" + } + } + } -ModuleName 'GitHub' + Mock Test-GitIsAncestor { + param ($Commit, $Branch) + # The commit is in the PR branch (ancestor of PR head) + return $Branch -eq "prhead123" + } -ModuleName 'GitHub' + Mock Get-GitCommitParents { + return @("prhead123", "someothercommit") + } -ModuleName 'GitHub' + + $result = Get-IsCurrentCommitLatestInPR -Org "testorg" -Repo "testrepo" -Token "test-token" -Hash "mergecommit456" -PrIDs @("123") + $result | Should -Be $false + } + } +} + +Describe 'IsPR' { + It 'returns true for manual builds that use a PR merge ref' { + Set-Item -Path "Env:BUILD_REASON" -Value "Manual" + Set-Item -Path "Env:BUILD_SOURCEBRANCH" -Value "refs/pull/123/merge" + + $githubComments = New-GitHubCommentsObject -Org "testorg" -Repo "testrepo" -Token "test-token" + + $githubComments.IsPR() | Should -Be $true + $githubComments.PRIds | Should -Contain "123" } } diff --git a/tools/devops/automation/scripts/GitHub.psm1 b/tools/devops/automation/scripts/GitHub.psm1 index 5f8ad2bc9190..3d866a38cfeb 100644 --- a/tools/devops/automation/scripts/GitHub.psm1 +++ b/tools/devops/automation/scripts/GitHub.psm1 @@ -41,6 +41,41 @@ function Invoke-Request { } while ($true) } +function Get-GitCommitParents { + param ( + [ValidateNotNullOrEmpty ()] + [string] + $Commit + ) + + $output = & git rev-list --parents -n 1 -- $Commit 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($output)) { + throw [System.InvalidOperationException]::new("Failed to get parent commits for '$Commit'.") + } + + $commits = $output.Trim() -split '\s+' + if ($commits.Length -le 1) { + return @() + } + + return @($commits[1..($commits.Length - 1)]) +} + +function Test-GitIsAncestor { + param ( + [ValidateNotNullOrEmpty ()] + [string] + $Commit, + + [ValidateNotNullOrEmpty ()] + [string] + $Branch + ) + + & git merge-base --is-ancestor $Commit $Branch 2>$null + return $LASTEXITCODE -eq 0 +} + class GitHubStatus { [ValidateNotNullOrEmpty ()] [string] $Status [ValidateNotNullOrEmpty ()] [string] $Description @@ -228,6 +263,8 @@ class GitHubComments { [ValidateNotNullOrEmpty ()][string] $Token [string] $Hash [string[]] $PRIds + [bool] $CurrentCommitIsLatestInPR + [bool] $CurrentCommitIsLatestInPRCalculated hidden static [string] $GitHubGraphQLEndpoint = "https://api.github.com/graphql" GitHubComments ( @@ -240,6 +277,8 @@ class GitHubComments { $this.Token = $githubToken $this.Hash = $null $this.PRIds = [string[]]@() + $this.CurrentCommitIsLatestInPR = $false + $this.CurrentCommitIsLatestInPRCalculated = $false } GitHubComments ( @@ -253,6 +292,8 @@ class GitHubComments { $this.Token = $githubToken $this.Hash = $hash $this.PRIds = Get-GitHubPRsForHash -Org $githubOrg -Repo $githubRepo -Token $githubToken -Hash $hash + $this.CurrentCommitIsLatestInPR = $false + $this.CurrentCommitIsLatestInPRCalculated = $false } [bool] IsPR() { @@ -268,13 +309,12 @@ class GitHubComments { return $true; } - if (($Env:BUILD_REASON -eq "ResourceTrigger")) { - $sourceBranch = $Env:BUILD_SOURCEBRANCH - if ($sourceBranch.StartsWith("refs/pull/") -and $sourceBranch.EndsWith("/merge")) { - # Set the PRs parsing the source branch - $this.PRIds = @($sourceBranch.Replace("refs/pull/", "").Replace("/merge", "")) - return $true - } + $sourceBranch = $Env:BUILD_SOURCEBRANCH + if ($sourceBranch -and $sourceBranch.StartsWith("refs/pull/") -and $sourceBranch.EndsWith("/merge")) { + # Some builds (such as pipeline-completion/manual follow-up jobs) still use PR merge refs + # even when BUILD_REASON is not "PullRequest". + $this.PRIds = @($sourceBranch.Replace("refs/pull/", "").Replace("/merge", "")) + return $true } return $false @@ -725,13 +765,21 @@ mutation { Also returns true if not in a PR context or if hash comparison cannot be performed. #> [bool] IsCurrentCommitLatestInPR() { + if ($this.CurrentCommitIsLatestInPRCalculated) { + return $this.CurrentCommitIsLatestInPR + } + # If we're not in a PR context, we can't determine this if (-not $this.IsPR()) { + $this.CurrentCommitIsLatestInPR = $true + $this.CurrentCommitIsLatestInPRCalculated = $true return $true } # If we don't have a hash to compare, assume it's latest if (-not $this.Hash) { + $this.CurrentCommitIsLatestInPR = $true + $this.CurrentCommitIsLatestInPRCalculated = $true return $true } @@ -747,14 +795,65 @@ mutation { $prInfo = Invoke-Request -Request { Invoke-RestMethod -Uri $url -Headers $headers -Method "GET" -ContentType 'application/json' } $latestCommit = $prInfo.head.sha - + Write-Host "Current commit: $($this.Hash)" Write-Host "Latest commit in PR #${prId}: $latestCommit" - - return $this.Hash -eq $latestCommit + + $hashesToCompare = [System.Collections.Generic.List[string]]::new() + $hashesToCompare.Add($this.Hash) + if ($Env:SYSTEM_PULLREQUEST_SOURCECOMMITID) { + $hashesToCompare.Add($Env:SYSTEM_PULLREQUEST_SOURCECOMMITID) + } + if ($Env:BUILD_SOURCEVERSION) { + $hashesToCompare.Add($Env:BUILD_SOURCEVERSION) + } + + foreach ($hash in ($hashesToCompare | Select-Object -Unique)) { + if ($hash -eq $latestCommit) { + Write-Host "Detected latest PR commit via hash comparison: $hash" + $this.CurrentCommitIsLatestInPR = $true + $this.CurrentCommitIsLatestInPRCalculated = $true + return $true + } + } + + # PR validation builds typically use a synthetic merge commit. If that's the hash we were + # given, accept it when one of its local git parents is the current PR head commit. + # However, skip this check if the commit is already in the base or PR branch - a commit + # that's in either branch is not a synthetic merge commit, and checking its parents could + # produce a false positive (e.g. a merge from the base branch into the PR branch). + $baseBranch = $prInfo.base.ref + $isInKnownBranch = $false + + if ($baseBranch -and (Test-GitIsAncestor -Commit $this.Hash -Branch "origin/$baseBranch")) { + Write-Host "Commit $($this.Hash) is in the base branch ($baseBranch), skipping synthetic merge check." + $isInKnownBranch = $true + } + + if (-not $isInKnownBranch -and (Test-GitIsAncestor -Commit $this.Hash -Branch $latestCommit)) { + Write-Host "Commit $($this.Hash) is in the PR branch, skipping synthetic merge check." + $isInKnownBranch = $true + } + + if (-not $isInKnownBranch) { + foreach ($parent in (Get-GitCommitParents -Commit $this.Hash)) { + if ($parent -eq $latestCommit) { + Write-Host "Detected latest PR commit via merge parent: $parent" + $this.CurrentCommitIsLatestInPR = $true + $this.CurrentCommitIsLatestInPRCalculated = $true + return $true + } + } + } + + $this.CurrentCommitIsLatestInPR = $false + $this.CurrentCommitIsLatestInPRCalculated = $true + return $false } catch { Write-Host "Error checking if current commit is latest in PR: $_" # On error, assume it's the latest to avoid hiding valid comments + $this.CurrentCommitIsLatestInPR = $true + $this.CurrentCommitIsLatestInPRCalculated = $true return $true } } diff --git a/tools/devops/automation/templates/common/checkout.yml b/tools/devops/automation/templates/common/checkout.yml index e19d8fddd6a3..ec054c523e92 100644 --- a/tools/devops/automation/templates/common/checkout.yml +++ b/tools/devops/automation/templates/common/checkout.yml @@ -55,7 +55,7 @@ steps: workingDirectory: $(System.DefaultWorkingDirectory)/$(BUILD_REPOSITORY_TITLE)/tools/devops/automation/scripts timeoutInMinutes: 15 env: - IS_PR: and(eq(parameters.isPR, 'true'), not(startsWith(variables['Build.SourceBranch'], 'refs/pull'))) + IS_PR: ${{ parameters.isPR }} - checkout: yaml-templates clean: true From 3cc6e0e212b9408d8215a13fb94af1cdad964005 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 23 Apr 2026 13:53:51 +0200 Subject: [PATCH 02/13] Test-GitIsAncestor: handle git error exit codes explicitly Distinguish exit code 1 (not an ancestor) from 128+ (git error such as missing refs in a shallow checkout). Throw on unexpected exit codes so the outer try/catch falls back to the safe path instead of silently returning false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/devops/automation/scripts/GitHub.psm1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/devops/automation/scripts/GitHub.psm1 b/tools/devops/automation/scripts/GitHub.psm1 index 3d866a38cfeb..82436b73540d 100644 --- a/tools/devops/automation/scripts/GitHub.psm1 +++ b/tools/devops/automation/scripts/GitHub.psm1 @@ -72,8 +72,12 @@ function Test-GitIsAncestor { $Branch ) - & git merge-base --is-ancestor $Commit $Branch 2>$null - return $LASTEXITCODE -eq 0 + & git merge-base --is-ancestor -- $Commit $Branch 2>$null + switch ($LASTEXITCODE) { + 0 { return $true } + 1 { return $false } + default { throw [System.InvalidOperationException]::new("Failed to determine whether '$Commit' is an ancestor of '$Branch'.") } + } } class GitHubStatus { From f91514b320baba31c55fc840e83bef9000c2c9f9 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 24 Apr 2026 09:28:22 +0200 Subject: [PATCH 03/13] [devops] Make the API diff pipeline use a pr: trigger. Unfortunately Azure DevOps doesn't properly report GitHub checks for pipelines triggered by another pipeline, when that other pipeline was triggered from a pr trigger. So go back to triggering the API diff pipeline using a pr: trigger. This effectively reverts #21880 ("[CI] Make the API diff be triggered as soon as the config of the build is done.") References: * https://stackoverflow.com/questions/78443654/reporting-stage-statuses-to-github-for-pipeline-triggered-by-another-pipeline --- tools/devops/automation/run-ci-api-diff.yml | 33 ++++++++++++++------- tools/devops/automation/run-pr-api-diff.yml | 26 +++++++++++----- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/tools/devops/automation/run-ci-api-diff.yml b/tools/devops/automation/run-ci-api-diff.yml index 8ccd511ab0c3..98a813647f23 100644 --- a/tools/devops/automation/run-ci-api-diff.yml +++ b/tools/devops/automation/run-ci-api-diff.yml @@ -1,16 +1,27 @@ # Pipeline that will calculate the api diff on a ci commit. -trigger: none -pr: none - -# we cannot use a template in a pipeline context -resources: - pipelines: - - pipeline: macios - source: \Xamarin\Mac-iOS\ci pipelines\xamarin-macios-ci - trigger: - stages: - - configure_build +trigger: + branches: + include: + - '*' + exclude: + - refs/heads/locfiles/* + - refs/heads/dev/* + - refs/heads/darc-* + - refs/heads/backport-pr-* + paths: + exclude: + - .github + - docs + - CODEOWNERS + - ISSUE_TEMPLATE.md + - LICENSE + - NOTICE.txt + - SECURITY.MD + - README.md + - src/README.md + - tools/mtouch/README.md + - msbuild/Xamarin.Localization.MSBuild/README.md extends: template: templates/pipelines/api-diff-pipeline.yml diff --git a/tools/devops/automation/run-pr-api-diff.yml b/tools/devops/automation/run-pr-api-diff.yml index 42924ca932a9..07401bdf077c 100644 --- a/tools/devops/automation/run-pr-api-diff.yml +++ b/tools/devops/automation/run-pr-api-diff.yml @@ -1,15 +1,25 @@ # Pipeline that will calculate the api diff on a pr commit. trigger: none -pr: none -resources: - pipelines: - - pipeline: macios - source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr - trigger: - stages: - - configure_build +pr: + autoCancel: true + branches: + include: + - '*' # yes, you do need the quote, * has meaning in yamls + paths: + exclude: + - .github + - docs + - CODEOWNERS + - ISSUE_TEMPLATE.md + - LICENSE + - NOTICE.txt + - SECURITY.MD + - README.md + - src/README.md + - tools/mtouch/README.md + - msbuild/Xamarin.Localization.MSBuild/README.md extends: template: templates/pipelines/api-diff-pipeline.yml From 7e5639dcb948bbfe426c39fd054022101f3133de Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 24 Apr 2026 09:33:42 +0200 Subject: [PATCH 04/13] Only the PR trigger. --- tools/devops/automation/run-ci-api-diff.yml | 33 +++++++-------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tools/devops/automation/run-ci-api-diff.yml b/tools/devops/automation/run-ci-api-diff.yml index 98a813647f23..8ccd511ab0c3 100644 --- a/tools/devops/automation/run-ci-api-diff.yml +++ b/tools/devops/automation/run-ci-api-diff.yml @@ -1,27 +1,16 @@ # Pipeline that will calculate the api diff on a ci commit. -trigger: - branches: - include: - - '*' - exclude: - - refs/heads/locfiles/* - - refs/heads/dev/* - - refs/heads/darc-* - - refs/heads/backport-pr-* - paths: - exclude: - - .github - - docs - - CODEOWNERS - - ISSUE_TEMPLATE.md - - LICENSE - - NOTICE.txt - - SECURITY.MD - - README.md - - src/README.md - - tools/mtouch/README.md - - msbuild/Xamarin.Localization.MSBuild/README.md +trigger: none +pr: none + +# we cannot use a template in a pipeline context +resources: + pipelines: + - pipeline: macios + source: \Xamarin\Mac-iOS\ci pipelines\xamarin-macios-ci + trigger: + stages: + - configure_build extends: template: templates/pipelines/api-diff-pipeline.yml From 36b134c779db63de5f356f7db0666a3db7627b4e Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 24 Apr 2026 09:28:22 +0200 Subject: [PATCH 05/13] [devops] Make the tests pipeline use a pr: trigger. Unfortunately Azure DevOps doesn't properly report GitHub checks for pipelines triggered by another pipeline, when that other pipeline was triggered from a pr trigger. So start triggering this pipeline using a pr: trigger. References: * https://stackoverflow.com/questions/78443654/reporting-stage-statuses-to-github-for-pipeline-triggered-by-another-pipeline --- .../automation/run-post-pr-build-tests.yml | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index 16bdd59d45bd..b85938c0fa6c 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -2,18 +2,28 @@ # This pipeline will execute the tests for the CI on PR as soon as the workloads have been complited. trigger: none -pr: none -# we cannot use a template in a pipeline context -resources: - pipelines: - - pipeline: macios - source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr - trigger: - stages: - - build_packages +pr: + autoCancel: true + branches: + include: + - '*' # yes, you do need the quote, * has meaning in yamls + paths: + exclude: + - .github + - docs + - CODEOWNERS + - ISSUE_TEMPLATE.md + - LICENSE + - NOTICE.txt + - SECURITY.MD + - README.md + - src/README.md + - tools/mtouch/README.md + - msbuild/Xamarin.Localization.MSBuild/README.md extends: template: templates/pipelines/run-tests-pipeline.yml parameters: isPR: true + pool: $(PRBuildPool) From 89c315bd6c829d5095a53739a883ab4f30c9c1f6 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 24 Apr 2026 17:25:52 +0200 Subject: [PATCH 06/13] Trigger build --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 0dcd1bcd2fa1..f60d19b26180 100644 --- a/Makefile +++ b/Makefile @@ -98,4 +98,3 @@ git-clean-all: @echo "Done" SUBDIRS += tests - From 376983c24198e7077d077be0aff5981b2a5363e2 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 10:42:50 +0200 Subject: [PATCH 07/13] [devops] Restore macios pipeline resource and remove invalid pool parameter Re-add the resources.pipelines declaration for the macios pipeline (without a trigger) because templates/common/load_configuration.yml uses 'download: macios' to fetch the build-configuration artifact. Remove the pool parameter since templates/pipelines/run-tests-pipeline.yml does not define one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/devops/automation/run-post-pr-build-tests.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index b85938c0fa6c..7e9b662583e1 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -22,8 +22,15 @@ pr: - tools/mtouch/README.md - msbuild/Xamarin.Localization.MSBuild/README.md +# we need to keep the pipeline resource declaration (without a trigger) +# because templates/common/load_configuration.yml uses 'download: macios' +# to fetch the build-configuration artifact. +resources: + pipelines: + - pipeline: macios + source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr + extends: template: templates/pipelines/run-tests-pipeline.yml parameters: isPR: true - pool: $(PRBuildPool) From 5cde94b3d65acee197f3d4a737bd95e1425f0c82 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 11:45:43 +0200 Subject: [PATCH 08/13] Try this --- tools/devops/automation/run-post-pr-build-tests.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index 7e9b662583e1..5fead91fead1 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -22,14 +22,6 @@ pr: - tools/mtouch/README.md - msbuild/Xamarin.Localization.MSBuild/README.md -# we need to keep the pipeline resource declaration (without a trigger) -# because templates/common/load_configuration.yml uses 'download: macios' -# to fetch the build-configuration artifact. -resources: - pipelines: - - pipeline: macios - source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr - extends: template: templates/pipelines/run-tests-pipeline.yml parameters: From 0e49b2853a5abc661601e9ed06a30aa94d62a3f7 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 11:55:04 +0200 Subject: [PATCH 09/13] Update comment. --- tools/devops/automation/run-post-pr-build-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index 5fead91fead1..0627031cd432 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -1,5 +1,5 @@ -# YAML pipeline for post build operations. -# This pipeline will execute the tests for the CI on PR as soon as the workloads have been complited. +# YAML pipeline for post build operations. +# This pipeline will build the workloads and then execute the tests for the CI on a PR. trigger: none From ebbbc166890b7bbace7307cfcf1f9beaf3a186da Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 12:18:59 +0200 Subject: [PATCH 10/13] Maybe it works now? --- tools/devops/automation/run-post-pr-build-tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index 0627031cd432..5f7313192145 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -22,6 +22,14 @@ pr: - tools/mtouch/README.md - msbuild/Xamarin.Localization.MSBuild/README.md +# we need to keep the pipeline resource declaration (without a trigger) +# because templates/common/load_configuration.yml uses 'download: macios' +# to fetch the build-configuration artifact. +resources: + pipelines: + - pipeline: macios + source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr + extends: template: templates/pipelines/run-tests-pipeline.yml parameters: From ff7def6ae723a4969174e6d929436565214293b0 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 13:07:07 +0200 Subject: [PATCH 11/13] [devops] Add build_packages stage to PR-triggered test pipeline When the test pipeline is triggered directly by a PR (not by a pipeline resource), artifacts like not-signed-package are not available because no preceding build produced them. Add a buildPackages parameter (default: false) to the test pipeline template. When true: - configure_build generates configuration from scratch (configure.yml) instead of downloading from a pipeline resource (load_configuration.yml) - A build_packages stage builds and publishes the required artifacts - Test stages depend on build_packages to download from the current run The existing behavior is preserved when buildPackages is false, keeping run-post-ci-build-tests.yml working unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../automation/run-post-pr-build-tests.yml | 9 +- .../automation/templates/common/configure.yml | 14 ++- .../pipelines/run-tests-pipeline.yml | 6 ++ .../automation/templates/tests-stage.yml | 87 ++++++++++++++++--- 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/tools/devops/automation/run-post-pr-build-tests.yml b/tools/devops/automation/run-post-pr-build-tests.yml index 5f7313192145..b83f2d8e95c3 100644 --- a/tools/devops/automation/run-post-pr-build-tests.yml +++ b/tools/devops/automation/run-post-pr-build-tests.yml @@ -22,15 +22,8 @@ pr: - tools/mtouch/README.md - msbuild/Xamarin.Localization.MSBuild/README.md -# we need to keep the pipeline resource declaration (without a trigger) -# because templates/common/load_configuration.yml uses 'download: macios' -# to fetch the build-configuration artifact. -resources: - pipelines: - - pipeline: macios - source: \Xamarin\Mac-iOS\pr pipelines\xamarin-macios-pr - extends: template: templates/pipelines/run-tests-pipeline.yml parameters: isPR: true + buildPackages: true diff --git a/tools/devops/automation/templates/common/configure.yml b/tools/devops/automation/templates/common/configure.yml index 4eef3b811f88..53f9561f456d 100644 --- a/tools/devops/automation/templates/common/configure.yml +++ b/tools/devops/automation/templates/common/configure.yml @@ -232,7 +232,19 @@ steps: $testMatrix = $testMatrix | ConvertFrom-Json | ConvertTo-Json -Compress # update the config file so that we do not recalculate the matrix in other pipelines Edit-BuildConfiguration -ConfigKey TEST_MATRIX -ConfigValue $testMatrix -ConfigFile $Env:CONFIG_PATH - #CONFIG_PATH + Write-Host "##vso[task.setvariable variable=TEST_MATRIX;isOutput=true]$testMatrix" + + # also compute the simulator test matrix and set it as output variable so that + # dependent stages in post-build test pipelines can consume it + $simulatorTestMatrix = Get-TestConfiguration ` + -TestConfigurations "$Env:TEST_CONFIGURATIONS" ` + -SupportedPlatforms "$Env:SUPPORTED_PLATFORMS" ` + -EnabledPlatforms "$Env:CONFIGURE_PLATFORMS_DOTNET_PLATFORMS" ` + -TestsLabels "${{ parameters.testsLabels }}" ` + -StatusContext "${{ parameters.statusContext }}" ` + -StageFilter "simulator_tests" ` + $simulatorTestMatrix = $simulatorTestMatrix | ConvertFrom-Json | ConvertTo-Json -Compress + Write-Host "##vso[task.setvariable variable=SIMULATOR_TEST_MATRIX;isOutput=true]$simulatorTestMatrix" name: test_matrix displayName: 'Create tests strategy matrix' env: diff --git a/tools/devops/automation/templates/pipelines/run-tests-pipeline.yml b/tools/devops/automation/templates/pipelines/run-tests-pipeline.yml index 2213a4425696..a3ef20748f63 100644 --- a/tools/devops/automation/templates/pipelines/run-tests-pipeline.yml +++ b/tools/devops/automation/templates/pipelines/run-tests-pipeline.yml @@ -31,6 +31,11 @@ parameters: type: boolean default: true + - name: buildPackages + displayName: Build packages as part of this pipeline (instead of downloading from a pipeline resource) + type: boolean + default: false + resources: repositories: - repository: self @@ -56,3 +61,4 @@ stages: provisionatorChannel: ${{ parameters.provisionatorChannel }} runTests: ${{ parameters.runTests }} runWindowsIntegration: ${{ parameters.runWindowsIntegration }} + buildPackages: ${{ parameters.buildPackages }} diff --git a/tools/devops/automation/templates/tests-stage.yml b/tools/devops/automation/templates/tests-stage.yml index fb3c7746d609..41f6fec21ddd 100644 --- a/tools/devops/automation/templates/tests-stage.yml +++ b/tools/devops/automation/templates/tests-stage.yml @@ -121,6 +121,10 @@ parameters: type: string default: '' +- name: buildPackages + type: boolean + default: false + stages: - template: ./build/linux-build-verification.yml @@ -148,13 +152,69 @@ stages: BRANCH_NAME: $[ replace(variables['Build.SourceBranch'], 'refs/heads/', '') ] steps: - - template: common/load_configuration.yml - parameters: - repositoryAlias: ${{ parameters.repositoryAlias }} - commit: ${{ parameters.commit }} - statusContext: 'VSTS: simulator tests' - uploadArtifacts: true - use1ES: false + - ${{ if eq(parameters.buildPackages, true) }}: + # When building packages in this pipeline, generate configuration from scratch + - template: common/configure.yml + parameters: + repositoryAlias: ${{ parameters.repositoryAlias }} + commit: ${{ parameters.commit }} + statusContext: 'VSTS: simulator tests' + uploadArtifacts: true + use1ES: false + isPR: ${{ parameters.isPR }} + - ${{ else }}: + # When using pre-built packages from a pipeline resource, load existing configuration + - template: common/load_configuration.yml + parameters: + repositoryAlias: ${{ parameters.repositoryAlias }} + commit: ${{ parameters.commit }} + statusContext: 'VSTS: simulator tests' + uploadArtifacts: true + use1ES: false + +# When buildPackages is true, build packages as part of this pipeline +- ${{ if eq(parameters.buildPackages, true) }}: + - stage: build_packages + displayName: '${{ parameters.stageDisplayNamePrefix }}Build' + dependsOn: [configure_build] + jobs: + - job: build + displayName: 'Build packages' + timeoutInMinutes: 1000 + variables: + DOTNET_PLATFORMS: $[ stageDependencies.configure_build.configure.outputs['configure_platforms.DOTNET_PLATFORMS'] ] + INCLUDE_DOTNET_IOS: $[ stageDependencies.configure_build.configure.outputs['configure_platforms.INCLUDE_DOTNET_IOS'] ] + INCLUDE_DOTNET_MACCATALYST: $[ stageDependencies.configure_build.configure.outputs['configure_platforms.INCLUDE_DOTNET_MACCATALYST'] ] + INCLUDE_DOTNET_MACOS: $[ stageDependencies.configure_build.configure.outputs['configure_platforms.INCLUDE_DOTNET_MACOS'] ] + INCLUDE_DOTNET_TVOS: $[ stageDependencies.configure_build.configure.outputs['configure_platforms.INCLUDE_DOTNET_TVOS'] ] + BuildPackage: $[ stageDependencies.configure_build.configure.outputs['labels.build_package'] ] + SkipPackages: $[ stageDependencies.configure_build.configure.outputs['labels.skip_packages'] ] + SkipNugets: $[ stageDependencies.configure_build.configure.outputs['labels.skip_nugets'] ] + SkipSigning: $[ stageDependencies.configure_build.configure.outputs['labels.skip_signing'] ] + SkipApiComparison: $[ stageDependencies.configure_build.configure.outputs['labels.skip_api_comparison'] ] + PR_ID: $[ stageDependencies.configure_build.configure.outputs['labels.pr_number'] ] + BRANCH_NAME: $[ replace(variables['Build.SourceBranch'], 'refs/heads/', '') ] + XHARNESS_LABELS: $[ stageDependencies.configure_build.configure.outputs['labels.xharness_labels'] ] + RUN_MAC_TESTS: $[ stageDependencies.configure_build.configure.outputs['decisions.RUN_MAC_TESTS'] ] + pool: + name: $(PRBuildPool) + demands: + - Agent.OS -equals Darwin + - Agent.OSVersion -gtVersion $(minimumMacOSVersion) + - macOS.Name -equals ${{ parameters.macOSName }} + - XcodeChannel -equals ${{ parameters.xcodeChannel }} + steps: + - template: build/build-pkgs.yml + parameters: + isPR: ${{ parameters.isPR }} + repositoryAlias: ${{ parameters.repositoryAlias }} + commit: ${{ parameters.commit }} + vsdropsPrefix: ${{ variables.vsdropsPrefix }} + keyringPass: $(pass--lab--mac--builder--keychain) + gitHubToken: $(Github.Token) + xqaCertPass: $(xqa--certificates--password) + use1ES: false + xcodeChannel: ${{ parameters.xcodeChannel }} # always run simulator tests - template: ./tests/stage.yml @@ -173,7 +233,7 @@ stages: gitHubToken: $(Github.Token) xqaCertPass: $(xqa--certificates--password) condition: ${{ parameters.runTests }} - postPipeline: true + postPipeline: ${{ not(parameters.buildPackages) }} - template: ./tests/publish-results.yml parameters: @@ -185,7 +245,7 @@ stages: isPR: ${{ parameters.isPR }} repositoryAlias: ${{ parameters.repositoryAlias }} commit: ${{ parameters.commit }} - postPipeline: true + postPipeline: ${{ not(parameters.buildPackages) }} macTestsConfigurations: ${{ parameters.macTestsConfigurations }} - ${{ if eq(parameters.runWindowsIntegration, true) }}: @@ -200,11 +260,14 @@ stages: statusContext: 'Windows Integration Tests' gitHubToken: $(Github.Token) xqaCertPass: $(xqa--certificates--password) - postPipeline: true + postPipeline: ${{ not(parameters.buildPackages) }} - stage: build_macos_tests displayName: '${{ parameters.stageDisplayNamePrefix }}Build macOS tests' - dependsOn: [configure_build] + dependsOn: + - configure_build + - ${{ if eq(parameters.buildPackages, true) }}: + - build_packages condition: and(succeeded(), ne(dependencies.configure_build.outputs['configure.configure_platforms.DOTNET_PLATFORMS'], '')) jobs: - template: ./build/build-mac-tests-stage.yml @@ -234,5 +297,5 @@ stages: keyringPass: $(pass--lab--mac--builder--keychain) demands: ${{ config.demands }} xqaCertPass: $(xqa--certificates--password) - postPipeline: true + postPipeline: ${{ not(parameters.buildPackages) }} label: ${{ config.label }} From 10728168527b0e364b4244d845f6c0a0c1e544d5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 28 Apr 2026 13:35:12 +0200 Subject: [PATCH 12/13] [devops] Fix trailing backtick in configure.yml PowerShell Remove the trailing backtick on the last parameter of the Get-TestConfiguration call for the simulator test matrix. The backtick caused PowerShell to interpret the next line as a continuation argument, resulting in 'A positional parameter cannot be found that accepts argument $null'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/devops/automation/templates/common/configure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/devops/automation/templates/common/configure.yml b/tools/devops/automation/templates/common/configure.yml index 53f9561f456d..c4587316eefe 100644 --- a/tools/devops/automation/templates/common/configure.yml +++ b/tools/devops/automation/templates/common/configure.yml @@ -242,7 +242,7 @@ steps: -EnabledPlatforms "$Env:CONFIGURE_PLATFORMS_DOTNET_PLATFORMS" ` -TestsLabels "${{ parameters.testsLabels }}" ` -StatusContext "${{ parameters.statusContext }}" ` - -StageFilter "simulator_tests" ` + -StageFilter "simulator_tests" $simulatorTestMatrix = $simulatorTestMatrix | ConvertFrom-Json | ConvertTo-Json -Compress Write-Host "##vso[task.setvariable variable=SIMULATOR_TEST_MATRIX;isOutput=true]$simulatorTestMatrix" name: test_matrix From 975f869d75fc85e976243717565f6d7240ae23d5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 30 Apr 2026 11:11:31 +0200 Subject: [PATCH 13/13] [devops] Add build_packages dependency to windows stage The windows/stage.yml template declared the postPipeline parameter but never used it, so the stage only depended on configure_build and would start before build_packages completed. This caused 'not-signed-package: No such file or directory' when the install-workloads script ran. Add a conditional dependency on build_packages when postPipeline is false (i.e. when packages are built as part of the pipeline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/devops/automation/templates/windows/stage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/devops/automation/templates/windows/stage.yml b/tools/devops/automation/templates/windows/stage.yml index bad97571f29a..7f287284164c 100644 --- a/tools/devops/automation/templates/windows/stage.yml +++ b/tools/devops/automation/templates/windows/stage.yml @@ -47,6 +47,8 @@ stages: displayName: ${{ parameters.displayName }} dependsOn: - configure_build + - ${{ if not(parameters.postPipeline) }}: + - build_packages condition: and(succeeded(), eq(dependencies.configure_build.outputs['configure.decisions.RUN_WINDOWS_TESTS'], 'true')) jobs: