Files
PowerToys/.pipelines/writeReleaseMetadata.ps1
Boliang Zhang e753ec51fb feat(release): automate draft preview release preparation (#49797)
## Summary of the Pull Request

Adds a `Prepare Preview Release` custom agent that autonomously turns a
successful PowerToys Azure DevOps release-candidate build into a
complete GitHub draft prerelease for final human review.

The implementation extends the existing `release-note-generation` skill
instead of duplicating it. It adds exact-build metadata resolution,
published-release baseline selection, semantic PR deltas across `main`
and `stable`, release asset validation, idempotent draft-only release
updates, and final draft verification.

## PR Checklist

- [x] **Communication:** The autonomous preview-release design was
reviewed and approved before implementation
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** N/A; no end-user-facing strings were added
- [x] **Dev docs:** Added preview scenario, delta, draft safety, and
reporting references

## Detailed Description of the Pull Request / Additional comments

- Adds `.github/agents/prepare-preview-release.agent.md` with a
no-mid-run-decision workflow and a strict prohibition on publishing
releases.
- Extends `.github/skills/release-note-generation/SKILL.md` with
stable/preview scenario routing while preserving the existing
stable-release workflow.
- Adds canonical scripts under
`.github/skills/release-note-generation/scripts/` to:
  - Resolve and validate ADO build metadata.
- Select the latest published stable or preview baseline before build
queue time.
- Calculate same-lineage or branch-transition PR deltas using PR
numbers, cherry-pick provenance, and patch-ID equivalence.
  - Collect normalized PR metadata and create `release-manifest.json`.
- Download and validate installers, symbols, and GPO assets, including
hashes, signatures, and ZIP contents.
- Create or update draft prereleases while preserving human text outside
managed markers.
- Verify draft flags, immutable target commit, body markers, and
uploaded assets.
- Updates `.pipelines/resolveBuildMetadata.ps1` and
`.pipelines/v2/release.yml` with explicit `auto`, `preview-release`, and
`stable-release` intent handling so preview candidates can be built from
either `main` or `stable`.
- Adds `.pipelines/writeReleaseMetadata.ps1` so each signed build
artifact records its resolved version, channel, intent, source branch,
and immutable source commit.
- Keeps release publication outside the agent: the automation can only
create or update a draft prerelease.

## Validation Steps Performed

- `Invoke-Pester` for:
  - `.pipelines/tests/resolveBuildMetadata.Tests.ps1`
  - `.pipelines/tests/writeReleaseMetadata.Tests.ps1`
-
`.github/skills/release-note-generation/tests/preview-release.Tests.ps1`
- 37 tests passed, covering stable-branch preview intent, metadata
contracts, baseline selection, same-lineage and branch-transition
deltas, patch-ID equivalence, managed-body preservation, and
published-release refusal.
- Parsed all added or modified PowerShell scripts with the PowerShell
AST parser.
- Parsed the modified pipeline YAML files with `ConvertFrom-Yaml`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e04a72e-3b0f-4ac4-8156-d04ea9b8bb85
Copilot-Session: e9f79ac2-9a7b-4083-834c-0d87e8c83bfd
Copilot-Session: 1ecea747-b313-49a1-9969-543c01ba1be8
2026-08-17 14:47:18 +08:00

86 lines
2.9 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory)][int]$DefinitionId,
[Parameter(Mandatory)][int]$BuildId,
[Parameter(Mandatory)][string]$BuildNumber,
[Parameter(Mandatory)][string]$Version,
[Parameter(Mandatory)][ValidateSet("private", "preview", "stable")][string]$Channel,
[Parameter(Mandatory)][ValidateSet("private-validation", "preview-validation", "preview-release", "stable-release")][string]$Intent,
[Parameter(Mandatory)][string]$SourceBranch,
[Parameter(Mandatory)][string]$SourceCommit,
[Parameter(Mandatory)][string]$BuildReason,
[Parameter(Mandatory)][string]$ShouldPublishPreview,
[AllowEmptyString()][string]$QueuedAt = "",
[AllowEmptyString()][string]$StartedAt = "",
[Parameter(Mandatory)][string]$OutputPath
)
$ErrorActionPreference = "Stop"
if ($Version -notmatch "^\d+\.\d+\.\d+\.0$") {
throw "Version '$Version' must use the four-component PowerToys release format."
}
if ($SourceCommit -notmatch "^[0-9a-fA-F]{40}$") {
throw "SourceCommit must be a full immutable commit SHA."
}
if ($SourceBranch -notmatch "^refs/heads/.+") {
throw "SourceBranch '$SourceBranch' is not a branch ref."
}
if ($Intent -ne "private-validation" -and $SourceBranch -notin @("refs/heads/main", "refs/heads/stable")) {
throw "Intent '$Intent' is only supported from main or stable."
}
$publishPreview = switch ($ShouldPublishPreview.ToLowerInvariant()) {
"true" { $true }
"false" { $false }
default { throw "ShouldPublishPreview must be true or false." }
}
if ($publishPreview -ne ($Intent -eq "preview-release")) {
throw "ShouldPublishPreview '$publishPreview' conflicts with intent '$Intent'."
}
$expectedChannel = switch ($Intent) {
"private-validation" { "private" }
"preview-validation" { "preview" }
"preview-release" { "preview" }
"stable-release" { "stable" }
}
if ($Channel -ne $expectedChannel) {
throw "Intent '$Intent' requires channel '$expectedChannel', not '$Channel'."
}
function ConvertTo-NullablePipelineValue {
param([AllowEmptyString()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value) -or $Value.StartsWith('$(')) {
return $null
}
return $Value
}
$metadata = [ordered]@{
schemaVersion = 1
definitionId = $DefinitionId
buildId = $BuildId
buildNumber = $BuildNumber
result = "succeeded"
version = $Version
channel = $Channel
intent = $Intent
sourceBranch = $SourceBranch
sourceCommit = $SourceCommit.ToLowerInvariant()
buildReason = $BuildReason
queuedAt = ConvertTo-NullablePipelineValue -Value $QueuedAt
startedAt = ConvertTo-NullablePipelineValue -Value $StartedAt
finishedAt = $null
shouldPublishPreview = $publishPreview
}
$parent = Split-Path -Parent $OutputPath
if ($parent) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
$metadata | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $OutputPath -Encoding utf8
[pscustomobject]$metadata