From f9ffd1d01e705d5af2c83e35e70faed8aa21152b Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 17 Mar 2026 09:59:57 -0700 Subject: [PATCH] Add SDK release kickoff pipeline and script Add an Azure DevOps pipeline (eng/ci/release-kickoff.yml) and a local PowerShell helper (eng/scripts/Start-Release.ps1) to automate the SDK release kickoff process: - Pipeline parameters for bump type (major/minor/patch/explicit), explicit version, and version suffix - Bumps VersionPrefix/VersionSuffix in eng/targets/Release.props - Generates changelog update via generate_changelog.py - Creates a release/v branch and pushes it - Opens a release PR targeting main --- eng/ci/release-kickoff.yml | 206 +++++++++++++++++++++++++++++++ eng/scripts/Start-Release.ps1 | 222 ++++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 eng/ci/release-kickoff.yml create mode 100644 eng/scripts/Start-Release.ps1 diff --git a/eng/ci/release-kickoff.yml b/eng/ci/release-kickoff.yml new file mode 100644 index 000000000..62807a741 --- /dev/null +++ b/eng/ci/release-kickoff.yml @@ -0,0 +1,206 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# This pipeline automates the SDK release kickoff process. +# It bumps package versions, generates a changelog update, creates a release branch, +# and opens a pull request for the release. + +trigger: none +pr: none + +parameters: + - name: bumpType + displayName: 'Version bump type' + type: string + default: 'minor' + values: + - major + - minor + - patch + - explicit + + - name: explicitVersion + displayName: 'Explicit version (required if bump type is "explicit", format: X.Y.Z)' + type: string + default: '' + + - name: versionSuffix + displayName: 'Version suffix (e.g., "preview", "rc.1"; leave empty for stable release)' + type: string + default: '' + +pool: + vmImage: 'ubuntu-latest' + +steps: + - checkout: self + persistCredentials: true + fetchDepth: 0 + + - task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + packageType: 'sdk' + useGlobalJson: true + + - task: UsePythonVersion@0 + displayName: 'Use Python 3.x' + inputs: + versionSpec: '3.x' + + - pwsh: | + $ErrorActionPreference = 'Stop' + + $repoRoot = "$(Build.SourcesDirectory)" + $releasePropsPath = Join-Path $repoRoot 'eng/targets/Release.props' + $changelogPath = Join-Path $repoRoot 'CHANGELOG.md' + $changelogScript = Join-Path $repoRoot 'generate_changelog.py' + + $bumpType = '${{ parameters.bumpType }}' + $explicitVersion = '${{ parameters.explicitVersion }}' + $versionSuffix = '${{ parameters.versionSuffix }}' + + # --- Read current version --- + [xml]$props = Get-Content $releasePropsPath -Raw + $currentVersion = $props.Project.PropertyGroup.VersionPrefix.Trim() + Write-Host "Current version: $currentVersion" + + # --- Compute new version --- + if ($bumpType -eq 'explicit') { + if (-not $explicitVersion) { + throw "explicitVersion parameter is required when bumpType is 'explicit'." + } + if ($explicitVersion -notmatch '^\d+\.\d+\.\d+$') { + throw "explicitVersion must be in the format 'X.Y.Z'. Got: '$explicitVersion'" + } + $newVersion = $explicitVersion + } + else { + $parts = $currentVersion.Split('.') + [int]$major = $parts[0] + [int]$minor = $parts[1] + [int]$patch = $parts[2] + + switch ($bumpType) { + 'major' { $major++; $minor = 0; $patch = 0 } + 'minor' { $minor++; $patch = 0 } + 'patch' { $patch++ } + } + $newVersion = "$major.$minor.$patch" + } + + $fullVersion = $newVersion + if ($versionSuffix) { + $fullVersion = "$newVersion-$versionSuffix" + } + + Write-Host "New version: $newVersion" + Write-Host "Full version: $fullVersion" + Write-Host "##vso[task.setvariable variable=NewVersion]$newVersion" + Write-Host "##vso[task.setvariable variable=FullVersion]$fullVersion" + Write-Host "##vso[task.setvariable variable=VersionSuffix]$versionSuffix" + + # --- Update Release.props --- + $content = Get-Content $releasePropsPath -Raw + $content = $content -replace '[^<]*', "$newVersion" + $content = $content -replace '[^<]*', "$versionSuffix" + Set-Content -Path $releasePropsPath -Value $content -NoNewline + Write-Host "Updated Release.props" + + # --- Update CHANGELOG.md --- + Write-Host "Generating changelog..." + $changelogEntry = python $changelogScript --tag "v$fullVersion" 2>&1 | Out-String + Write-Host "Changelog generator output:" + Write-Host $changelogEntry + + $existingChangelog = Get-Content $changelogPath -Raw + if ($existingChangelog -match '## Unreleased') { + # Insert the new version header after "## Unreleased" and a blank line + $updatedChangelog = $existingChangelog -replace '(## Unreleased\r?\n)', "`$1`n## v$fullVersion`n" + Set-Content -Path $changelogPath -Value $updatedChangelog -NoNewline + Write-Host "Updated CHANGELOG.md" + } + else { + Write-Warning "Could not find '## Unreleased' section in CHANGELOG.md." + } + displayName: 'Bump version and update changelog' + + - pwsh: | + $ErrorActionPreference = 'Stop' + + $fullVersion = "$(FullVersion)" + $branchName = "release/v$fullVersion" + + git config user.email "azuredevops@microsoft.com" + git config user.name "Azure DevOps Pipeline" + + git checkout -b $branchName + git add eng/targets/Release.props + git add CHANGELOG.md + git commit -m "Bump version to $fullVersion and update changelog" + git push origin $branchName + + Write-Host "##vso[task.setvariable variable=ReleaseBranch]$branchName" + Write-Host "Release branch '$branchName' created and pushed." + displayName: 'Create and push release branch' + + - pwsh: | + $ErrorActionPreference = 'Stop' + + $fullVersion = "$(FullVersion)" + $newVersion = "$(NewVersion)" + $versionSuffix = "$(VersionSuffix)" + $branchName = "$(ReleaseBranch)" + $repo = "microsoft/durabletask-dotnet" + + $prTitle = "Release v$fullVersion" + $prBody = @" + ## SDK Release v$fullVersion + + This PR prepares the release of **v$fullVersion**. + + ### Changes + - Bumped ``VersionPrefix`` to ``$newVersion`` in ``eng/targets/Release.props`` + - Updated ``VersionSuffix`` to ``$versionSuffix`` + - Updated ``CHANGELOG.md`` + + ### Release Checklist + - [ ] Verify version bump is correct + - [ ] Verify CHANGELOG.md entries are accurate + - [ ] Verify RELEASENOTES.md files are updated (if needed) + - [ ] Merge this PR + - [ ] Tag the release: ``git tag v$fullVersion`` + - [ ] Kick off the [ADO release build](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_build?definitionId=29) + "@ + + # Create PR using the Azure DevOps REST API + $org = $env:SYSTEM_COLLECTIONURI + $project = $env:SYSTEM_TEAMPROJECT + $repoId = $env:BUILD_REPOSITORY_ID + + $url = "${org}${project}/_apis/git/repositories/${repoId}/pullrequests?api-version=7.1" + + $body = @{ + sourceRefName = "refs/heads/$branchName" + targetRefName = "refs/heads/main" + title = $prTitle + description = $prBody + } | ConvertTo-Json -Depth 10 + + $headers = @{ + 'Content-Type' = 'application/json' + 'Authorization' = "Bearer $(System.AccessToken)" + } + + try { + $response = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body + Write-Host "Pull request created: $($response.url)" + Write-Host "PR ID: $($response.pullRequestId)" + } + catch { + Write-Warning "Failed to create PR via ADO API: $_" + Write-Host "You can create the PR manually at: https://github.com/$repo/compare/main...$branchName" + } + displayName: 'Open release pull request' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/eng/scripts/Start-Release.ps1 b/eng/scripts/Start-Release.ps1 new file mode 100644 index 000000000..07ccd044e --- /dev/null +++ b/eng/scripts/Start-Release.ps1 @@ -0,0 +1,222 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Bumps package versions, generates changelog, creates a release branch, and opens a PR. + +.DESCRIPTION + This script automates the SDK release kickoff process: + 1. Bumps the version in eng/targets/Release.props based on the bump type or explicit version. + 2. Runs generate_changelog.py to produce a changelog entry. + 3. Prepends the generated changelog entry to CHANGELOG.md. + 4. Creates a release branch and pushes it. + 5. Opens a pull request targeting main. + +.PARAMETER BumpType + The type of version bump: 'major', 'minor', 'patch', or 'explicit'. + +.PARAMETER ExplicitVersion + The explicit version to set (required when BumpType is 'explicit'). Format: 'X.Y.Z'. + +.PARAMETER VersionSuffix + Optional pre-release suffix (e.g., 'preview', 'rc.1'). Leave empty for stable releases. +#> + +param( + [Parameter(Mandatory = $true)] + [ValidateSet('major', 'minor', 'patch', 'explicit')] + [string]$BumpType, + + [Parameter(Mandatory = $false)] + [string]$ExplicitVersion = '', + + [Parameter(Mandatory = $false)] + [string]$VersionSuffix = '' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path "$PSScriptRoot/../..").Path +$releasePropsPath = Join-Path $repoRoot 'eng/targets/Release.props' +$changelogPath = Join-Path $repoRoot 'CHANGELOG.md' +$changelogScript = Join-Path $repoRoot 'generate_changelog.py' + +function Get-CurrentVersion { + [xml]$props = Get-Content $releasePropsPath -Raw + $versionPrefix = $props.Project.PropertyGroup.VersionPrefix + if (-not $versionPrefix) { + throw "Could not find VersionPrefix in $releasePropsPath" + } + return $versionPrefix.Trim() +} + +function Get-BumpedVersion { + param( + [string]$CurrentVersion, + [string]$BumpType, + [string]$ExplicitVersion + ) + + if ($BumpType -eq 'explicit') { + if (-not $ExplicitVersion) { + throw "ExplicitVersion is required when BumpType is 'explicit'." + } + if ($ExplicitVersion -notmatch '^\d+\.\d+\.\d+$') { + throw "ExplicitVersion must be in the format 'X.Y.Z'. Got: '$ExplicitVersion'" + } + return $ExplicitVersion + } + + $parts = $CurrentVersion.Split('.') + if ($parts.Count -ne 3) { + throw "Current version '$CurrentVersion' is not in expected X.Y.Z format." + } + + [int]$major = $parts[0] + [int]$minor = $parts[1] + [int]$patch = $parts[2] + + switch ($BumpType) { + 'major' { $major++; $minor = 0; $patch = 0 } + 'minor' { $minor++; $patch = 0 } + 'patch' { $patch++ } + } + + return "$major.$minor.$patch" +} + +function Set-Version { + param( + [string]$NewVersion, + [string]$Suffix + ) + + $content = Get-Content $releasePropsPath -Raw + + # Update VersionPrefix + $content = $content -replace '[^<]*', "$NewVersion" + + # Update VersionSuffix + $content = $content -replace '[^<]*', "$Suffix" + + Set-Content -Path $releasePropsPath -Value $content -NoNewline + Write-Host "Updated $releasePropsPath -> VersionPrefix=$NewVersion, VersionSuffix=$Suffix" +} + +function Update-Changelog { + param( + [string]$VersionTag + ) + + Write-Host "Generating changelog for tag '$VersionTag'..." + + # Run the changelog generator + $changelogEntry = & python $changelogScript --tag $VersionTag 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Changelog generation returned non-zero exit code. Output: $changelogEntry" + } + + # Read the existing changelog + $existingChangelog = Get-Content $changelogPath -Raw + + # Replace "## Unreleased" section with the new version entry + # The unreleased content gets moved under the new version heading + if ($existingChangelog -match '(?s)(## Unreleased\r?\n)(.*?)((?=\r?\n## )|$)') { + $unreleasedContent = $Matches[2].Trim() + $newSection = "## Unreleased`n`n## v$VersionTag`n" + if ($unreleasedContent) { + $newSection = "## Unreleased`n`n## v$VersionTag`n$unreleasedContent`n" + } + $updatedChangelog = $existingChangelog -replace '(?s)## Unreleased\r?\n.*?(?=(\r?\n## )|$)', $newSection + Set-Content -Path $changelogPath -Value $updatedChangelog -NoNewline + Write-Host "Updated CHANGELOG.md with version v$VersionTag" + } + else { + Write-Warning "Could not find '## Unreleased' section in CHANGELOG.md. Skipping changelog update." + } +} + +# --- Main Script --- + +Write-Host "=== SDK Release Kickoff ===" +Write-Host "" + +# Step 1: Compute new version +$currentVersion = Get-CurrentVersion +Write-Host "Current version: $currentVersion" + +$newVersion = Get-BumpedVersion -CurrentVersion $currentVersion -BumpType $BumpType -ExplicitVersion $ExplicitVersion +Write-Host "New version: $newVersion" + +$fullVersion = $newVersion +if ($VersionSuffix) { + $fullVersion = "$newVersion-$VersionSuffix" +} +Write-Host "Full version: $fullVersion" + +# Step 2: Create release branch +$branchName = "release/v$fullVersion" +Write-Host "" +Write-Host "Creating release branch: $branchName" + +Push-Location $repoRoot +try { + git checkout -b $branchName + if ($LASTEXITCODE -ne 0) { throw "Failed to create branch '$branchName'." } + + # Step 3: Bump version in Release.props + Set-Version -NewVersion $newVersion -Suffix $VersionSuffix + + # Step 4: Update CHANGELOG.md + Update-Changelog -VersionTag $fullVersion + + # Step 5: Commit and push + git add eng/targets/Release.props + git add CHANGELOG.md + git commit -m "Bump version to $fullVersion and update changelog" + if ($LASTEXITCODE -ne 0) { throw "Failed to commit changes." } + + git push origin $branchName + if ($LASTEXITCODE -ne 0) { throw "Failed to push branch '$branchName'." } + + Write-Host "" + Write-Host "Release branch '$branchName' pushed successfully." + + # Step 6: Open a PR using the GitHub CLI + $prTitle = "Release v$fullVersion" + $prBody = @" +## SDK Release v$fullVersion + +This PR prepares the release of **v$fullVersion**. + +### Changes +- Bumped ``VersionPrefix`` to ``$newVersion`` in ``eng/targets/Release.props`` +- Updated ``VersionSuffix`` to ``$VersionSuffix`` +- Updated ``CHANGELOG.md`` + +### Release Checklist +- [ ] Verify version bump is correct +- [ ] Verify CHANGELOG.md entries are accurate +- [ ] Verify RELEASENOTES.md files are updated (if needed) +- [ ] Merge this PR +- [ ] Tag the release: ``git tag v$fullVersion`` +- [ ] Kick off the [ADO release build](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_build?definitionId=29) targeting ``refs/tags/v$fullVersion`` +"@ + + Write-Host "Opening pull request..." + gh pr create --base main --head $branchName --title $prTitle --body $prBody + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to create PR via 'gh'. You can create it manually at: https://github.com/microsoft/durabletask-dotnet/compare/main...$branchName" + } + else { + Write-Host "Pull request created successfully." + } +} +finally { + Pop-Location +} + +Write-Host "" +Write-Host "=== Release kickoff complete ==="