|
| 1 | +#!/usr/bin/env pwsh |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + Automate the release process for Windows App Development CLI |
| 5 | +.DESCRIPTION |
| 6 | + This script automates the release workflow: |
| 7 | + 1. Verifies you are on the main branch with a clean working tree and latest changes |
| 8 | + 2. Reads and confirms the version from version.json |
| 9 | + 3. Creates and pushes a rel/v{version} branch to origin (triggers the release pipeline) |
| 10 | + 4. Returns to main, bumps the patch version in version.json |
| 11 | + 5. Creates a PR to merge the version bump back into main |
| 12 | +
|
| 13 | + Prerequisites: |
| 14 | + - Git must be installed and authenticated with push access to origin |
| 15 | + - GitHub CLI (gh) must be installed and authenticated (for PR creation) |
| 16 | +.PARAMETER SkipConfirmation |
| 17 | + Skip the interactive confirmation prompt before creating the release branch |
| 18 | +.PARAMETER DryRun |
| 19 | + Show what would happen without making any changes (no branches created, no pushes, no PRs) |
| 20 | +.EXAMPLE |
| 21 | + .\scripts\start-release.ps1 |
| 22 | +.EXAMPLE |
| 23 | + .\scripts\start-release.ps1 -SkipConfirmation |
| 24 | +.EXAMPLE |
| 25 | + .\scripts\start-release.ps1 -DryRun |
| 26 | +#> |
| 27 | + |
| 28 | +param( |
| 29 | + [switch]$SkipConfirmation = $false, |
| 30 | + [switch]$DryRun = $false |
| 31 | +) |
| 32 | + |
| 33 | +$ErrorActionPreference = "Stop" |
| 34 | +$ProjectRoot = $PSScriptRoot | Split-Path -Parent |
| 35 | +$VersionFilePath = Join-Path $ProjectRoot "version.json" |
| 36 | + |
| 37 | +# ─── Helpers ──────────────────────────────────────────────────────────────────── |
| 38 | + |
| 39 | +function Write-Step { param([string]$msg) Write-Host "`n==> $msg" -ForegroundColor Cyan } |
| 40 | +function Write-Info { param([string]$msg) Write-Host " $msg" -ForegroundColor Gray } |
| 41 | +function Write-Ok { param([string]$msg) Write-Host " $msg" -ForegroundColor Green } |
| 42 | +function Write-Warn { param([string]$msg) Write-Host " $msg" -ForegroundColor Yellow } |
| 43 | + |
| 44 | +function Confirm-Step { |
| 45 | + param([string]$Prompt) |
| 46 | + if ($script:SkipConfirmation -or $script:DryRun) { return } |
| 47 | + Write-Host "" |
| 48 | + $response = Read-Host " $Prompt (y/N)" |
| 49 | + if ($response -notin @("y", "Y", "yes", "Yes")) { |
| 50 | + Write-Warn "Release cancelled by user." |
| 51 | + exit 0 |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +function Invoke-GitOrDryRun { |
| 56 | + param([string]$Description, [string[]]$Arguments) |
| 57 | + if ($DryRun) { |
| 58 | + Write-Warn "[DRY RUN] git $($Arguments -join ' ')" |
| 59 | + } else { |
| 60 | + Write-Info "git $($Arguments -join ' ')" |
| 61 | + & git @Arguments |
| 62 | + if ($LASTEXITCODE -ne 0) { |
| 63 | + throw "git $($Arguments -join ' ') failed with exit code $LASTEXITCODE" |
| 64 | + } |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +function Invoke-GhOrDryRun { |
| 69 | + param([string]$Description, [string[]]$Arguments) |
| 70 | + if ($DryRun) { |
| 71 | + Write-Warn "[DRY RUN] gh $($Arguments -join ' ')" |
| 72 | + } else { |
| 73 | + Write-Info "gh $($Arguments -join ' ')" |
| 74 | + & gh @Arguments |
| 75 | + if ($LASTEXITCODE -ne 0) { |
| 76 | + throw "gh $($Arguments -join ' ') failed with exit code $LASTEXITCODE" |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +# ─── Pre-flight checks ───────────────────────────────────────────────────────── |
| 82 | + |
| 83 | +Push-Location $ProjectRoot |
| 84 | +try { |
| 85 | + Write-Host "" |
| 86 | + Write-Host "╔══════════════════════════════════════════════╗" -ForegroundColor Magenta |
| 87 | + Write-Host "║ Windows App Development CLI - Release ║" -ForegroundColor Magenta |
| 88 | + Write-Host "╚══════════════════════════════════════════════╝" -ForegroundColor Magenta |
| 89 | + |
| 90 | + if ($DryRun) { |
| 91 | + Write-Host "" |
| 92 | + Write-Warn "DRY RUN MODE — no changes will be made" |
| 93 | + } |
| 94 | + |
| 95 | + # 1. Check we are on main |
| 96 | + Write-Step "Checking current branch..." |
| 97 | + $currentBranch = (git rev-parse --abbrev-ref HEAD).Trim() |
| 98 | + if ($currentBranch -ne "main") { |
| 99 | + Write-Error "You must be on the 'main' branch to start a release. Current branch: '$currentBranch'. Please run: git checkout main" |
| 100 | + exit 1 |
| 101 | + } |
| 102 | + Write-Ok "On branch: main" |
| 103 | + |
| 104 | + # 2. Check for clean working tree |
| 105 | + Write-Step "Checking working tree..." |
| 106 | + $status = git status --porcelain |
| 107 | + if ($status) { |
| 108 | + Write-Error "Working tree is not clean. Please commit or stash your changes first." |
| 109 | + exit 1 |
| 110 | + } |
| 111 | + Write-Ok "Working tree is clean" |
| 112 | + |
| 113 | + # 3. Pull latest from origin |
| 114 | + Write-Step "Pulling latest from origin/main..." |
| 115 | + Invoke-GitOrDryRun -Description "Fetch and pull latest" -Arguments @("pull", "--ff-only", "origin", "main") |
| 116 | + Write-Ok "Up to date with origin/main" |
| 117 | + |
| 118 | + # 4. Read version from version.json |
| 119 | + Write-Step "Reading version from version.json..." |
| 120 | + if (-not (Test-Path $VersionFilePath)) { |
| 121 | + Write-Error "version.json not found at: $VersionFilePath" |
| 122 | + exit 1 |
| 123 | + } |
| 124 | + |
| 125 | + $versionJson = Get-Content $VersionFilePath -Raw | ConvertFrom-Json |
| 126 | + $releaseVersion = $versionJson.version |
| 127 | + if (-not $releaseVersion) { |
| 128 | + Write-Error "Could not read 'version' property from version.json" |
| 129 | + exit 1 |
| 130 | + } |
| 131 | + Write-Ok "Release version: $releaseVersion" |
| 132 | + |
| 133 | + Confirm-Step "Is '$releaseVersion' the correct version to release?" |
| 134 | + |
| 135 | + # Parse version components |
| 136 | + $versionParts = $releaseVersion -split '\.' |
| 137 | + if ($versionParts.Count -ne 3) { |
| 138 | + Write-Error "Version '$releaseVersion' is not in the expected Major.Minor.Patch format" |
| 139 | + exit 1 |
| 140 | + } |
| 141 | + $major = [int]$versionParts[0] |
| 142 | + $minor = [int]$versionParts[1] |
| 143 | + $patch = [int]$versionParts[2] |
| 144 | + |
| 145 | + $releaseBranch = "rel/v$releaseVersion" |
| 146 | + $nextPatch = $patch + 1 |
| 147 | + $nextVersion = "$major.$minor.$nextPatch" |
| 148 | + $bumpBranch = "bump/v$nextVersion" |
| 149 | + |
| 150 | + # 5. Check that the release branch doesn't already exist |
| 151 | + Write-Step "Checking for existing release branch..." |
| 152 | + $existingRemoteBranch = git ls-remote --heads origin $releaseBranch 2>$null |
| 153 | + if ($existingRemoteBranch) { |
| 154 | + Write-Error "Release branch '$releaseBranch' already exists on origin. Has this version already been released?" |
| 155 | + exit 1 |
| 156 | + } |
| 157 | + Write-Ok "Branch '$releaseBranch' does not exist yet — good to go" |
| 158 | + |
| 159 | + # 6. Confirm with user |
| 160 | + Write-Host "" |
| 161 | + Write-Host " ┌─────────────────────────────────────────────┐" -ForegroundColor White |
| 162 | + Write-Host " │ Release Plan │" -ForegroundColor White |
| 163 | + Write-Host " │ │" -ForegroundColor White |
| 164 | + Write-Host " │ Release version : $releaseVersion$((' ' * (25 - $releaseVersion.Length)))│" -ForegroundColor White |
| 165 | + Write-Host " │ Release branch : $releaseBranch$((' ' * (25 - $releaseBranch.Length)))│" -ForegroundColor White |
| 166 | + Write-Host " │ Next dev version: $nextVersion$((' ' * (25 - $nextVersion.Length)))│" -ForegroundColor White |
| 167 | + Write-Host " │ Bump branch : $bumpBranch$((' ' * (25 - $bumpBranch.Length)))│" -ForegroundColor White |
| 168 | + Write-Host " │ │" -ForegroundColor White |
| 169 | + Write-Host " │ Steps: │" -ForegroundColor White |
| 170 | + Write-Host " │ 1. Create & push $releaseBranch$((' ' * (18 - $releaseBranch.Length)))│" -ForegroundColor White |
| 171 | + Write-Host " │ 2. Bump version.json to $nextVersion$((' ' * (12 - $nextVersion.Length)))│" -ForegroundColor White |
| 172 | + Write-Host " │ 3. Create PR to merge bump into main │" -ForegroundColor White |
| 173 | + Write-Host " └─────────────────────────────────────────────┘" -ForegroundColor White |
| 174 | + |
| 175 | + Confirm-Step "Does this release plan look correct? Proceed?" |
| 176 | + |
| 177 | + # ─── Step 1: Create and push the release branch ───────────────────────────── |
| 178 | + |
| 179 | + Write-Step "Step 1/3: Creating release branch '$releaseBranch'..." |
| 180 | + Invoke-GitOrDryRun -Description "Create release branch" -Arguments @("checkout", "-b", $releaseBranch) |
| 181 | + Write-Ok "Local branch '$releaseBranch' created" |
| 182 | + |
| 183 | + Confirm-Step "Push '$releaseBranch' to origin? This will kick off the release pipeline" |
| 184 | + |
| 185 | + Invoke-GitOrDryRun -Description "Push release branch" -Arguments @("push", "-u", "origin", $releaseBranch) |
| 186 | + Write-Ok "Release branch '$releaseBranch' pushed to origin" |
| 187 | + |
| 188 | + # ─── Step 2: Go back to main and bump the version ─────────────────────────── |
| 189 | + |
| 190 | + Write-Step "Step 2/3: Bumping version to $nextVersion..." |
| 191 | + Invoke-GitOrDryRun -Description "Switch back to main" -Arguments @("checkout", "main") |
| 192 | + |
| 193 | + # Create the bump branch from main |
| 194 | + Invoke-GitOrDryRun -Description "Create bump branch" -Arguments @("checkout", "-b", $bumpBranch) |
| 195 | + |
| 196 | + # Update version.json |
| 197 | + if ($DryRun) { |
| 198 | + Write-Warn "[DRY RUN] Would update version.json: $releaseVersion -> $nextVersion" |
| 199 | + } else { |
| 200 | + $newVersionJson = @{ version = $nextVersion } | ConvertTo-Json |
| 201 | + Set-Content -Path $VersionFilePath -Value $newVersionJson -NoNewline |
| 202 | + Write-Info "Updated version.json: $releaseVersion -> $nextVersion" |
| 203 | + } |
| 204 | + |
| 205 | + # Commit and push |
| 206 | + Invoke-GitOrDryRun -Description "Stage version.json" -Arguments @("add", "version.json") |
| 207 | + Invoke-GitOrDryRun -Description "Commit version bump" -Arguments @("commit", "-m", "Bump version to $nextVersion for development") |
| 208 | + |
| 209 | + Confirm-Step "Push version bump branch '$bumpBranch' to origin and create PR?" |
| 210 | + |
| 211 | + Invoke-GitOrDryRun -Description "Push bump branch" -Arguments @("push", "-u", "origin", $bumpBranch) |
| 212 | + Write-Ok "Bump branch '$bumpBranch' pushed to origin" |
| 213 | + |
| 214 | + # ─── Step 3: Create a PR for the version bump ─────────────────────────────── |
| 215 | + |
| 216 | + Write-Step "Step 3/3: Creating pull request..." |
| 217 | + |
| 218 | + # Check that gh CLI is available |
| 219 | + $ghAvailable = Get-Command gh -ErrorAction SilentlyContinue |
| 220 | + if (-not $ghAvailable -and -not $DryRun) { |
| 221 | + Write-Warn "GitHub CLI (gh) is not installed. Please create the PR manually:" |
| 222 | + Write-Warn " gh pr create --base main --head $bumpBranch --title 'Bump version to $nextVersion' --body 'Auto-generated version bump after releasing v$releaseVersion.'" |
| 223 | + } else { |
| 224 | + Invoke-GhOrDryRun -Description "Create pull request" -Arguments @( |
| 225 | + "pr", "create", |
| 226 | + "--base", "main", |
| 227 | + "--head", $bumpBranch, |
| 228 | + "--title", "Bump version to $nextVersion for development", |
| 229 | + "--body", "Auto-generated version bump after releasing v$releaseVersion.`n`nThis PR bumps the patch version in ``version.json`` from ``$releaseVersion`` to ``$nextVersion`` so that prerelease builds pick up the new version number." |
| 230 | + ) |
| 231 | + Write-Ok "Pull request created" |
| 232 | + } |
| 233 | + |
| 234 | + # ─── Done ─────────────────────────────────────────────────────────────────── |
| 235 | + |
| 236 | + # Return to main so you're in a good state |
| 237 | + Invoke-GitOrDryRun -Description "Switch back to main" -Arguments @("checkout", "main") |
| 238 | + |
| 239 | + Write-Host "" |
| 240 | + Write-Host "╔══════════════════════════════════════════════╗" -ForegroundColor Green |
| 241 | + Write-Host "║ Release started successfully! ║" -ForegroundColor Green |
| 242 | + Write-Host "╠══════════════════════════════════════════════╣" -ForegroundColor Green |
| 243 | + Write-Host "║ ║" -ForegroundColor Green |
| 244 | + Write-Host "║ • Release branch '$releaseBranch' pushed$((' ' * (14 - $releaseBranch.Length)))║" -ForegroundColor Green |
| 245 | + Write-Host "║ • Version bump PR created for $nextVersion$((' ' * (10 - $nextVersion.Length)))║" -ForegroundColor Green |
| 246 | + Write-Host "║ ║" -ForegroundColor Green |
| 247 | + Write-Host "║ Next steps: ║" -ForegroundColor Green |
| 248 | + Write-Host "║ 1. Monitor the release pipeline ║" -ForegroundColor Green |
| 249 | + Write-Host "║ 2. Review & merge the version bump PR ║" -ForegroundColor Green |
| 250 | + Write-Host "╚══════════════════════════════════════════════╝" -ForegroundColor Green |
| 251 | + Write-Host "" |
| 252 | + |
| 253 | +} catch { |
| 254 | + Write-Host "" |
| 255 | + Write-Host "ERROR: $_" -ForegroundColor Red |
| 256 | + Write-Host "" |
| 257 | + Write-Warn "The release process did not complete. You may need to manually clean up:" |
| 258 | + Write-Warn " - Check your current branch: git rev-parse --abbrev-ref HEAD" |
| 259 | + Write-Warn " - Switch back to main: git checkout main" |
| 260 | + if ($releaseBranch) { |
| 261 | + Write-Warn " - Delete local release branch: git branch -D $releaseBranch" |
| 262 | + } |
| 263 | + if ($bumpBranch) { |
| 264 | + Write-Warn " - Delete local bump branch: git branch -D $bumpBranch" |
| 265 | + Write-Warn " - Restore version.json: git checkout -- version.json" |
| 266 | + } |
| 267 | + |
| 268 | + exit 1 |
| 269 | +} finally { |
| 270 | + Pop-Location |
| 271 | +} |
0 commit comments