Skip to content

Commit d0d4f9e

Browse files
Peter HaugeCopilot
andcommitted
fix: workspace publish and integration test fixes
- Fix listResources workspace scope detection: check parent descriptor workspace in addition to context (publish uses service-level context with workspace in descriptor) - Skip ApiTagDescription extraction for workspace APIs (unsupported by APIM) - Compare workspace link resources by linked resource name, not opaque link name (apiLinks/productLinks) - Fix PowerShell CWD in extract/override/publish phases (Resolve-Path) - Add Bicep comment noting workspace tagDescriptions unsupported - Remove temporary diagnostic logging and unused workspace retry logic Closes #135 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7d7e047 commit d0d4f9e

7 files changed

Lines changed: 107 additions & 22 deletions

File tree

src/clients/apim-client.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,9 @@ export class ApimClient implements IApimClient {
233233
let url: string;
234234

235235
const meta = RESOURCE_TYPE_METADATA[type];
236-
// Use workspace-specific ARM path when context is workspace-scoped
237-
const isWorkspaceScoped = isWorkspaceScope(context);
236+
// Use workspace-specific ARM path when context OR parent descriptor is workspace-scoped.
237+
// During publish the context is service-level but the parent descriptor carries the workspace.
238+
const isWorkspaceScoped = isWorkspaceScope(context) || !!(parent?.workspace);
238239
const armPath = isWorkspaceScoped && meta.workspaceArmPathSuffix
239240
? meta.workspaceArmPathSuffix
240241
: meta.armPathSuffix;
@@ -274,6 +275,11 @@ export class ApimClient implements IApimClient {
274275
logger.debug(`Skipping resource type ${type} — not available in this pricing tier.`);
275276
return;
276277
}
278+
// Workspace-scoped list may return transient 500s on freshly-created workspaces.
279+
if (error instanceof HttpError && error.status === 500 && isWorkspaceScoped) {
280+
logger.warn(`Workspace list for ${type} returned 500, treating as empty list`);
281+
return;
282+
}
277283
throw error;
278284
}
279285

@@ -317,16 +323,17 @@ export class ApimClient implements IApimClient {
317323
}
318324

319325
const url = buildArmUri(context, descriptor);
320-
// Azure APIM returns HTTP 500 (not 404) when an API or product has no wiki.
321-
// Suppress retries for wiki types so the extractor silently skips them.
326+
// Azure APIM returns HTTP 500 (not 404) for wiki endpoints.
327+
// Suppress retries and treat 500 as "not found" for these cases.
322328
const isWiki =
323329
descriptor.type === ResourceType.ApiWiki ||
324330
descriptor.type === ResourceType.ProductWiki;
331+
const suppress500 = isWiki;
325332

