Skip to content

fix: use workspace-specific ARM paths for association resources#136

Open
petehauge wants to merge 8 commits into
mainfrom
fix/workspace-items-135
Open

fix: use workspace-specific ARM paths for association resources#136
petehauge wants to merge 8 commits into
mainfrom
fix/workspace-items-135

Conversation

@petehauge

Copy link
Copy Markdown
Collaborator

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. apiLinks instead of apis) 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 — Added workspaceArmPathSuffix and workspaceLinkIdProperty fields to ResourceTypeMetadata. Configured for ProductApi, ProductGroup, ApiTag, ProductTag. Added workspaceSupported: true to VersionSet. Removed workspaceSupported from Documentation (endpoint doesn't exist).
  • src/lib/resource-uri.tsbuildArmUri uses workspace ARM paths when in workspace scope; prevents double-workspace prefix bug. parseArmUri also tries workspaceArmPathSuffix when matching.
  • src/lib/resource-path.ts — Fixed parseTemplatePath to re-sort regex captures by placeholder index (not left-to-right appearance order), so inverted templates like tags/{1}/apiLinks/{0} return correct nameParts.
  • src/clients/apim-client.tslistResources detects 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). Added extractWorkspaceApiTags() that iterates tags and lists their apiLinks.
  • src/services/product-extractor.ts — Workspace-aware extractProductAssociations() parses link responses. Added extractWorkspaceProductTags() 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 calls extractWorkspaceApiTags() and extractWorkspaceProductTags() after all resources are available.

Publishing changes

  • src/services/product-publisher.ts — Workspace-aware publishProductAssociations() with link payloads. Workspace-aware publishProductTags() (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 product apis.json, product tags/, and API tags/.
  • 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 include patchResource, validatePreFlight, and explicit AsyncGenerator<Record<string, unknown>> return type.
  • resource-types.test.ts — Updated expected workspace type list for VersionSet addition / Documentation removal.

Bugs found & fixed during review

  1. ProductTag double-swappublishProductTags was swapping nameParts for workspace scope, but the template tags/{1}/productLinks/{0} already handles inversion via placeholder indices. Removed the swap.
  2. parseTemplatePath index ordering — Captures were returned in left-to-right order, not by placeholder index. Fixed to re-sort so result[i] corresponds to {i}.
  3. Fragile workspace detection — Replaced baseUrl.includes('/workspaces/') with ARM path regex anchored to Microsoft.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.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / productLinks for 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 thread src/services/product-publisher.ts Outdated
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 thread src/services/product-publisher.ts Outdated
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]
}
Peter Hauge and others added 7 commits June 9, 2026 20:58
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>
StandardV2 does not support workspaces (Premium tier only).
Reverts the StandardV2 portion of #128 that was missed in #129.

Closes #135

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace-scoped extraction and publishing uses wrong ARM paths for association resources

2 participants