Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions eng/ci/release-kickoff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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: |
git config user.email "azuredevops@microsoft.com"
git config user.name "Azure DevOps Pipeline"
displayName: 'Configure git identity'

- pwsh: |
$ErrorActionPreference = 'Stop'

$params = @{
BumpType = '${{ parameters.bumpType }}'
}
if ('${{ parameters.explicitVersion }}') {
$params['ExplicitVersion'] = '${{ parameters.explicitVersion }}'
}
if ('${{ parameters.versionSuffix }}') {
$params['VersionSuffix'] = '${{ parameters.versionSuffix }}'
}

& "$(Build.SourcesDirectory)/eng/scripts/Start-Release.ps1" @params
displayName: 'Run release kickoff'
env:
GH_TOKEN: $(GitHubToken)
240 changes: 240 additions & 0 deletions eng/scripts/Start-Release.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# 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)]
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VersionSuffix is used to build the git branch name and tag (and is also written into XML via -replace) but it isn’t validated. Please add input validation to restrict it to a safe SemVer prerelease charset (e.g., alphanumerics plus . and -, no whitespace/quotes/slashes) to avoid invalid branch/tag names or accidental PowerShell/regex replacement quirks with characters like $.

Suggested change
[Parameter(Mandatory = $false)]
[Parameter(Mandatory = $false)]
[ValidatePattern('^[0-9A-Za-z.-]*$')]

Copilot uses AI. Check for mistakes.
[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 '<VersionPrefix>[^<]*</VersionPrefix>', "<VersionPrefix>$NewVersion</VersionPrefix>"

# Update VersionSuffix
$content = $content -replace '<VersionSuffix>[^<]*</VersionSuffix>', "<VersionSuffix>$Suffix</VersionSuffix>"

Set-Content -Path $releasePropsPath -Value $content -NoNewline -Encoding UTF8
Comment on lines +96 to +104
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set-Version updates Release.props using regex -replace with a replacement string that directly interpolates $NewVersion/$Suffix. In PowerShell regex replacements, $ sequences in the replacement are treated as capture-group substitutions, so certain suffix values can produce incorrect output. Consider switching to XML DOM updates or a match-evaluator replacement so the inserted values are treated literally.

Suggested change
$content = Get-Content $releasePropsPath -Raw
# Update VersionPrefix
$content = $content -replace '<VersionPrefix>[^<]*</VersionPrefix>', "<VersionPrefix>$NewVersion</VersionPrefix>"
# Update VersionSuffix
$content = $content -replace '<VersionSuffix>[^<]*</VersionSuffix>', "<VersionSuffix>$Suffix</VersionSuffix>"
Set-Content -Path $releasePropsPath -Value $content -NoNewline -Encoding UTF8
[xml]$props = Get-Content $releasePropsPath -Raw
# Update VersionPrefix
$props.Project.PropertyGroup.VersionPrefix = $NewVersion
# Update VersionSuffix
$props.Project.PropertyGroup.VersionSuffix = $Suffix
$props.Save($releasePropsPath)

Copilot uses AI. Check for mistakes.
Write-Host "Updated $releasePropsPath -> VersionPrefix=$NewVersion, VersionSuffix=$Suffix"
}

function Update-Changelog {
param(
[string]$VersionTag
)

Write-Host "Generating changelog for tag 'v$VersionTag'..."

# Run the changelog generator with v-prefixed tag to match repo tagging convention
$generatorOutput = & python $changelogScript --tag "v$VersionTag" 2>&1 | Out-String
$generatorSucceeded = $LASTEXITCODE -eq 0
if (-not $generatorSucceeded) {
Write-Warning "Changelog generation returned non-zero exit code. Output: $generatorOutput"
}

# Extract changelog entries (lines starting with "- ") from generator output
$generatedEntries = ''
if ($generatorSucceeded -and $generatorOutput) {
$entryLines = $generatorOutput -split "`n" | Where-Object { $_ -match '^- ' }
$generatedEntries = ($entryLines -join "`n").Trim()
}

# Read the existing changelog
$existingChangelog = Get-Content $changelogPath -Raw

# Replace "## Unreleased" section with the new version entry
if ($existingChangelog -match '(?s)(## Unreleased\r?\n)(.*?)((?=\r?\n## )|$)') {
$unreleasedContent = $Matches[2].Trim()

# Use generated entries if available, fall back to existing unreleased content
$versionContent = if ($generatedEntries) { $generatedEntries } else { $unreleasedContent }

$newSection = "## Unreleased`n`n## v$VersionTag`n"
if ($versionContent) {
$newSection = "## Unreleased`n`n## v$VersionTag`n`n$versionContent`n"
}
$updatedChangelog = $existingChangelog -replace '(?s)## Unreleased\r?\n.*?(?=(\r?\n## )|$)', $newSection
Set-Content -Path $changelogPath -Value $updatedChangelog -NoNewline -Encoding UTF8
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 {
# Ensure we start from an up-to-date main branch
Write-Host "Switching to main and pulling latest..."
git checkout main
if ($LASTEXITCODE -ne 0) { throw "Failed to checkout main." }
git pull origin main
if ($LASTEXITCODE -ne 0) { throw "Failed to pull latest main." }
Comment on lines +177 to +182
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before switching branches and pulling (git checkout main / git pull), the script should validate that the working tree is clean (e.g., git status --porcelain is empty). Otherwise uncommitted local changes can be carried into the release branch/PR or cause checkout/pull to fail in a confusing way, which is risky for a release kickoff workflow.

Copilot uses AI. Check for mistakes.

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 ==="
Loading