@@ -14,12 +14,15 @@ import type { PublishConfig } from '../models/config.js';
1414import * as yaml from 'js-yaml' ;
1515import { ResourceType } from '../models/resource-types.js' ;
1616import {
17+ normalizeApiAuthenticationSettings ,
1718 normalizeMcpToolOperationIds ,
19+ prefersLegacyAuthOverride ,
1820 publishResource ,
1921 type ResourcePublishResult ,
2022} from './resource-publisher.js' ;
2123import { runParallel } from '../lib/parallel-runner.js' ;
2224import { applyOverrides } from './override-merger.js' ;
25+ import { mapDescriptor } from './env-mapper.js' ;
2326import { logger } from '../lib/logger.js' ;
2427import { getNamePart , getPublishTier } from '../lib/resource-path.js' ;
2528import { isAutoGeneratedId } from '../lib/auto-generated.js' ;
@@ -54,8 +57,26 @@ export async function publishApi(
5457 config : PublishConfig
5558) : Promise < ResourcePublishResult > {
5659 try {
57- // Step 1: Publish root API (with spec import if available)
58- const rootResult = await publishRootApi ( client , store , context , descriptor , config ) ;
60+ // Deployed (env-mapped) base descriptor — derived once and used for every
61+ // direct APIM call so canonical and affixed names can never diverge.
62+ const deployedDescriptor = config . envMapping
63+ ? mapDescriptor ( descriptor , config . envMapping )
64+ : descriptor ;
65+
66+ // Step 1: Publish root API (with spec import if available).
67+ // On a fresh target the root is created at its source revision number so
68+ // it cannot collide with ;rev=N revision artifacts.
69+ const putDescriptor = await resolveRootApiPutDescriptor (
70+ client ,
71+ store ,
72+ context ,
73+ descriptor ,
74+ deployedDescriptor ,
75+ config
76+ ) ;
77+ const rootResult = await publishRootApi ( client , store , context , descriptor , config , {
78+ putDescriptor,
79+ } ) ;
5980 if ( rootResult . status !== 'success' ) {
6081 return rootResult ;
6182 }
@@ -64,7 +85,7 @@ export async function publishApi(
6485 await alignImportedOperationDescriptions (
6586 client ,
6687 context ,
67- descriptor ,
88+ deployedDescriptor ,
6889 rootResult . operationIdsWithNullDescription
6990 ) ;
7091 }
@@ -75,13 +96,10 @@ export async function publishApi(
7596 // Step 2b: Align root API only when source marks it as current.
7697 // Source of truth is properties.isCurrent in root apiInformation.json.
7798 if ( publishedRevisionCount > 0 && rootResult . isCurrent === true ) {
78- const alignResult = await alignActiveRevisionWithSource (
79- client ,
80- store ,
81- context ,
82- descriptor ,
83- config
84- ) ;
99+ const alignResult = await publishRootApi ( client , store , context , descriptor , config , {
100+ includeSpecification : false ,
101+ putDescriptor : deployedDescriptor ,
102+ } ) ;
85103 if ( alignResult . status !== 'success' ) {
86104 return alignResult ;
87105 }
@@ -148,6 +166,49 @@ interface RootApiResult {
148166
149167interface PublishRootApiOptions {
150168 includeSpecification ?: boolean ;
169+ /** Descriptor to PUT to (defaults to the artifact descriptor). Lets the root
170+ * API be created at apis/{name};rev=N while still reading apis/{name} artifacts. */
171+ putDescriptor ?: ResourceDescriptor ;
172+ }
173+
174+ /**
175+ * A plain root PUT creates a brand-new API as revision 1, which collides with
176+ * a ;rev=1 revision artifact and silently absorbs it whenever the source's
177+ * current revision number is > 1. When the API does not yet exist on the
178+ * target, PUT the root at its true revision number (apis/{name};rev=N) instead.
179+ * Existing APIs keep the plain root PUT — their current revision cannot be
180+ * renumbered.
181+ *
182+ * Reads artifacts via the canonical descriptor; all APIM lookups and the
183+ * returned PUT target use the deployed (env-mapped) descriptor, with ;rev=N
184+ * appended after affixing so suffix mappings cannot corrupt it.
185+ */
186+ async function resolveRootApiPutDescriptor (
187+ client : IApimClient ,
188+ store : IArtifactStore ,
189+ context : ApimServiceContext ,
190+ descriptor : ResourceDescriptor ,
191+ deployedDescriptor : ResourceDescriptor ,
192+ config : PublishConfig
193+ ) : Promise < ResourceDescriptor > {
194+ const json = await store . readResource ( config . sourceDir , descriptor ) ;
195+ const rev = ( json ?. properties as Record < string , unknown > | undefined ) ?. apiRevision ;
196+ if ( typeof rev !== 'string' || rev === '' || rev === '1' ) {
197+ return deployedDescriptor ;
198+ }
199+
200+ const existing = await client . getResource ( context , deployedDescriptor ) ;
201+ if ( existing ) {
202+ return deployedDescriptor ;
203+ }
204+
205+ return {
206+ ...deployedDescriptor ,
207+ nameParts : [
208+ `${ getNamePart ( deployedDescriptor . nameParts , 0 ) } ;rev=${ rev } ` ,
209+ ...deployedDescriptor . nameParts . slice ( 1 ) ,
210+ ] ,
211+ } ;
151212}
152213
153214/**
@@ -176,11 +237,18 @@ async function publishRootApi(
176237
177238 // Root APIs publish through api-publisher rather than publishResource, so
178239 // they need the same pre-override MCP tool normalization here that revision
179- // APIs receive in resource-publisher.
180- json = normalizeMcpToolOperationIds ( json , context ) ;
240+ // APIs receive in resource-publisher — including env-mapped API names so the
241+ // tool operationIds match the affixed API this PUT targets.
242+ json = normalizeMcpToolOperationIds ( json , context , config . envMapping ) ;
181243
182244 // Apply overrides
183245 json = applyOverrides ( descriptor , json , config . overrides ) ;
246+ json = normalizeApiAuthenticationSettings ( json , {
247+ preferLegacyFields : prefersLegacyAuthOverride (
248+ getNamePart ( descriptor . nameParts , 0 ) ,
249+ config . overrides ?. apis
250+ ) ,
251+ } ) ;
184252 const isCurrent = getApiIsCurrent ( json ) ;
185253
186254 // Try to read the specification file for this API
@@ -239,7 +307,7 @@ async function publishRootApi(
239307 }
240308
241309 // PUT the API resource to APIM
242- await client . putResource ( context , descriptor , json ) ;
310+ await client . putResource ( context , options ?. putDescriptor ?? descriptor , json ) ;
243311
244312 return {
245313 descriptor,
@@ -251,22 +319,6 @@ async function publishRootApi(
251319 } ;
252320}
253321
254- async function alignActiveRevisionWithSource (
255- client : IApimClient ,
256- store : IArtifactStore ,
257- context : ApimServiceContext ,
258- descriptor : ResourceDescriptor ,
259- config : PublishConfig
260- ) : Promise < RootApiResult & ResourcePublishResult > {
261- logger . debug (
262- `Source marks "${ getNamePart ( descriptor . nameParts , 0 ) } " as current; re-applying root metadata to align active revision`
263- ) ;
264-
265- return publishRootApi ( client , store , context , descriptor , config , {
266- includeSpecification : false ,
267- } ) ;
268- }
269-
270322/**
271323 * Find and publish API revisions in numeric order
272324 */
@@ -294,12 +346,41 @@ async function publishApiRevisions(
294346 return revA - revB ;
295347 } ) ;
296348
297- // Publish each revision in order
349+ // The current revision is already published as the root API, so skip a
350+ // revision artifact that carries the same number — otherwise we re-create /
351+ // collide with it (e.g. the root PUT created at ;rev=N for a fresh
352+ // multi-revision API, or a stale ;rev=N folder from an older extract).
353+ const rootJson = await store . readResource ( config . sourceDir , apiDescriptor ) ;
354+ const rootRevision = ( rootJson ?. properties as Record < string , unknown > | undefined ) ?. apiRevision ;
355+ const rootRevisionNumber =
356+ typeof rootRevision === 'string' && rootRevision !== '' ? Number ( rootRevision ) : undefined ;
357+
358+ // Publish each revision in order; a failed revision must fail the API —
359+ // otherwise errors are silently swallowed and the exit code stays 0.
360+ let publishedCount = 0 ;
298361 for ( const revDescriptor of sortedRevisions ) {
299- await publishResource ( client , store , context , revDescriptor , config ) ;
362+ if (
363+ rootRevisionNumber !== undefined &&
364+ extractRevisionNumber ( getNamePart ( revDescriptor . nameParts , 0 ) ) === rootRevisionNumber
365+ ) {
366+ logger . debug (
367+ `Skipping revision ${ getNamePart ( revDescriptor . nameParts , 0 ) } — already published as the root API`
368+ ) ;
369+ continue ;
370+ }
371+ const result = await publishResource ( client , store , context , revDescriptor , config ) ;
372+ if ( result . status === 'failed' ) {
373+ throw new Error (
374+ `Failed to publish revision ${ getNamePart ( revDescriptor . nameParts , 0 ) } : ` +
375+ `${ result . error ?. message ?? 'unknown error' } `
376+ ) ;
377+ }
378+ if ( result . status === 'success' ) publishedCount ++ ;
300379 }
301380
302- return sortedRevisions . length ;
381+ // Return only revisions actually published so the caller does not run a
382+ // spurious active-revision alignment PUT when nothing changed.
383+ return publishedCount ;
303384}
304385
305386/**
@@ -467,8 +548,15 @@ async function reconcileOperationsAfterSpecImport(
467548
468549 const patchBody : Record < string , unknown > = { properties : patchProps } ;
469550
551+ // Artifact lookup above uses the canonical descriptor; the PATCH must target
552+ // the deployed (env-mapped) name so reconciliation hits the API that the
553+ // root create actually produced under environment mapping.
554+ const patchDescriptor = config . envMapping
555+ ? mapDescriptor ( descriptor , config . envMapping )
556+ : descriptor ;
557+
470558 try {
471- await client . patchResource ( context , descriptor , patchBody ) ;
559+ await client . patchResource ( context , patchDescriptor , patchBody ) ;
472560 logger . debug ( `Reconciled operation "${ getNamePart ( descriptor . nameParts , 1 ) } " after spec import` ) ;
473561 } catch ( error ) {
474562 logger . warn (
0 commit comments