Skip to content

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

Merged
Alexander Zaslonov (azaslonov) merged 15 commits into
Azure:mainfrom
Alexey-Zheltov:main
Sep 4, 2026

Conversation

@Alexey-Zheltov

Copy link
Copy Markdown
Collaborator

Fixes

1. publish --delete-unmatched fails on APIs with revisions (Closes #260)

  • apim-client.ts: deleteResource now appends &deleteRevisions=true when deleting a base API (id without ;rev=N), so APIM removes the API and all its revisions in a single call instead of failing with Cannot delete the current revision of an API.
  • publish-service.ts: new filterRevisionDeletesHandledByBaseApi helper removes individual ;rev=N deletes 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: deleteResource now treats the APIM "is used by the following entities" ValidationError as non-fatal. The resource is logged as a warning and marked skipped in the summary instead of failing the entire run.

3. extract mishandles APIs whose current revision is not rev=1 (Closes #258)

  • api-extractor.ts: extractApiRevisions now skips the current revision via isCurrent === true instead of hard-coding revision number '1'. Previously, when the current revision was not 1, it was re-extracted as a redundant ;rev=N folder containing only apiInformation.json (missing spec/operations/schemas), which also caused publish to attempt re-creating the current revision.

4. Root API create collides with source ;rev=N artifact

  • api-publisher.ts: resolveRootApiPutDescriptor detects 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 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.

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's GET returns both forms together, but PUT rejects the combination with Cannot use OAuth2AuthenticationSettings in combination with OAuth2 nor openid.

Security

  • Bumped fast-uri from 3.1.2 to 3.1.7 (npm audit fix, transitive dependency).

Testing

  • Added/updated unit tests covering all five scenarios above (apim-client.test.ts, api-extractor.test.ts, api-publisher.test.ts, publish-service.test.ts, resource-publisher.test.ts).
  • Manually verified extract and publish --delete-unmatched --dry-run / live publish against dev-apim-uk-01 using a multi-revision API (webapitest;rev=1, webapitest;rev=2) to confirm:
    • extract produces exactly one full folder per revision (no redundant thin duplicate for the current revision).
    • publish --delete-unmatched plans a single delete against the base API, with no separate deletes for individual revisions.
    • Policy fragments still referenced by the service policy are skipped with a warning instead of failing the run.

Related issues

Closes #258
Closes #260
Closes #261

Copilot AI and others added 7 commits August 31, 2026 23:05
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).
Copilot AI balanced review requested due to automatic review settings September 3, 2026 17:15

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.

🟡 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-uri to 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.

Comment thread src/services/publish-service.ts Outdated
Comment thread src/services/publish-service.ts Outdated
Comment thread src/services/api-publisher.ts
…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).

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.

🟡 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.oAuth2 or openid fields: 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 publishResource returns status: 'failed' for a revision and assert that publishApi returns 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

Comment thread src/services/api-publisher.ts Outdated
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.

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.

🟡 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 Api names are prefixed/suffixed, this GET probes the canonical name, so an existing deployed API is treated as absent; the returned ;rev=N descriptor is also passed directly to putResource, creating the canonical API while revision payloads reference the mapped API. Resolve existence and the final PUT descriptor from mapDescriptor(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

Comment thread src/services/api-publisher.ts Outdated
Comment thread src/services/resource-publisher.ts
…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

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.

🟡 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=1dev-orders-api;rev=1-eu) instead of producing dev-orders-api-eu;rev=1; their sourceApiId meanwhile points at dev-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=N for every revision path, not only this root PUT.
      `${deployedBaseName};rev=${rev}`,

src/services/resource-publisher.ts:153

  • preferLegacyFields is enabled for any override of authenticationSettings, including overrides that explicitly set oAuth2AuthenticationSettings or openidAuthenticationSettings. Because extracted GET payloads retain non-null legacy mirrors, hasLegacyValues then 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

Comment thread src/services/api-publisher.ts Outdated
…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.

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.

🟡 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

  • preferLegacyFields is enabled for any authenticationSettings override, not specifically a legacy-field override. Since applyOverrides deep-merges the object, an explicit oAuth2AuthenticationSettings/openidAuthenticationSettings collection 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

Comment thread src/services/api-publisher.ts
…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

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.

🟡 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

  • preferLegacyFields makes one global choice for both authentication families. Since overrides are deep-merged, overriding only legacy openid can coexist with an extracted multi-entry oAuth2AuthenticationSettings; 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

Comment thread src/services/api-publisher.ts
Comment thread src/services/env-mapper.ts Outdated
Comment thread src/services/resource-publisher.ts Outdated

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.

🔵 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 on ResourceType.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 legacy oAuth2 value 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, applyOverrides looks up the full name such as my-api;rev=2, but this checks the base name my-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 applyOverrides matched the full revision key. Consequently, a description override configured for my-api;rev=2 is deleted below without the promised warning. Check apiName so 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

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.

🟡 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 applyOverrides matches API overrides by the descriptor’s exact name. As a result, a my-api;rev=2 description override is silently discarded without this warning, while an unrelated my-api override can trigger the warning whenever the revision artifact already has a description. Check apiName here 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 oAuth2 also removes every openidAuthenticationSettings entry (potentially collapsing multiple settings to the singular extracted openid value), 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

Comment thread src/services/api-publisher.ts
Comment thread src/services/api-publisher.ts

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.

🔵 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 applyOverrides matched this revision using the full apiName. Consequently, a revision-specific description override 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. Check apiName so 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 oAuth2AuthenticationSettings array plus a legacy-only openid value 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

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.

🟡 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 oAuth2AuthenticationSettings with [], the extracted legacy oAuth2 mirror remains; hasCollections becomes false and the else branch 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

Comment thread src/services/api-publisher.ts
Comment thread src/services/api-publisher.ts
@azaslonov
Alexander Zaslonov (azaslonov) merged commit 42f182d into Azure:main Sep 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants