Public/Switch-TaxEditorRevision.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Switch-TaxEditorRevision { <# .SYNOPSIS Shifts ACA traffic to a specific or previous revision (T1 rollback). .DESCRIPTION Moves 100% traffic to the target revision, deactivates the previously active revision, then runs Test-TaxEditorHealth to verify. Supports -WhatIf/-Confirm for safe operation. .PARAMETER Revision The exact revision name to switch to. .PARAMETER Previous Switch to the most recent non-active revision (one-command rollback). .PARAMETER ResourceGroup Azure resource group name. Default: ai-triad. .PARAMETER AppName Container App name. Default: taxonomy-editor. .PARAMETER BaseUrl URL for post-switch health check. Default: production. .PARAMETER SkipHealthCheck Skip the post-switch health check. .EXAMPLE Switch-TaxEditorRevision -Previous .EXAMPLE Switch-TaxEditorRevision -Revision 'taxonomy-editor--deploy-abc1234' .EXAMPLE Switch-TaxEditorRevision -Previous -WhatIf .LINK Show-AITriadHelp .LINK Deploy-TaxEditorImage .LINK Deploy-TaxEditorInfra .LINK Get-TaxEditorImage .LINK Get-TaxEditorRevision .LINK Test-TaxEditorInfra .LINK Invoke-TaxEditorSmokeTest #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'ByName')] param( [Parameter(Mandatory, ParameterSetName = 'ByName', Position = 0)] [string]$Revision, [Parameter(Mandatory, ParameterSetName = 'Previous')] [switch]$Previous, [Parameter()] [string]$ResourceGroup = 'ai-triad', [Parameter()] [string]$AppName = 'taxonomy-editor', [Parameter()] [string]$BaseUrl = (Get-TaxEditorBaseUrl), [Parameter()] [switch]$SkipHealthCheck ) Set-StrictMode -Version Latest # ── Validate az CLI ────────────────────────────────────────────────── $AzCmd = Get-Command az -ErrorAction SilentlyContinue if (-not $AzCmd) { throw (New-ActionableError ` -Goal 'Switch ACA revision' ` -Problem 'Azure CLI (az) not found on PATH' ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @('Install Azure CLI: https://aka.ms/installazurecli')) } $AccountJson = & az account show --output json 2>$null if ($LASTEXITCODE -ne 0 -or -not $AccountJson) { throw (New-ActionableError ` -Goal 'Switch ACA revision' ` -Problem 'Azure CLI is not logged in' ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @('Run: az login', 'Verify subscription: az account show')) } # ── Get current revisions ──────────────────────────────────────────── $Revisions = @(Get-TaxEditorRevision -ResourceGroup $ResourceGroup -AppName $AppName) if ($Revisions.Count -eq 0) { throw (New-ActionableError ` -Goal 'Switch ACA revision' ` -Problem "No revisions found for '$AppName' in '$ResourceGroup'" ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @("Verify app exists: az containerapp show -g $ResourceGroup -n $AppName")) } $CurrentActive = @($Revisions | Where-Object { $_.Active }) # ── Resolve target revision ────────────────────────────────────────── $TargetName = $null if ($Previous) { $Inactive = @($Revisions | Where-Object { -not $_.Active } | Sort-Object { $_.CreatedAt } -Descending) if ($Inactive.Count -eq 0) { throw (New-ActionableError ` -Goal 'Roll back to previous revision' ` -Problem 'No inactive revisions found — nothing to roll back to' ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @('Use Get-TaxEditorRevision to inspect available revisions', 'There may only be one revision deployed')) } $TargetName = $Inactive[0].Name } else { $Match = @($Revisions | Where-Object { $_.Name -eq $Revision }) if ($Match.Count -eq 0) { throw (New-ActionableError ` -Goal "Switch to revision '$Revision'" ` -Problem "Revision '$Revision' not found" ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @('Run Get-TaxEditorRevision to list available revisions', 'Verify the revision name is spelled correctly')) } $TargetName = $Revision } # ── Build action description ───────────────────────────────────────── $CurrentNames = if ($CurrentActive.Count -gt 0) { ($CurrentActive | ForEach-Object { $_.Name }) -join ', ' } else { '(none)' } $ActionDesc = "Shift 100% traffic to '$TargetName' (currently active: $CurrentNames)" if (-not $PSCmdlet.ShouldProcess($AppName, $ActionDesc)) { return } # ── Activate target revision if not running ────────────────────────── $TargetRev = @($Revisions | Where-Object { $_.Name -eq $TargetName }) if ($TargetRev.Count -gt 0 -and $TargetRev[0].RunningState -ne 'Running') { Write-Verbose "Activating revision '$TargetName'..." & az containerapp revision activate -g $ResourceGroup -n $AppName --revision $TargetName 2>$null if ($LASTEXITCODE -ne 0) { throw (New-ActionableError ` -Goal "Activate revision '$TargetName'" ` -Problem "az containerapp revision activate failed (exit code $LASTEXITCODE)" ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @("Check revision state: az containerapp revision show -g $ResourceGroup -n $AppName --revision $TargetName")) } } # ── Shift traffic ──────────────────────────────────────────────────── Write-Verbose "Shifting 100% traffic to '$TargetName'..." & az containerapp ingress traffic set -g $ResourceGroup -n $AppName ` --revision-weight "${TargetName}=100" 2>$null if ($LASTEXITCODE -ne 0) { throw (New-ActionableError ` -Goal "Shift traffic to '$TargetName'" ` -Problem "az containerapp ingress traffic set failed (exit code $LASTEXITCODE)" ` -Location 'Switch-TaxEditorRevision' ` -NextSteps @("Verify revision is active: az containerapp revision show -g $ResourceGroup -n $AppName --revision $TargetName", "Check ingress config: az containerapp ingress show -g $ResourceGroup -n $AppName")) } # ── Deactivate old active revisions ────────────────────────────────── foreach ($Old in $CurrentActive) { if ($Old.Name -ne $TargetName) { Write-Verbose "Deactivating previous revision '$($Old.Name)'..." & az containerapp revision deactivate -g $ResourceGroup -n $AppName ` --revision $Old.Name 2>$null if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to deactivate revision '$($Old.Name)' — traffic already shifted, manual cleanup needed." } } } # ── Post-switch health check ───────────────────────────────────────── if (-not $SkipHealthCheck) { Write-Verbose 'Running post-switch health check...' $Health = Test-TaxEditorHealth -BaseUrl $BaseUrl if (-not $Health.Healthy) { Write-Warning "Post-switch health check FAILED — the target revision may not be healthy. Run Test-TaxEditorHealth for details." } else { Write-Verbose "Post-switch health check passed (avg $($Health.AverageMs)ms)." } } [PSCustomObject]@{ Action = 'RevisionSwitch' TargetRevision = $TargetName PreviousRevision = $CurrentNames AppName = $AppName ResourceGroup = $ResourceGroup HealthCheck = if ($SkipHealthCheck) { 'Skipped' } elseif ($Health.Healthy) { 'Passed' } else { 'Failed' } Timestamp = (Get-Date).ToString('o') } } |