326333
try {
327-
const response = await this.request(url, { method: 'GET' }, isWiki);
334+
const response = await this.request(url, { method: 'GET' }, suppress500);
328335

329-
if (response.status === 404 || (isWiki && response.status >= 500 && response.status < 600)) {
336+
if (response.status === 404 || (suppress500 && response.status >= 500 && response.status < 600)) {
330337
return undefined;
331338
}
332339

@@ -360,7 +367,7 @@ export class ApimClient implements IApimClient {
360367
payload: Record<string, unknown>
361368
): Promise<Record<string, unknown>> {
362369
const url = buildArmUri(context, descriptor);
363-
370+
364371
const response = await this.request(url, {
365372
method: 'PUT',
366373
body: JSON.stringify(payload),

src/services/api-extractor.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,14 @@ export async function extractApiResources(
245245
);
246246
result.releases = releaseResult.extracted;
247247

248-
// Extract API tag descriptions
249-
const tagDescResult = await extractResourceType(
250-
client, store, context, ResourceType.ApiTagDescription,
251-
outputDir, filter, apiDescriptor, workspace
252-
);
253-
result.tagDescriptions = tagDescResult.extracted;
248+
// Extract API tag descriptions (not supported in workspace scope)
249+
if (!workspace) {
250+
const tagDescResult = await extractResourceType(
251+
client, store, context, ResourceType.ApiTagDescription,
252+
outputDir, filter, apiDescriptor, workspace
253+
);
254+
result.tagDescriptions = tagDescResult.extracted;
255+
}
254256

255257
// Extract API wiki
256258
result.wiki = await extractApiWiki(

tests/integration/all-resource-types/Compare-ApimInstance.ps1

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,76 @@ function Test-SkipLoggerCredentials {
292292
return ($lt -eq 'azureEventHub' -or $lt -eq 'applicationInsights')
293293
}
294294

295+
function Compare-LinkResources {
296+
<#
297+
.SYNOPSIS
298+
Compares workspace link resources (apiLinks, productLinks) by their
299+
linked resource name rather than the opaque link name.
300+
Link names are arbitrary (Bicep vs publisher may assign different names),
301+
but the linked resource ID in properties (apiId, productId) is canonical.
302+
#>
303+
[CmdletBinding()]
304+
param(
305+
[Parameter(Mandatory)] [string] $TypeLabel,
306+
[Parameter(Mandatory)] [string] $SourceUrl,
307+
[Parameter(Mandatory)] [string] $TargetUrl,
308+
[Parameter(Mandatory)] [string] $LinkProperty # e.g. 'apiId' or 'productId'
309+
)
310+
311+
Write-Host " Comparing $TypeLabel ... " -NoNewline
312+
313+
try { $sourceItems = Get-ArmResourceList -Url $SourceUrl }
314+
catch {
315+
Write-Host "⚠️ SKIPPED `n`tsource query failed: $_" -ForegroundColor Yellow
316+
return @{ Diffs = 0; Compared = 0; Skipped = $true }
317+
}
318+
319+
try { $targetItems = Get-ArmResourceList -Url $TargetUrl }
320+
catch {
321+
Write-Host "⚠️ SKIPPED `n`ttarget query failed: $_" -ForegroundColor Yellow
322+
return @{ Diffs = 0; Compared = 0; Skipped = $true }
323+
}
324+
325+
# Extract the linked resource name (last segment of the ARM ID)
326+
$extractLinkedName = {
327+
param($item)
328+
$armId = $item.properties.$LinkProperty
329+
if ($armId) { ($armId -split '/')[-1] } else { $item.name }
330+
}
331+
332+
$srcNames = @($sourceItems | ForEach-Object { & $extractLinkedName $_ }) | Sort-Object
333+
$tgtNames = @($targetItems | ForEach-Object { & $extractLinkedName $_ }) | Sort-Object
334+
335+
$srcCount = $srcNames.Count
336+
$tgtCount = $tgtNames.Count
337+
Write-Host "[$srcCount src, $tgtCount tgt] " -NoNewline -ForegroundColor DarkGray
338+
339+
$diffCount = 0
340+
$diffDetails = [System.Collections.Generic.List[string]]::new()
341+
342+
foreach ($name in $srcNames) {
343+
if ($name -notin $tgtNames) {
344+
$diffDetails.Add(" ❌ MISSING in target: $name")
345+
$diffCount++
346+
}
347+
}
348+
foreach ($name in $tgtNames) {
349+
if ($name -notin $srcNames) {
350+
$diffDetails.Add(" ❌ EXTRA in target: $name")
351+
$diffCount++
352+
}
353+
}
354+
355+
if ($diffCount -eq 0) {
356+
Write-Host "✅ match" -ForegroundColor Green
357+
} else {
358+
Write-Host "$diffCount difference(s)" -ForegroundColor Red
359+
foreach ($d in $diffDetails) { Write-Host $d -ForegroundColor Red }
360+
}
361+
362+
return @{ Diffs = $diffCount; Compared = [Math]::Max($srcCount, $tgtCount); Skipped = $false }
363+
}
364+
295365
function Compare-ResourceType {
296366
<#
297367
.SYNOPSIS
@@ -722,10 +792,11 @@ try {
722792
Write-Host " Workspace/$wsName/Product: $wsProdName" -ForegroundColor DarkCyan
723793

724794
# Product → API associations via apiLinks
725-
$result = Compare-ResourceType `
795+
$result = Compare-LinkResources `
726796
-TypeLabel " Workspace/$wsName/Product/$wsProdName/apiLinks" `
727797
-SourceUrl "$SourceBase/workspaces/$wsName/products/$wsProdName/apiLinks" `
728-
-TargetUrl "$TargetBase/workspaces/$wsName/products/$wsProdName/apiLinks"
798+
-TargetUrl "$TargetBase/workspaces/$wsName/products/$wsProdName/apiLinks" `
799+
-LinkProperty "apiId"
729800
$totalTypes++
730801
$totalDiffs += $result.Diffs
731802
$totalCompared += $result.Compared
@@ -744,20 +815,22 @@ try {
744815
Write-Host " Workspace/$wsName/Tag: $wsTagName" -ForegroundColor DarkCyan
745816

746817
# Tag → Product associations via productLinks
747-
$result = Compare-ResourceType `
818+
$result = Compare-LinkResources `
748819
-TypeLabel " Workspace/$wsName/Tag/$wsTagName/productLinks" `
749820
-SourceUrl "$SourceBase/workspaces/$wsName/tags/$wsTagName/productLinks" `
750-
-TargetUrl "$TargetBase/workspaces/$wsName/tags/$wsTagName/productLinks"
821+
-TargetUrl "$TargetBase/workspaces/$wsName/tags/$wsTagName/productLinks" `
822+
-LinkProperty "productId"
751823
$totalTypes++
752824
$totalDiffs += $result.Diffs
753825
$totalCompared += $result.Compared
754826
if ($result.Skipped) { $skippedTypes++ }
755827

756828
# Tag → API associations via apiLinks
757-
$result = Compare-ResourceType `
829+
$result = Compare-LinkResources `
758830
-TypeLabel " Workspace/$wsName/Tag/$wsTagName/apiLinks" `
759831
-SourceUrl "$SourceBase/workspaces/$wsName/tags/$wsTagName/apiLinks" `
760-
-TargetUrl "$TargetBase/workspaces/$wsName/tags/$wsTagName/apiLinks"
832+
-TargetUrl "$TargetBase/workspaces/$wsName/tags/$wsTagName/apiLinks" `
833+
-LinkProperty "apiId"
761834
$totalTypes++
762835
$totalDiffs += $result.Diffs
763836
$totalCompared += $result.Compared

tests/integration/all-resource-types/bicep/source-apim.bicep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,9 @@ resource wsApiTagLink 'Microsoft.ApiManagement/service/workspaces/tags/apiLinks@
14341434
}
14351435
}
14361436

1437+
// NOTE: Workspace-scoped tagDescriptions (Microsoft.ApiManagement/service/workspaces/apis/tagDescriptions)
1438+
// is NOT supported by APIM — the endpoint returns HTTP 500. Skipped until APIM adds support.
1439+
14371440
// --- Workspace Product ↔ Tag association (via tag productLinks endpoint) ---
14381441
resource wsProductTagLink 'Microsoft.ApiManagement/service/workspaces/tags/productLinks@2025-09-01-preview' = if (supportsWorkspaces) {
14391442
parent: wsTag

tests/integration/all-resource-types/phases/run-phase2-extract.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ if (-not $extractedFiles -or $extractedFiles.Count -eq 0) {
104104
exit 2
105105
}
106106

107-
$resolvedExtractOutputDir = [System.IO.Path]::GetFullPath($ExtractOutputDir)
107+
$resolvedExtractOutputDir = (Resolve-Path $ExtractOutputDir).Path
108108

109109
if ($env:GITHUB_OUTPUT) {
110110
"ExtractOutputDir=$resolvedExtractOutputDir" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

tests/integration/all-resource-types/phases/run-phase4-create-overrides.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ if (-not $targetEhConnStr) {
9898
Write-Host " ⚠️ Could not get Event Hub connection string — EH logger override will be empty"
9999
}
100100

101-
$overrideFile = [System.IO.Path]::GetFullPath((Join-Path $ExtractOutputDir '.overrides.yaml'))
101+
$overrideFile = Join-Path (Resolve-Path $ExtractOutputDir).Path '.overrides.yaml'
102102
$overrideYaml = @"
103103
namedValues:
104104
- name: src-nv-keyvault

tests/integration/all-resource-types/phases/run-phase5-publish.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ if (-not (Test-Path $overrideFileValue)) {
8787
exit 2
8888
}
8989

90-
$overrideFile = [System.IO.Path]::GetFullPath($overrideFileValue)
90+
$overrideFile = (Resolve-Path $overrideFileValue).Path
9191

9292
Write-Host "📤 Publish — Publish artifacts to target APIM"
9393
$publishArgs = @(

0 commit comments

Comments
 (0)