fix: use workspace-specific ARM paths for association resources#136
Open
petehauge wants to merge 8 commits into
Open
fix: use workspace-specific ARM paths for association resources#136petehauge wants to merge 8 commits into
petehauge wants to merge 8 commits into
Conversation
Workspace-scoped extraction and publishing used service-scope ARM paths for association resources (ProductApi, ProductGroup, ApiTag, ProductTag), causing HTTP 500 errors. Workspace associations use different ARM endpoints (e.g. apiLinks instead of apis) and link response shapes. - Add workspaceArmPathSuffix and workspaceLinkIdProperty metadata - Handle inverted parent-child for ApiTag and ProductTag in workspace scope - Fix double-workspace prefix bug in buildArmUri - Parse link responses to extract real resource names - Add workspace association publishing with link payloads - Centralize workspace scope detection with ARM path regex - Fix parseTemplatePath to re-sort captures by placeholder index - Add VersionSet workspace support; remove Documentation (no endpoint) - Add workspace association resources to integration test Bicep - Add workspace product/API children comparison to roundtrip test Closes #135 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes workspace-scoped extraction/publishing for APIM association resources by introducing workspace-specific ARM path metadata, link-response handling (opaque link IDs + properties.*Id), and tag-centric extraction flows for inverted workspace relationships (e.g. tags/{tag}/apiLinks/{api}).
Changes:
- Added workspace-specific ARM path suffix + link-id-property metadata and updated URI building/parsing to support inverted templates.
- Implemented workspace-link helpers and updated extractors to enumerate tag-centric
apiLinks/productLinksfor workspace associations. - Extended integration fixtures/validation (Bicep + expected structure + compare script) and updated unit tests for workspace-supported type lists.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/models/resource-types.ts | Adds workspace-specific ARM path/link metadata; enables workspace VersionSet; disables workspace Documentation. |
| src/lib/resource-uri.ts | Uses workspace ARM templates when workspace-scoped; avoids double /workspaces/ prefix; parses workspace link paths too. |
| src/lib/resource-path.ts | Fixes parseTemplatePath to return captures ordered by placeholder index (supports inverted templates). |
| src/lib/workspace-link.ts | New helpers for workspace link payloads and link-response name extraction + workspace-scope detection. |
| src/clients/apim-client.ts | Derives list URLs from workspace ARM templates when context is workspace-scoped. |
| src/services/api-extractor.ts | Skips workspace ApiTag extraction in per-API flow; adds tag-centric extractWorkspaceApiTags(). |
| src/services/product-extractor.ts | Parses workspace association link responses; adds extractWorkspaceProductTags() (tag-centric). |
| src/services/workspace-extractor.ts | Tracks extracted APIs/tags/products and runs workspace tag-centric association extraction after the main loop. |
| src/services/product-publisher.ts | Adds workspace-aware link payload publishing for ProductApi/ProductGroup/ProductTag. |
| src/services/resource-publisher.ts | Adds workspace ApiTag link publishing path. |
| tests/unit/services/workspace-extractor.test.ts | Updates mock client + expected workspace type order (VersionSet in, Documentation out). |
| tests/unit/models/resource-types.test.ts | Updates expected workspace-supported types. |
| tests/integration/all-resource-types/bicep/source-apim.bicep | Adds workspace association resources to the deployment fixture. |
| tests/integration/all-resource-types/expected-structure.json | Adds workspace product/API tag association expectations. |
| tests/integration/all-resource-types/Compare-ApimInstance.ps1 | Adds workspace association comparisons. |
Comment on lines
+93
to
+98
| // Handle workspace ApiTag — uses link endpoint with link payload. | ||
| // In service scope, ApiTag is a regular PUT with the tag JSON; in workspace | ||
| // scope it becomes a link resource at `tags/{tag}/apiLinks/{api}`. | ||
| if (descriptor.type === ResourceType.ApiTag && isWorkspaceScope(context)) { | ||
| return await publishWorkspaceApiTagLink(client, context, descriptor); | ||
| } |
Comment on lines
+576
to
+579
| const apiName = getNamePart(descriptor.nameParts, 0); | ||
| const linkProperty = RESOURCE_TYPE_METADATA[ResourceType.ApiTag].workspaceLinkIdProperty!; | ||
| const payload = buildLinkPayload(context, linkProperty, 'apis', apiName); | ||
|
|
Comment on lines
+182
to
+202
| @@ -186,8 +194,12 @@ async function publishProductAssociations( | |||
| }; | |||
|
|
|||
| try { | |||
| // PUT empty body for association (APIM uses PUT to create association) | |||
| await client.putResource(context, assocDescriptor, {}); | |||
| // In workspace scope, PUT with link payload; otherwise empty body | |||
| let payload: Record<string, unknown> = {}; | |||
| if (workspaceScoped && linkProperty) { | |||
| payload = buildLinkPayload(context, linkProperty, resourceTypeSegment, name); | |||
| } | |||
| await client.putResource(context, assocDescriptor, payload); | |||
Comment on lines
+236
to
+252
| @@ -229,8 +245,11 @@ async function publishProductTags( | |||
| }; | |||
|
|
|||
| try { | |||
| // PUT empty body for tag association | |||
| await client.putResource(context, tagDescriptor, {}); | |||
| let payload: Record<string, unknown> = {}; | |||
| if (workspaceScoped && linkProperty) { | |||
| payload = buildLinkPayload(context, linkProperty, 'products', productName); | |||
| } | |||
| await client.putResource(context, tagDescriptor, payload); | |||
Comment on lines
145
to
148
| /** | ||
| * Extract product tags and write to artifact store as tags.json. | ||
| * In workspace scope, uses the tag-centric productLinks endpoint. | ||
| */ |
Comment on lines
691
to
712
| "expected": [ | ||
| { | ||
| "name": "src-ws-product", | ||
| "files": ["productInformation.json"], | ||
| "files": ["productInformation.json", "apis.json"], | ||
| "spotChecks": { | ||
| "productInformation.json": { | ||
| "properties.displayName": "Workspace Product", | ||
| "properties.subscriptionRequired": false, | ||
| "properties.state": "published" | ||
| } | ||
| }, | ||
| "directories": { | ||
| "tags": { | ||
| "minCount": 1, | ||
| "expected": [ | ||
| { | ||
| "name": "src-ws-tag", | ||
| "files": ["tagInformation.json"] | ||
| } | ||
| ] | ||
| } | ||
| } |
Comment on lines
+722
to
+727
| foreach ($wsProductChild in @('apis', 'tags')) { | ||
| $result = Compare-ResourceType ` | ||
| -TypeLabel " Workspace/$wsName/Product/$wsProdName/$wsProductChild" ` | ||
| -SourceUrl "$SourceBase/workspaces/$wsName/products/$wsProdName/$wsProductChild" ` | ||
| -TargetUrl "$TargetBase/workspaces/$wsName/products/$wsProdName/$wsProductChild" | ||
| $totalTypes++ |
Comment on lines
+745
to
+753
| $result = Compare-ResourceType ` | ||
| -TypeLabel " Workspace/$wsName/API/$wsApiName/tags" ` | ||
| -SourceUrl "$SourceBase/workspaces/$wsName/apis/$wsApiName/tags" ` | ||
| -TargetUrl "$TargetBase/workspaces/$wsName/apis/$wsApiName/tags" | ||
| $totalTypes++ | ||
| $totalDiffs += $result.Diffs | ||
| $totalCompared += $result.Compared | ||
| if ($result.Skipped) { $skippedTypes++ } | ||
| } |
Comment on lines
+1419
to
+1438
| // --- Workspace Product ↔ API association --- | ||
| resource wsProductApi 'Microsoft.ApiManagement/service/workspaces/products/apis@2025-09-01-preview' = if (supportsWorkspaces) { | ||
| parent: wsProduct | ||
| name: 'src-ws-api-rest' | ||
| dependsOn: [wsApi] | ||
| } | ||
|
|
||
| // --- Workspace API ↔ Tag association --- | ||
| resource wsApiTag 'Microsoft.ApiManagement/service/workspaces/apis/tags@2025-09-01-preview' = if (supportsWorkspaces) { | ||
| parent: wsApi | ||
| name: 'src-ws-tag' | ||
| dependsOn: [wsTag] | ||
| } | ||
|
|
||
| // --- Workspace Product ↔ Tag association --- | ||
| resource wsProductTag 'Microsoft.ApiManagement/service/workspaces/products/tags@2025-09-01-preview' = if (supportsWorkspaces) { | ||
| parent: wsProduct | ||
| name: 'src-ws-tag' | ||
| dependsOn: [wsTag] | ||
| } |
PowerShell's Set-Location updates $PWD but not .NET's [Environment]::CurrentDirectory. Invoke-MaskedProcess uses System.Diagnostics.Process which inherits the .NET CWD, causing relative paths (like --output ./extracted-artifacts2) to resolve from the repo root instead of the test directory. Set WorkingDirectory = $PWD.Path so child processes use the same directory as the calling PowerShell session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Detect workspace scope from descriptor.workspace in addition to isWorkspaceScope(context) in resource-publisher and product-publisher - Pass workspace name to buildLinkPayload so ARM IDs include the workspace segment when context is service-scoped - Add validation guard for workspaceLinkIdProperty before assertion - Fix misleading comment in product-extractor (only used in service scope) - Fix expected-structure.json: workspace product tags use tags.json association file, not tags/ subdirectory with tagInformation.json - Use link endpoints (apiLinks, productLinks) in Compare-ApimInstance.ps1 instead of classic endpoints that return HTTP 500 in workspace scope - Use *Links ARM types in source-apim.bicep for workspace associations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
StandardV2 supports workspaces when using the *Links ARM types (apiLinks, productLinks) instead of the classic association endpoints which return HTTP 500. Verified via deployment test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes workspace-scoped extraction and publishing for association resources (ProductApi, ProductGroup, ApiTag, ProductTag, VersionSet, Documentation) that used incorrect service-scope ARM paths, causing HTTP 500 errors.
Workspace associations use different ARM endpoints than service-scope (e.g.
apiLinksinstead ofapis) and return link response shapes with opaque IDs. Additionally, ApiTag and ProductTag have inverted parent-child relationships in workspace scope (e.g. service:apis/{api}/tags/{tag}→ workspace:tags/{tag}/apiLinks/{api}).Changes
Core fix — workspace ARM path metadata & URL construction
src/models/resource-types.ts— AddedworkspaceArmPathSuffixandworkspaceLinkIdPropertyfields toResourceTypeMetadata. Configured for ProductApi, ProductGroup, ApiTag, ProductTag. AddedworkspaceSupported: trueto VersionSet. RemovedworkspaceSupportedfrom Documentation (endpoint doesn't exist).src/lib/resource-uri.ts—buildArmUriuses workspace ARM paths when in workspace scope; prevents double-workspace prefix bug.parseArmUrialso triesworkspaceArmPathSuffixwhen matching.src/lib/resource-path.ts— FixedparseTemplatePathto re-sort regex captures by placeholder index (not left-to-right appearance order), so inverted templates liketags/{1}/apiLinks/{0}return correctnameParts.src/clients/apim-client.ts—listResourcesdetects workspace scope and uses workspace ARM paths for URL derivation.New helper module
src/lib/workspace-link.ts— Utility functions:extractNameFromLink(),buildWorkspaceResourceId(),isWorkspaceScope()(using ARM path regex, not fragile substring match),buildLinkPayload().Extraction changes
src/services/api-extractor.ts— Skips ApiTag extraction in workspace scope (inverted path would fail). AddedextractWorkspaceApiTags()that iterates tags and lists theirapiLinks.src/services/product-extractor.ts— Workspace-awareextractProductAssociations()parses link responses. AddedextractWorkspaceProductTags()for tag-centric extraction. Skips ProductTag in workspace scope during normal extraction.src/services/workspace-extractor.ts— Tracks extracted tags/APIs/products during main loop, then callsextractWorkspaceApiTags()andextractWorkspaceProductTags()after all resources are available.Publishing changes
src/services/product-publisher.ts— Workspace-awarepublishProductAssociations()with link payloads. Workspace-awarepublishProductTags()(lets template handle index inversion).src/services/resource-publisher.ts— Added workspace ApiTag link publishing handler.Integration test coverage
bicep/source-apim.bicep— Added 3 workspace association resources:wsProductApi,wsApiTag,wsProductTag.expected-structure.json— Added expected artifact paths for workspace productapis.json, producttags/, and APItags/.Compare-ApimInstance.ps1— Added workspace product children (apis, tags) and API children (tags) comparison loops.Unit test fixes
workspace-extractor.test.ts— Fixed mock client to includepatchResource,validatePreFlight, and explicitAsyncGenerator<Record<string, unknown>>return type.resource-types.test.ts— Updated expected workspace type list for VersionSet addition / Documentation removal.Bugs found & fixed during review
publishProductTagswas swappingnamePartsfor workspace scope, but the templatetags/{1}/productLinks/{0}already handles inversion via placeholder indices. Removed the swap.parseTemplatePathindex ordering — Captures were returned in left-to-right order, not by placeholder index. Fixed to re-sort soresult[i]corresponds to{i}.baseUrl.includes('/workspaces/')with ARM path regex anchored toMicrosoft.ApiManagement/service/{name}/workspaces/{ws}.Related Issue(s)
Closes #135
Not in scope
The issue mentions 4 GAP items (Workspace Policy, Workspace Certificate, Workspace Tag Operation Link, Workspace Api Version Set associations). These require new resource types or further investigation and are tracked separately.