This PR fixes several bugs in extract and publish related to API revision handling, resource deletion, and authentication settings, plus a dependency security bump. - #263
Conversation
Co-authored-by: azaslonov <2320302+azaslonov@users.noreply.github.com>
Co-authored-by: azaslonov <2320302+azaslonov@users.noreply.github.com>
…tings PUT, and current-revision extract duplication Bug 1 — Cannot delete the current revision: - apim-client.ts: deleteResource appends &deleteRevisions=true for base API deletes (id without ;rev=N), removing the API and all revisions in one call instead of erroring on the current revision. - publish-service.ts: filterRevisionDeletesHandledByBaseApi strips individual ;rev=N deletes when the base API is also being deleted. Bug 2 — policy fragment referenced by service policy: - apim-client.ts: deleteResource treats 'is used by the following entities' as non-fatal — resource is logged WARN and marked 'skipped' instead of failing the run. Bug 3 (Closes Azure#258) — extract mishandles APIs whose current revision is not rev=1: - api-extractor.ts: extractApiRevisions skips the current revision via isCurrent === true instead of hard-coding revision number '1'. - Renamed/updated test to explicitly cover current=2, non-current=1, asserting the non-current revision (;rev=1) is what gets extracted. Bug 4 — root API create collides with source ;rev=N artifact: - api-publisher.ts: resolveRootApiPutDescriptor detects when the source's current revision number is > 1 and the API does not yet exist on the target; in that case the root is PUT at apis/{name};rev=N instead of apis/{name} (which APIM always creates as revision 1), preventing it from colliding with and silently absorbing the real ;rev=1 artifact. Existing APIs keep the plain root PUT since their current revision cannot be renumbered. - api-publisher.ts: publishApiRevisions now throws when a revision publish fails, instead of silently swallowing the error and returning exit code 0. Bug 5 — authenticationSettings PUT rejected when legacy and collection fields are both present: - resource-publisher.ts: normalizeApiAuthenticationSettings drops the legacy oAuth2/openid fields when the newer oAuth2AuthenticationSettings/openidAuthenticationSettings collections are non-empty (APIM GET returns both, but PUT rejects the combination), keeping the singular fields only when the collections are empty. Verified via unit tests (new cases added for all scenarios) and manual dry-run/live extract+publish against dev-apim-uk-01 with a multi-revision API (webapitest;rev=1, ;rev=2).
There was a problem hiding this comment.
🟡 Changes recommended
Delete filtering must also cover dry runs and preserve workspace scope.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes APIM extraction and publishing issues involving revisions, deletion, and authentication settings.
Changes:
- Corrects API revision extraction, creation, failure propagation, and deletion.
- Normalizes authentication settings and safely skips referenced-resource deletion.
- Expands regression tests and upgrades
fast-urito 3.1.7.
File summaries
| File | Description |
|---|---|
tests/unit/services/resource-publisher.test.ts |
Tests authentication normalization. |
tests/unit/services/publish-service.test.ts |
Tests publishing order and revision deletion. |
tests/unit/services/api-publisher.test.ts |
Tests revision-aware API creation. |
tests/unit/services/api-product-extractor.test.ts |
Tests current-revision extraction. |
tests/unit/clients/apim-client.test.ts |
Tests deletion and conflict behavior. |
src/services/resource-publisher.ts |
Normalizes API authentication settings. |
src/services/publish-service.ts |
Orders publishing and filters revision deletes. |
src/services/api-publisher.ts |
Handles revision-aware creation and failures. |
src/services/api-extractor.ts |
Correctly skips the current API revision. |
src/clients/apim-client.ts |
Adds revision deletion and referenced-resource handling. |
package-lock.json |
Upgrades fast-uri to 3.1.7. |
Review details
- Files reviewed: 10/11 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ce scope Addresses Copilot review: filterRevisionDeletesHandledByBaseApi is now shared via delete-unmatched-service, applied to both dry-run delete paths (incremental and delete-unmatched), and keys base APIs by (workspace, name).
There was a problem hiding this comment.
🟡 Changes recommended
Target name mapping and override semantics must be corrected, and revision-failure coverage added.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/services/resource-publisher.ts:154
- This unconditional collection precedence can silently discard an explicit environment override of the supported legacy
authenticationSettings.oAuth2oropenidfields: overrides are deep-merged first, then this branch removes those overridden values whenever the extracted artifact contains a non-empty collection. Preserve override semantics by reconciling the two representations with override awareness (or translating a legacy override into the corresponding collection) rather than always preferring extracted collections.
src/services/api-publisher.ts:350
- Please add a regression test where
publishResourcereturnsstatus: 'failed'for a revision and assert thatpublishApireturns a failed result containing that revision error. The existing tests cover root PUT exceptions but not this newly added failure-propagation path, so the exit-code regression described by the PR remains unprotected.
const result = await publishResource(client, store, context, revDescriptor, config);
if (result.status === 'failed') {
throw new Error(
`Failed to publish revision ${getNamePart(revDescriptor.nameParts, 0)}: ` +
`${result.error?.message ?? 'unknown error'}`
);
- Files reviewed: 15/16 changed files
- Comments generated: 1
- Review effort level: Balanced
Revision creation via sourceApiId copies apiRevisionDescription from the current revision; send an explicit empty string when the artifact omits it. properties.description is never sent for ;rev=N PUTs (APIM rejects Description changes on non-current revisions); an explicit override of it now logs a warning instead of being silently dropped.
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate issues remain in environment mapping and authentication override handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/services/api-publisher.ts:183
- API environment mappings are not applied here. When
Apinames are prefixed/suffixed, this GET probes the canonical name, so an existing deployed API is treated as absent; the returned;rev=Ndescriptor is also passed directly toputResource, creating the canonical API while revision payloads reference the mapped API. Resolve existence and the final PUT descriptor frommapDescriptor(descriptor, config.envMapping), while continuing to read artifacts with the canonical descriptor.
const existing = await client.getResource(context, descriptor);
- Files reviewed: 15/16 changed files
- Comments generated: 2
- Review effort level: Balanced
…root auth normalization - resolveRootApiPutDescriptor checks API existence via the deployed (env-mapped) name and appends ;rev=N after affixing so suffix mappings cannot corrupt the revision suffix - root API normalization passes preferLegacyFields (same explicit-override signal as publishResource) so root and revision APIs honor authenticationSettings overrides consistently - add regression tests: revision publish failure propagates to a failed publishApi result; env-mapped existence check and rev suffix placement
There was a problem hiding this comment.
🟡 Changes recommended
Critical environment-mapping and authentication override issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
src/services/api-publisher.ts:203
- This revision-aware mapping is applied only to the newly created root. The non-current revisions published immediately afterward still go through
mapDescriptor, which affixes the entire name (orders-api;rev=1→dev-orders-api;rev=1-eu) instead of producingdev-orders-api-eu;rev=1; theirsourceApiIdmeanwhile points atdev-orders-api-eu. With a non-empty environment suffix, a fresh multi-revision publish therefore targets revision IDs under the wrong base and can fail. Make API descriptor mapping preserve;rev=Nfor every revision path, not only this root PUT.
`${deployedBaseName};rev=${rev}`,
src/services/resource-publisher.ts:153
preferLegacyFieldsis enabled for any override ofauthenticationSettings, including overrides that explicitly setoAuth2AuthenticationSettingsoropenidAuthenticationSettings. Because extracted GET payloads retain non-null legacy mirrors,hasLegacyValuesthen selects this branch and silently discards the explicit collection override in favor of the stale singular value. Inspect the overridden authentication subkeys so an explicitly supplied collection wins, while legacy fields win only when those legacy fields were explicitly overridden.
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
- Files reviewed: 15/16 changed files
- Comments generated: 1
- Review effort level: Balanced
…n publishApi Derive the env-mapped base descriptor once and use it for the existence check, root PUT, ;rev=N fresh-create target, revision alignment PUT, and imported-operation reconciliation. Previously only the fresh rev>1 branch was mapped, so publishes with envMapping could create or align an un-affixed API alongside the mapped one.
There was a problem hiding this comment.
🟡 Changes recommended
Critical revision mapping and moderate authentication override defects remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
src/clients/apim-client.ts:215
- The PR description enumerates five fixes but does not mention this new 409 retry policy; it also omits the new tier-2 serialization and revision-description sanitization. These are substantive runtime changes, so the description should summarize them and their motivation/testing to represent the full branch scope.
// APIM reports operations blocked by an API's in-progress async operation
// as a transient 409. Other client errors are deterministic.
const isPessimisticConcurrencyConflict =
error instanceof HttpError &&
error.status === 409 &&
error.code === 'PessimisticConcurrencyConflict';
src/services/resource-publisher.ts:153
preferLegacyFieldsis enabled for anyauthenticationSettingsoverride, not specifically a legacy-field override. SinceapplyOverridesdeep-merges the object, an explicitoAuth2AuthenticationSettings/openidAuthenticationSettingscollection override still leaves the extracted legacy field present; this branch then selects the legacy representation and silently discards the user's collection override. A metadata-only authentication override can likewise downgrade a multi-entry collection. Track which representation the override explicitly supplies and prefer only that representation.
const hasLegacyValues = oAuth2 != null || openid != null;
const hasCollections =
(Array.isArray(oAuth2AuthenticationSettings) && oAuth2AuthenticationSettings.length > 0) ||
(Array.isArray(openidAuthenticationSettings) && openidAuthenticationSettings.length > 0);
const normalizedAuth: Record<string, unknown> = { ...rest };
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
- Files reviewed: 15/16 changed files
- Comments generated: 1
- Review effort level: Balanced
…e precedence - env-mapper affixes the base API name and re-appends ;rev=N in both directions (toDeployedName/toCanonicalName/isInEnvNamespace), fixing revision PUT targets and delete-unmatched canonicalization under suffix mappings - prefersLegacyAuthOverride inspects which authenticationSettings representation the override explicitly supplies; collection and metadata-only overrides keep collection precedence
There was a problem hiding this comment.
🟡 Changes recommended
One critical and three moderate publishing, mapping, and authentication issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/services/resource-publisher.ts:190
preferLegacyFieldsmakes one global choice for both authentication families. Since overrides are deep-merged, overriding only legacyopenidcan coexist with an extracted multi-entryoAuth2AuthenticationSettings; this branch then drops that entire OAuth collection and keeps only the singular extracted value, silently losing additional authorization servers. Preserve untouched collection families by converting only explicitly overridden legacy values to collection entries rather than switching both families to legacy representation.
const normalizedAuth: Record<string, unknown> = { ...rest };
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
if (Array.isArray(oAuth2AuthenticationSettings) && oAuth2AuthenticationSettings.length > 0) {
normalizedAuth.oAuth2AuthenticationSettings = oAuth2AuthenticationSettings;
}
if (Array.isArray(openidAuthenticationSettings) && openidAuthenticationSettings.length > 0) {
normalizedAuth.openidAuthenticationSettings = openidAuthenticationSettings;
}
} else {
- Files reviewed: 17/18 changed files
- Comments generated: 3
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Four moderate issues remain in revision-aware environment mapping and override handling for authentication and descriptions.
Review details
Suppressed comments (4)
src/services/env-mapper.ts:183
- Revision splitting is applied to every resource type in
appliesTo, although;rev=is API-only syntax. A non-API identifier containing that literal text now has the environment suffix inserted in the middle of its name, which can also break rewritten references. Gate the split onResourceType.Api.
const { base, revSuffix } = splitRevisionSuffix(name);
src/services/resource-publisher.ts:101
- Explicit clearing overrides are treated as absent here. Because overrides are deep-merged,
{ oAuth2AuthenticationSettings: [] }leaves the extracted legacyoAuth2value in the payload, and{ oAuth2: null }leaves an extracted non-empty collection; normalization then restores the old authentication instead of clearing it. Determine representation preference from own-property presence in the override and preserve the selected empty/null values.
const suppliesLegacy = auth.oAuth2 != null || auth.openid != null;
const suppliesCollections =
(Array.isArray(auth.oAuth2AuthenticationSettings) && auth.oAuth2AuthenticationSettings.length > 0) ||
(Array.isArray(auth.openidAuthenticationSettings) && auth.openidAuthenticationSettings.length > 0);
src/services/resource-publisher.ts:398
- For revision descriptors,
applyOverrideslooks up the full name such asmy-api;rev=2, but this checks the base namemy-api. A legacy authentication override keyed to the revision is therefore merged and then discarded whenever the extracted collection fields are non-empty. Use the same resource name used by override lookup.
apiName.split(';rev=')[0] ?? apiName,
src/services/resource-publisher.ts:428
- This checks the base API key even though
applyOverridesmatched the full revision key. Consequently, adescriptionoverride configured formy-api;rev=2is deleted below without the promised warning. CheckapiNameso valid revision-specific overrides are reported.
hasExplicitPropertyOverride(baseApiName ?? apiName, 'description', config.overrides?.apis)
- Files reviewed: 17/18 changed files
- Comments generated: 0 new
- Review effort level: Balanced
… key, revision-collision guard, tests
There was a problem hiding this comment.
🟡 Changes recommended
Moderate issues remain in revision counting, MCP environment mapping, revision override diagnostics, and authentication-setting preservation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
src/services/resource-publisher.ts:425
- This looks up the base API key, but
applyOverridesmatches API overrides by the descriptor’s exact name. As a result, amy-api;rev=2description override is silently discarded without this warning, while an unrelatedmy-apioverride can trigger the warning whenever the revision artifact already has a description. CheckapiNamehere so the diagnostic corresponds to the override actually applied to this revision.
hasExplicitPropertyOverride(baseApiName ?? apiName, 'description', config.overrides?.apis)
src/services/resource-publisher.ts:183
- Selecting legacy mode for any one legacy override discards both extracted collection fields. For example, overriding only
oAuth2also removes everyopenidAuthenticationSettingsentry (potentially collapsing multiple settings to the singular extractedopenidvalue), even though overrides are otherwise deep-merged. Translate the explicitly overridden legacy family into collection form, or otherwise preserve unaffected collection settings, rather than switching the entire authentication object to legacy mode.
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
- Files reviewed: 17/18 changed files
- Comments generated: 2
- Review effort level: Balanced
…hed revision count
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate authentication and revision-override handling issues remain unresolved.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/services/resource-publisher.ts:426
- This checks the base API key, but
applyOverridesmatched this revision using the fullapiName. Consequently, a revision-specificdescriptionoverride is silently removed without the promised warning, while an unrelated base-level override can trigger a misleading warning even though it was never applied here. CheckapiNameso the warning reflects the override that was actually merged.
src/services/resource-publisher.ts:183
- This selection is global across both auth families: if either collection is non-empty, both legacy fields are discarded. A valid merged artifact such as a non-empty
oAuth2AuthenticationSettingsarray plus a legacy-onlyopenidvalue therefore silently loses its OpenID configuration. Normalize each legacy value into its corresponding collection (or reject incomplete mixed representations) before dropping the singular fields so enabling one collection does not erase the other auth mode.
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
- Files reviewed: 17/18 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Two critical revision-publishing defects and one authentication override defect remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/services/resource-publisher.ts:183
- This non-empty check silently defeats explicit empty collection overrides. For example, after an override replaces
oAuth2AuthenticationSettingswith[], the extracted legacyoAuth2mirror remains;hasCollectionsbecomes false and theelsebranch sends that legacy value, so the OAuth setting is not cleared. Track whether the override explicitly selected the collection representation (including an empty array) and make normalization honor that desired state.
const hasCollections =
(Array.isArray(oAuth2AuthenticationSettings) && oAuth2AuthenticationSettings.length > 0) ||
(Array.isArray(openidAuthenticationSettings) && openidAuthenticationSettings.length > 0);
const normalizedAuth: Record<string, unknown> = { ...rest };
if (hasCollections && !(options?.preferLegacyFields && hasLegacyValues)) {
- Files reviewed: 17/18 changed files
- Comments generated: 2
- Review effort level: Balanced
Fixes
1.
publish --delete-unmatchedfails on APIs with revisions (Closes #260)apim-client.ts:deleteResourcenow appends&deleteRevisions=truewhen deleting a base API (id without;rev=N), so APIM removes the API and all its revisions in a single call instead of failing withCannot delete the current revision of an API.publish-service.ts: newfilterRevisionDeletesHandledByBaseApihelper removes individual;rev=Ndeletes from the delete set when the base API is also being deleted, avoiding redundant and racy per-revision delete calls.2. Policy fragment still referenced by the service policy cannot be deleted (Closes #261)
apim-client.ts:deleteResourcenow treats the APIM "is used by the following entities"ValidationErroras non-fatal. The resource is logged as a warning and markedskippedin the summary instead of failing the entire run.3.
extractmishandles APIs whose current revision is notrev=1(Closes #258)api-extractor.ts:extractApiRevisionsnow skips the current revision viaisCurrent === trueinstead of hard-coding revision number'1'. Previously, when the current revision was not1, it was re-extracted as a redundant;rev=Nfolder containing onlyapiInformation.json(missing spec/operations/schemas), which also causedpublishto attempt re-creating the current revision.4. Root API create collides with source
;rev=Nartifactapi-publisher.ts:resolveRootApiPutDescriptordetects when the source's current revision number is greater than 1 and the API does not yet exist on the target. In that case, the root is PUT atapis/{name};rev=Ninstead ofapis/{name}(which APIM always creates as revision 1), preventing it from colliding with and silently absorbing the real;rev=1artifact. Existing APIs keep the plain root PUT since their current revision cannot be renumbered.api-publisher.ts:publishApiRevisionsnow throws when a revision publish fails, instead of silently swallowing the error and returning exit code0.5.
authenticationSettingsPUT rejected when legacy and collection fields are both presentresource-publisher.ts:normalizeApiAuthenticationSettingsdrops the legacyoAuth2/openidfields when the neweroAuth2AuthenticationSettings/openidAuthenticationSettingscollections are non-empty. APIM's GET returns both forms together, but PUT rejects the combination withCannot use OAuth2AuthenticationSettings in combination with OAuth2 nor openid.Security
fast-urifrom3.1.2to3.1.7(npm audit fix, transitive dependency).Testing
apim-client.test.ts,api-extractor.test.ts,api-publisher.test.ts,publish-service.test.ts,resource-publisher.test.ts).extractandpublish --delete-unmatched --dry-run/ live publish againstdev-apim-uk-01using a multi-revision API (webapitest;rev=1,webapitest;rev=2) to confirm:extractproduces exactly one full folder per revision (no redundant thin duplicate for the current revision).publish --delete-unmatchedplans a single delete against the base API, with no separate deletes for individual revisions.Related issues
Closes #258
Closes #260
Closes #261