Skip to content

Commit 59c2e7a

Browse files
authored
Refactor integration roundtrip into phased scripts
1 parent 971e7c7 commit 59c2e7a

5 files changed

Lines changed: 573 additions & 697 deletions

File tree

.github/workflows/integration-test.yml

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,9 @@ jobs:
120120
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
121121
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
122122

123-
- name: Run Round-Trip Test
123+
- name: Run Round-Trip Phase 1 (Deploy)
124124
shell: pwsh
125125
run: |
126-
$skipTeardown = '${{ inputs.skip_teardown }}' -eq 'true'
127126
$logLevel = '${{ inputs.log_level }}'
128127
if ([string]::IsNullOrWhiteSpace($logLevel)) { $logLevel = 'Verbose' }
129128
if ($logLevel -notin @('Info', 'Verbose', 'Debug')) {
@@ -137,12 +136,35 @@ jobs:
137136
Location = '${{ inputs.location }}'
138137
LogLevel = $logLevel
139138
PublisherEmail = '${{ secrets.APIM_PUBLISHER_EMAIL }}'
140-
ExtractOutputDir = './extracted-artifacts'
141-
HardDelete = $true
139+
StateFile = './roundtrip-state.json'
142140
}
143-
if ($skipTeardown) { $params.SkipTeardown = $true }
141+
./tests/integration/all-resource-types/run-roundtrip-phase1-deploy.ps1 @params
144142
145-
./tests/integration/all-resource-types/run-roundtrip-test.ps1 @params
143+
- name: Azure Login (OIDC) - Refresh Before Phase 2
144+
if: success()
145+
uses: azure/login@v2
146+
with:
147+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
148+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
149+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
150+
151+
- name: Run Round-Trip Phase 2 (Extract→Publish→Verify)
152+
if: success()
153+
shell: pwsh
154+
env:
155+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
156+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
157+
run: |
158+
$logLevel = '${{ inputs.log_level }}'
159+
if ([string]::IsNullOrWhiteSpace($logLevel)) { $logLevel = 'Verbose' }
160+
if ($logLevel -notin @('Info', 'Verbose', 'Debug')) {
161+
throw "Invalid log_level '$logLevel'. Allowed values: Info, Verbose, Debug."
162+
}
163+
164+
./tests/integration/all-resource-types/run-roundtrip-phase2-roundtrip.ps1 `
165+
-StateFile './roundtrip-state.json' `
166+
-LogLevel $logLevel `
167+
-ExtractOutputDir './extracted-artifacts'
146168
147169
- name: Upload Extracted Artifacts
148170
if: always()
@@ -152,40 +174,12 @@ jobs:
152174
path: ./extracted-artifacts/
153175
if-no-files-found: ignore
154176

155-
- name: Emergency Teardown
156-
if: failure() && inputs.skip_teardown != true
177+
- name: Run Round-Trip Phase 3 (Teardown)
178+
if: always() && inputs.skip_teardown != true
157179
shell: pwsh
158180
run: |
159-
$sourceRg = '${{ env.SOURCE_RG }}'
160-
$targetRg = '${{ env.TARGET_RG }}'
161-
$location = '${{ inputs.location }}'
162-
163-
# Capture APIM names before RG deletion so we can purge soft-deleted services.
164-
$sourceApimName = az apim list --resource-group $sourceRg --query "[0].name" -o tsv 2>$null
165-
$targetApimName = az apim list --resource-group $targetRg --query "[0].name" -o tsv 2>$null
166-
167-
Write-Host "🚨 Emergency teardown — deleting resource groups..."
168-
az group delete --name $sourceRg --yes --no-wait 2>$null
169-
az group delete --name $targetRg --yes --no-wait 2>$null
170-
171-
Write-Host "⏳ Waiting for resource group deletions before APIM purge..."
172-
$maxWaitSeconds = 900
173-
$waited = 0
174-
$interval = 30
175-
176-
while ($waited -lt $maxWaitSeconds) {
177-
$srcExists = (az group exists --name $sourceRg -o tsv 2>$null) -eq 'true'
178-
$tgtExists = (az group exists --name $targetRg -o tsv 2>$null) -eq 'true'
179-
if (-not $srcExists -and -not $tgtExists) {
180-
break
181-
}
182-
Start-Sleep -Seconds $interval
183-
$waited += $interval
184-
}
185-
186-
foreach ($apimName in @($sourceApimName, $targetApimName)) {
187-
if (-not [string]::IsNullOrWhiteSpace($apimName)) {
188-
Write-Host "🗑️ Purging soft-deleted APIM: $apimName"
189-
az apim deletedservice purge --service-name $apimName --location $location 2>$null
190-
}
191-
}
181+
./tests/integration/all-resource-types/run-roundtrip-phase3-teardown.ps1 `
182+
-SourceResourceGroup '${{ env.SOURCE_RG }}' `
183+
-TargetResourceGroup '${{ env.TARGET_RG }}' `
184+
-Location '${{ inputs.location }}' `
185+
-HardDelete
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#requires -Version 7.0
2+
3+
[CmdletBinding()]
4+
param(
5+
[Parameter(Mandatory)]
6+
[string]$SourceResourceGroup,
7+
8+
[Parameter(Mandatory)]
9+
[string]$TargetResourceGroup,
10+
11+
[ValidateSet('Developer', 'Premium', 'StandardV2', 'PremiumV2')]
12+
[string]$SkuName = 'StandardV2',
13+
14+
[string]$Location = 'eastus2',
15+
16+
[ValidateSet('Info', 'Verbose', 'Debug')]
17+
[string]$LogLevel = 'Verbose',
18+
19+
[Parameter(Mandatory)]
20+
[string]$PublisherEmail,
21+
22+
[Parameter(Mandatory)]
23+
[string]$StateFile
24+
)
25+
26+
$ErrorActionPreference = 'Stop'
27+
$VerbosePreference = if ($LogLevel -in @('Verbose', 'Debug')) { 'Continue' } else { 'SilentlyContinue' }
28+
$DebugPreference = if ($LogLevel -eq 'Debug') { 'Continue' } else { 'SilentlyContinue' }
29+
30+
$maskingModule = Join-Path $PSScriptRoot 'MaskingHelpers.psm1'
31+
Import-Module $maskingModule -Force
32+
33+
$account = az account show --output json 2>$null | ConvertFrom-Json
34+
if (-not $account) {
35+
Write-Error "Not logged in to Azure CLI. Run 'az login' first."
36+
exit 2
37+
}
38+
39+
$subscriptionId = $account.id
40+
Write-Host "🔐 Azure CLI authenticated: $($account.name) ($(Protect-SubscriptionId -Value $subscriptionId))"
41+
42+
$deploySourceScript = Join-Path $PSScriptRoot 'Deploy-SourceApim.ps1'
43+
$deployTargetScript = Join-Path $PSScriptRoot 'Deploy-TargetApim.ps1'
44+
45+
foreach ($requiredFile in @($maskingModule, $deploySourceScript, $deployTargetScript)) {
46+
if (-not (Test-Path $requiredFile)) {
47+
Write-Error "Required file not found: $requiredFile"
48+
exit 2
49+
}
50+
}
51+
52+
$logsDir = Join-Path $PSScriptRoot 'logs'
53+
if (-not (Test-Path $logsDir)) {
54+
New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
55+
}
56+
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
57+
$sourceLogFile = Join-Path $logsDir "source-deploy-$timestamp.log"
58+
$targetLogFile = Join-Path $logsDir "target-deploy-$timestamp.log"
59+
60+
Write-Host "🚀 PHASE 1 — Deploy source and target APIM instances (parallel)"
61+
62+
$sourceJob = Start-Job -Name 'DeploySource' -ScriptBlock {
63+
param($script, $rg, $sku, $loc, $email, $transcriptFile, $logLevel)
64+
$ErrorActionPreference = 'Stop'
65+
Start-Transcript -Path $transcriptFile -Force -UseMinimalHeader | Out-Null
66+
try {
67+
$scriptArgs = @{
68+
ResourceGroupName = $rg
69+
SkuName = $sku
70+
Location = $loc
71+
PublisherEmail = $email
72+
LogLevel = $logLevel
73+
}
74+
$result = & $script @scriptArgs
75+
if (-not $result -or -not $result.apimServiceName) {
76+
throw "Source deployment returned no outputs"
77+
}
78+
return $result
79+
} finally {
80+
Stop-Transcript | Out-Null
81+
}
82+
} -ArgumentList $deploySourceScript, $SourceResourceGroup, $SkuName, $Location, $PublisherEmail, $sourceLogFile, $LogLevel
83+
84+
$targetJob = Start-Job -Name 'DeployTarget' -ScriptBlock {
85+
param($script, $rg, $sku, $loc, $email, $transcriptFile, $logLevel)
86+
$ErrorActionPreference = 'Stop'
87+
Start-Transcript -Path $transcriptFile -Force -UseMinimalHeader | Out-Null
88+
try {
89+
$scriptArgs = @{
90+
ResourceGroupName = $rg
91+
SkuName = $sku
92+
Location = $loc
93+
PublisherEmail = $email
94+
LogLevel = $logLevel
95+
}
96+
$result = & $script @scriptArgs
97+
if (-not $result -or -not $result.apimServiceName -or -not $result.apimServiceName.value) {
98+
throw "Target deployment returned no outputs"
99+
}
100+
return $result
101+
} finally {
102+
Stop-Transcript | Out-Null
103+
}
104+
} -ArgumentList $deployTargetScript, $TargetResourceGroup, $SkuName, $Location, $PublisherEmail, $targetLogFile, $LogLevel
105+
106+
$jobs = @($sourceJob, $targetJob)
107+
$sourceLastPos = 0
108+
$targetLastPos = 0
109+
110+
while (($jobs | Where-Object { $_.State -eq 'Running' }).Count -gt 0) {
111+
if (Test-Path $sourceLogFile) {
112+
$content = Get-Content $sourceLogFile -Raw -ErrorAction SilentlyContinue
113+
if ($content -and $content.Length -gt $sourceLastPos) {
114+
$newContent = $content.Substring($sourceLastPos)
115+
$newContent -split "`n" | Where-Object { $_.Trim() } | ForEach-Object { Write-Host " [SRC] $_" }
116+
$sourceLastPos = $content.Length
117+
}
118+
}
119+
120+
if (Test-Path $targetLogFile) {
121+
$content = Get-Content $targetLogFile -Raw -ErrorAction SilentlyContinue
122+
if ($content -and $content.Length -gt $targetLastPos) {
123+
$newContent = $content.Substring($targetLastPos)
124+
$newContent -split "`n" | Where-Object { $_.Trim() } | ForEach-Object { Write-Host " [TGT] $_" }
125+
$targetLastPos = $content.Length
126+
}
127+
}
128+
129+
Start-Sleep -Seconds 5
130+
}
131+
132+
$sourceOutputs = $null
133+
$targetOutputs = $null
134+
$exitCode = 0
135+
136+
if ($sourceJob.State -eq 'Failed') {
137+
Write-Host "❌ Source deployment failed:"
138+
$jobOutput = Receive-Job $sourceJob -ErrorAction SilentlyContinue 2>&1
139+
if ($jobOutput) { $jobOutput | ForEach-Object { Write-Host " $_" } }
140+
if ($sourceJob.ChildJobs[0].Error) {
141+
$sourceJob.ChildJobs[0].Error | ForEach-Object { Write-Host " $_" }
142+
}
143+
$exitCode = 2
144+
} else {
145+
$sourceOutputs = Receive-Job $sourceJob
146+
Write-Host " ✅ Source deployed: $(Protect-ApimName -Value $sourceOutputs.apimServiceName)"
147+
}
148+
Remove-Job $sourceJob
149+
150+
if ($targetJob.State -eq 'Failed') {
151+
Write-Host "❌ Target deployment failed:"
152+
$jobOutput = Receive-Job $targetJob -ErrorAction SilentlyContinue 2>&1
153+
if ($jobOutput) { $jobOutput | ForEach-Object { Write-Host " $_" } }
154+
if ($targetJob.ChildJobs[0].Error) {
155+
$targetJob.ChildJobs[0].Error | ForEach-Object { Write-Host " $_" }
156+
}
157+
$exitCode = 2
158+
} else {
159+
$targetOutputs = Receive-Job $targetJob
160+
Write-Host " ✅ Target deployed: $(Protect-ApimName -Value $targetOutputs.apimServiceName.value)"
161+
}
162+
Remove-Job $targetJob
163+
164+
if ($exitCode -ne 0) {
165+
exit $exitCode
166+
}
167+
168+
$sourceSubId = if ($sourceOutputs -and $sourceOutputs.subscriptionId) { $sourceOutputs.subscriptionId } else { $subscriptionId }
169+
$sourceRg = if ($sourceOutputs -and $sourceOutputs.resourceGroup) { $sourceOutputs.resourceGroup } else { $SourceResourceGroup }
170+
$sourceName = if ($sourceOutputs -and $sourceOutputs.apimServiceName) { $sourceOutputs.apimServiceName } else {
171+
$apimName = az apim list --resource-group $SourceResourceGroup --query "[0].name" -o tsv 2>$null
172+
if (-not $apimName) {
173+
Write-Host "❌ No APIM instance found in resource group $(Protect-ResourceGroupName -Value $SourceResourceGroup)"
174+
exit 2
175+
}
176+
$apimName
177+
}
178+
179+
$targetSubId = if ($targetOutputs -and $targetOutputs.subscriptionId.value) { $targetOutputs.subscriptionId.value } else { $subscriptionId }
180+
$targetRg = if ($targetOutputs -and $targetOutputs.resourceGroupName.value) { $targetOutputs.resourceGroupName.value } else { $TargetResourceGroup }
181+
$targetName = if ($targetOutputs -and $targetOutputs.apimServiceName.value) { $targetOutputs.apimServiceName.value } else {
182+
$apimName = az apim list --resource-group $TargetResourceGroup --query "[0].name" -o tsv 2>$null
183+
if (-not $apimName) {
184+
Write-Host "❌ No APIM instance found in resource group $(Protect-ResourceGroupName -Value $TargetResourceGroup)"
185+
exit 2
186+
}
187+
$apimName
188+
}
189+
190+
if ($SkuName -in @('Developer', 'Premium')) {
191+
$postActivationState = az deployment group list `
192+
--resource-group $sourceRg `
193+
--query "sort_by([?starts_with(name, 'source-apim-post-activation-')], &properties.timestamp)[-1].properties.provisioningState" `
194+
-o tsv 2>$null
195+
if (-not $postActivationState) {
196+
Write-Host "❌ Could not find source-apim-post-activation deployment in $(Protect-ResourceGroupName -Value $sourceRg)"
197+
exit 2
198+
}
199+
if ($postActivationState -ne 'Succeeded') {
200+
Write-Host "❌ source-apim-post-activation deployment state is '$postActivationState' (expected 'Succeeded')"
201+
exit 2
202+
}
203+
Write-Host " ✅ source-apim-post-activation deployment confirmed"
204+
}
205+
206+
$state = [ordered]@{
207+
sourceSubscriptionId = $sourceSubId
208+
sourceResourceGroup = $sourceRg
209+
sourceApimName = $sourceName
210+
targetSubscriptionId = $targetSubId
211+
targetResourceGroup = $targetRg
212+
targetApimName = $targetName
213+
skuName = $SkuName
214+
location = $Location
215+
}
216+
217+
$stateDir = Split-Path -Parent $StateFile
218+
if (-not [string]::IsNullOrWhiteSpace($stateDir) -and -not (Test-Path $stateDir)) {
219+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
220+
}
221+
222+
$state | ConvertTo-Json -Depth 5 | Set-Content -Path $StateFile -Encoding utf8
223+
Write-Host "✅ PHASE 1 complete — state saved to $StateFile"
224+
exit 0

0 commit comments

Comments
 (0)