Sync eng/common directory with azure-sdk-tools for PR 16928 (#3574)

Sync eng/common directory with azure-sdk-tools repository for Tools PR 16928

Co-authored-by: azure-sdk <azuresdk@microsoft.com>
This commit is contained in:
azure-sdk-automation[bot]andazure-sdk authored and GitHub committed 2026-09-04 16:02:19 -04:00
1 parent 8f2bea21d8
commit 51f927dde2
1 file changed
+274
@@ -0,0 +1,274 @@
<#
.SYNOPSIS
Determines which of a pipeline's packages should be auto-released after a labeled PR merge to main.
.DESCRIPTION
Language-agnostic. Intended to run in an internal post-merge CI run on 'main'. Given the build's merge
commit, this script:
1. Uses the shared Get-GitHubAutoReleasePullRequestForCommit policy to resolve the pull request for
the commit: it selects the newest PR merged into the base branch (default 'main') and requires the
'auto-release' label.
2. Builds a PR diff object (New-GitHubPullRequestDiffObject) from the PR's changed files and reuses
the repo's existing package-detection logic (Get-PrPkgProperties) to identify the changed packages
(honoring triggering paths and deleted files while ignoring only files directly under
sdk/<service>/, not files in package subdirectories), excluding validation-only packages.
Get-PrPkgProperties delegates to each repo's own
Get-AllPackageInfoFromRepo (language-settings.ps1), so this script works for any language repo.
3. Intersects those packages with this pipeline's declared artifacts and emits Azure DevOps output
variables consumed by the release stages. Both consumption styles are emitted:
- per-artifact ReleaseArtifact_<safeName> booleans, for pipelines that loop over their
compile-time Artifacts list and gate each entry (e.g. .NET);
- AutoReleaseArtifactsJson, the matched declared-artifact objects serialized as a JSON array,
for pipelines that iterate the releasable set at runtime (e.g. Java).
The script FAILS CLOSED: on any error, or if no qualifying labeled PR / changed package is found, it
emits HasAutoReleaseArtifacts=false, AutoReleaseArtifactsJson=[] and ReleaseArtifact_<safeName>=false
for every artifact, and exits 0 so the CI run is not failed.
.PARAMETER CommitSha
The build source version (merge commit) to resolve the pull request from. Typically $(Build.SourceVersion).
.PARAMETER RepoId
The GitHub repository id in '<owner>/<name>' form. Typically $(Build.Repository.Name).
.PARAMETER Artifacts
JSON array of the pipeline's declared artifacts. Each entry must have 'name' and 'safeName'; entries may
also carry a 'groupId' (used to disambiguate name collisions across groups) and any other fields the
consuming stage needs (they are passed through unchanged in AutoReleaseArtifactsJson).
Defaults to the AUTORELEASE_ARTIFACTS environment variable, which the pipeline sets to
'${{ convertToJson(parameters.Artifacts) }}' (passed via env because it is multi-line JSON).
.PARAMETER AuthToken
GitHub token used for API calls. Defaults to the GH_TOKEN environment variable produced by
login-to-github.yml (passed via env so the secret is not written to the task command line).
.PARAMETER AutoReleaseLabel
The GitHub PR label that opts a merged PR into auto-release. Defaults to 'auto-release'.
.PARAMETER BaseBranch
The base branch a PR must have been merged into to qualify. Defaults to 'main'.
.PARAMETER PipelineUrl
The URL of the pipeline run, used for logging or linking back to the pipeline. Defaults to empty.
.PARAMETER AzsdkExePath
The path to the azsdk executable used for release operations. Defaults to the AZSDK environment variable.
.OUTPUTS
Azure DevOps output variables (reference cross-stage via dependencies.<stage>.outputs['<job>.<step>.<name>']):
- HasAutoReleaseArtifacts : 'true' if at least one declared package is releasable
- AutoReleaseArtifactsJson : JSON array of the matched declared-artifact objects (or '[]')
- ReleaseArtifact_<safeName> : 'true'/'false' per declared artifact
HasAutoReleaseArtifacts is the single eligibility gate: it is 'true' only when a merged, auto-release-labeled
PR changed at least one declared package, and it is emitted last so any earlier failure fails closed.
#>
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string] $CommitSha,
[Parameter(Mandatory = $true)][string] $RepoId,
[string] $Artifacts = $env:AUTORELEASE_ARTIFACTS,
[string] $AuthToken = $env:GH_TOKEN,
[string] $AutoReleaseLabel = 'auto-release',
[string] $BaseBranch = 'main',
[string] $PipelineUrl = '',
[string] $AzsdkExePath = $env:AZSDK
)
$ErrorActionPreference = 'Stop'
# Import shared logic unless a caller (e.g. a test harness) has already provided it. common.ps1 provides
# Get-PrPkgProperties, the GitHub helpers, Set-PipelineVariable, $RepoRoot and language settings;
# AutoRelease-Operations.ps1 provides the shared auto-release PR selection and diff helpers. StrictMode
# is intentionally not enabled here because the shared package-detection code is not written to run under it.
if (-not (Get-Command 'Get-PrPkgProperties' -ErrorAction SilentlyContinue)) {
. (Join-Path $PSScriptRoot "common.ps1")
}
if (-not (Get-Command 'Get-GitHubAutoReleasePullRequestForCommit' -ErrorAction SilentlyContinue)) {
. (Join-Path $PSScriptRoot "AutoRelease-Operations.ps1")
}
# Parse the declared artifacts.
$declaredArtifacts = @()
try {
$parsed = $Artifacts | ConvertFrom-Json
if ($null -ne $parsed) { $declaredArtifacts = @($parsed) }
}
catch {
LogWarning "Failed to parse -Artifacts JSON; treating as empty. $($_.Exception.Message)"
}
# Fail-closed defaults: nothing releases unless we positively determine otherwise below.
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'false' -IsOutput
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value '[]' -IsOutput
foreach ($artifact in $declaredArtifacts) {
if ($artifact.PSObject.Properties['safeName'] -and $artifact.safeName) {
Set-PipelineVariable -Name "ReleaseArtifact_$($artifact.safeName)" -Value 'false' -IsOutput
}
}
function Invoke-AutoReleaseResolution {
Write-Host "Resolving the auto-release pull request for commit '$CommitSha' in '$RepoId'..."
$release = Get-GitHubAutoReleasePullRequestForCommit `
-RepoId $RepoId `
-CommitSha $CommitSha `
-TargetBranch $BaseBranch `
-RequiredLabel $AutoReleaseLabel `
-AuthToken $AuthToken
if (-not $release.IsEligible) {
Write-Host "Skipping auto-release: $($release.SkipReason)"
return
}
$pr = $release.PullRequest
# Prefer the PR's canonical html_url; fall back to constructing it so the log link stays clickable even
# if the field is absent from the payload.
$prLink = if ($pr.PSObject.Properties['html_url'] -and $pr.html_url) { "$($pr.html_url)" } else { "https://github.com/$RepoId/pull/$($pr.number)" }
Write-Host "PR $prLink is eligible for auto-release (merged into '$BaseBranch' with the '$AutoReleaseLabel' label)."
if ($declaredArtifacts.Count -eq 0) {
LogWarning "PR $prLink has the '$AutoReleaseLabel' label but this pipeline declares no artifacts; nothing will be auto-released."
return
}
# Turn the PR's changed files into a diff object (Generate-PR-Diff.ps1 shape) and reuse the repo's
# package-detection logic to identify the changed packages.
Write-Host "Fetching changed files for PR $prLink..."
$files = @(Get-GitHubPullRequestFiles -RepoId $RepoId -PullRequestNumber $pr.number -AuthToken $AuthToken)
$diff = New-GitHubPullRequestDiffObject `
-PullRequestNumber $pr.number `
-PullRequestFiles $files `
-ExcludeServiceRootFiles
Write-Host "PR $prLink changed $($diff.ChangedFiles.Count) file(s) and deleted $($diff.DeletedFiles.Count) file(s)."
$diffPath = Join-Path ([System.IO.Path]::GetTempPath()) ("autorelease-diff-" + [System.Guid]::NewGuid().ToString('N') + ".json")
$diff | ConvertTo-Json -Depth 10 | Set-Content -Path $diffPath -Encoding utf8
try {
$changedPackages = @(Get-PrPkgProperties -InputDiffJson $diffPath)
}
finally {
Remove-Item -Path $diffPath -ErrorAction SilentlyContinue
}
# Build the releasable key set. Add each changed package's name and, when the group is known, a
# 'group/name' composite so that pipelines whose declared artifacts carry a groupId (e.g. Java) match
# the correct group and are not confused by name collisions across groups. Packages pulled in solely
# for validation are not releasable.
$releasableKeys = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($package in $changedPackages) {
if ($package.IncludedForValidation) { continue }
$names = @()
if ($package.Name) { $names += [string]$package.Name }
if ($package.PSObject.Properties['ArtifactName'] -and $package.ArtifactName) { $names += [string]$package.ArtifactName }
$group = $null
if ($package.PSObject.Properties['Group'] -and $package.Group) { $group = [string]$package.Group }
foreach ($name in ($names | Sort-Object -Unique)) {
[void]$releasableKeys.Add($name)
if ($group) { [void]$releasableKeys.Add("$group/$name") }
}
}
$matchedArtifacts = @()
foreach ($artifact in $declaredArtifacts) {
try {
$name = $artifact.name
$safeName = $artifact.safeName
if (-not $name -or -not $safeName) {
Write-Host " Skipping artifact with missing name/safeName."
continue
}
$groupId = $null
if ($artifact.PSObject.Properties['groupId'] -and $artifact.groupId) { $groupId = [string]$artifact.groupId }
# Prefer a group-qualified match when the artifact declares a group; otherwise match by name.
if ($groupId) {
$isMatch = $releasableKeys.Contains("$groupId/$name")
}
else {
$isMatch = $releasableKeys.Contains([string]$name)
}
if ($isMatch) {
Write-Host " [$name] changed by PR $prLink -> releasable."
Set-PipelineVariable -Name "ReleaseArtifact_$safeName" -Value 'true' -IsOutput
$matchedArtifacts += $artifact
# Update release pending status and release pipeline URL in the release plan for this package.
# The release status is updated to "Released" after successful completion; until then, mark it as "Release In Progress" to indicate that the release is underway.
try
{
if($AzsdkExePath)
{
$sdkPullRequestUrl = $pr.html_url
$cliArgs = @("release-plan", "update-release-status", "--package-name", $name, "--language", $LanguageDisplayName, "--status", "Release In Progress", "--sdk-pull-request", $sdkPullRequestUrl)
if ($PipelineUrl)
{
$cliArgs += @("--release-pipeline", $PipelineUrl)
}
else
{
LogWarning "Pipeline URL is not set; Not setting release pipeline link for package '$name' in release plan."
}
& $AzsdkExePath @cliArgs
if ($LASTEXITCODE -ne 0)
{
## Not all releases have a release plan. So we should not fail the script even if a release plan is missing.
Write-Host "Failed to update release pending status for package '$name' using azsdk. Exit code: $LASTEXITCODE"
}
}
else
{
Write-Host "AzsdkExePath is not set; skipping release plan update for package '$name'."
}
}
catch
{
Write-Host "Failed to update release pending status in release plan for package '$name'. $($_.Exception.Message)"
}
}
else {
Write-Host " [$name] not changed by PR $prLink."
}
}
catch {
LogWarning "Failed to evaluate an artifact; treating as not releasable. $($_.Exception.Message)"
}
}
if ($matchedArtifacts.Count -gt 0) {
# Pipe (not -InputObject) with -AsArray so a single match still serializes as a JSON array, '[{...}]'.
$artifactsJson = $matchedArtifacts | ConvertTo-Json -Depth 100 -Compress -AsArray
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value $artifactsJson -IsOutput
Write-Host "Auto-release packages from PR ${prLink}: $((@($matchedArtifacts | ForEach-Object { $_.name })) -join ', ')"
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'true' -IsOutput
}
else {
LogWarning "PR $prLink has the '$AutoReleaseLabel' label but changed no releasable package in this pipeline; nothing will be auto-released."
}
}
try {
Invoke-AutoReleaseResolution
}
catch {
# Re-emit the fail-closed defaults so a failure after any positive signal was set (e.g. after a
# ReleaseArtifact_<safeName> flag was flipped to 'true') cannot leak a partial "release" decision to
# downstream stages, regardless of how each consumer gates on the outputs.
LogWarning "Auto-release resolution failed; skipping auto-release. $($_.Exception.Message)"
Set-PipelineVariable -Name 'HasAutoReleaseArtifacts' -Value 'false' -IsOutput
Set-PipelineVariable -Name 'AutoReleaseArtifactsJson' -Value '[]' -IsOutput
foreach ($artifact in $declaredArtifacts) {
if ($artifact.PSObject.Properties['safeName'] -and $artifact.safeName) {
Set-PipelineVariable -Name "ReleaseArtifact_$($artifact.safeName)" -Value 'false' -IsOutput
}
}
}
exit 0