Skip to content

Commit 8bf9200

Browse files
committed
saving work
1 parent 9ca14e6 commit 8bf9200

4 files changed

Lines changed: 87 additions & 5 deletions

File tree

src/lib/resource-path.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ export function parseArtifactPath(
369369
return undefined;
370370
}
371371

372+
const workspaceContainer = parseWorkspaceContainerDescriptor(fileName, workspace);
373+
if (workspaceContainer) {
374+
return workspaceContainer;
375+
}
376+
372377
// Try to match against each resource type's pattern
373378
for (const [typeKey, metadata] of Object.entries(RESOURCE_TYPE_METADATA)) {
374379
const type = typeKey as ResourceType;
@@ -396,6 +401,24 @@ export function parseArtifactPath(
396401
return undefined;
397402
}
398403

404+
function parseWorkspaceContainerDescriptor(
405+
fileName: string,
406+
workspace?: string
407+
): ResourceDescriptor | undefined {
408+
// Workspace container descriptors are stored at:
409+
// workspaces/{workspace}/workspaceInformation.json
410+
// and are not workspace-scoped children. Return a top-level Workspace
411+
// descriptor so publish can create the container before workspace children.
412+
if (fileName === 'workspaceInformation.json' && workspace) {
413+
return {
414+
type: ResourceType.Workspace,
415+
nameParts: [workspace],
416+
};
417+
}
418+
419+
return undefined;
420+
}
421+
399422
/**
400423
* Parse a changed artifact file path into a ResourceDescriptor.
401424
*

src/services/resource-publisher.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ResourceType } from '../models/resource-types.js';
1515
import { applyOverrides } from './override-merger.js';
1616
import { checkKeyVaultSecretAccess } from './keyvault-checker.js';
1717
import { getNamePart } from '../lib/resource-path.js';
18+
import { isAutoGeneratedId } from '../lib/auto-generated.js';
1819

1920
export interface ResourcePublishResult {
2021
descriptor: ResourceDescriptor;
@@ -148,6 +149,7 @@ export async function publishResource(
148149
if (descriptor.type === ResourceType.Subscription) {
149150
const props = json.properties as Record<string, unknown> | undefined;
150151
const scope = props?.scope as string | undefined;
152+
const subscriptionName = getNamePart(descriptor.nameParts, 0);
151153

152154
// Built-in master subscription has scope ending with the service path (no /apis or /products suffix)
153155
// Skip it since APIM doesn't allow updates to built-in subscriptions
@@ -159,6 +161,14 @@ export async function publishResource(
159161
};
160162
}
161163

164+
if (isAutoGeneratedProductSubscription(subscriptionName, scope)) {
165+
return {
166+
descriptor,
167+
status: 'skipped',
168+
action: 'noop',
169+
};
170+
}
171+
162172
json = normalizeSubscriptionScope(json, context);
163173
}
164174

@@ -491,3 +501,14 @@ function normalizeMcpToolOperationIds(
491501
},
492502
};
493503
}
504+
505+
function isAutoGeneratedProductSubscription(
506+
subscriptionName: string,
507+
scope?: string
508+
): boolean {
509+
if (!scope || !isAutoGeneratedId(subscriptionName)) {
510+
return false;
511+
}
512+
513+
return scope.includes('/products/');
514+
}

tests/unit/lib/resource-path.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,15 @@ describe('parseArtifactPath', () => {
293293
expect(result!.nameParts[0]).toBe('ws-api');
294294
});
295295

296+
it('should parse workspace container descriptor', () => {
297+
const filePath = path.join(baseDir, 'workspaces', 'ws1', 'workspaceInformation.json');
298+
const result = parseArtifactPath(baseDir, filePath);
299+
expect(result).toBeDefined();
300+
expect(result!.type).toBe(ResourceType.Workspace);
301+
expect(result!.nameParts).toEqual(['ws1']);
302+
expect(result!.workspace).toBeUndefined();
303+
});
304+
296305
it('should parse ApiDiagnostic info file (nameParts[0]=apiName, nameParts[1]=diagName)', () => {
297306
const filePath = path.join(baseDir, 'apis', 'my-api', 'diagnostics', 'applicationinsights', 'diagnosticInformation.json');
298307
const result = parseArtifactPath(baseDir, filePath);

tests/unit/services/resource-publisher.test.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ describe('resource-publisher', () => {
645645
expect(client.putResource).not.toHaveBeenCalled();
646646
});
647647

648-
it('should publish product subscription with empty displayName', async () => {
648+
it('should skip auto-generated product subscription with empty displayName', async () => {
649649
const client = createMockClient();
650650
const store = createMockStore();
651651
const autoGeneratedId = generatedSubscriptionId('a');
@@ -671,12 +671,12 @@ describe('resource-publisher', () => {
671671

672672
const result = await publishResource(client, store, testContext, descriptor, testConfig);
673673

674-
expect(result.status).toBe('success');
675-
expect(result.action).toBe('put');
676-
expect(client.putResource).toHaveBeenCalledTimes(1);
674+
expect(result.status).toBe('skipped');
675+
expect(result.action).toBe('noop');
676+
expect(client.putResource).not.toHaveBeenCalled();
677677
});
678678

679-
it('should publish product subscription when displayName is set', async () => {
679+
it('should skip auto-generated product subscription when displayName is set', async () => {
680680
const client = createMockClient();
681681
const store = createMockStore();
682682
const autoGeneratedId = generatedSubscriptionId('b');
@@ -701,6 +701,35 @@ describe('resource-publisher', () => {
701701

702702
const result = await publishResource(client, store, testContext, descriptor, testConfig);
703703

704+
expect(result.status).toBe('skipped');
705+
expect(result.action).toBe('noop');
706+
expect(client.putResource).not.toHaveBeenCalled();
707+
});
708+
709+
it('should publish user-defined product subscription', async () => {
710+
const client = createMockClient();
711+
const store = createMockStore();
712+
713+
const armScopePrefix =
714+
'/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.ApiManagement/service/apim-1';
715+
716+
const subscriptionJson = {
717+
name: 'team-a-product-sub',
718+
properties: {
719+
scope: `${armScopePrefix}/products/starter`,
720+
displayName: 'Team A starter product',
721+
state: 'active',
722+
},
723+
};
724+
store.readResource.mockResolvedValue(subscriptionJson);
725+
726+
const descriptor: ResourceDescriptor = {
727+
type: ResourceType.Subscription,
728+
nameParts: ['team-a-product-sub'],
729+
};
730+
731+
const result = await publishResource(client, store, testContext, descriptor, testConfig);
732+
704733
expect(result.status).toBe('success');
705734
expect(result.action).toBe('put');
706735
expect(client.putResource).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)