From 3dca4139964a44ea60db3a4a7a1bcc5cbe47857f Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 15:15:53 +0530 Subject: [PATCH 01/13] feat(DX-9754): force api_version 3.2 on all entry/asset publish/unpublish call sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - operation-executor.ts: addHeader('api_version', '3.2') on entry and asset instances before SINGLE-mode publish/unpublish - base-class.ts: replace conditional stackHeaders mutation in publish-assets with unconditional addHeader; add addHeader to publish-entries chain - create-stack.ts: add api_version: '3.2' to scheduleEntryAction axios headers (raw-axios path, no SDK) - assets.ts: remove now-dead additionalInfo: { api_version: '3.2' } — base-class owns the header unconditionally - .talismanrc: whitelist create-stack.ts false-positive (api_key: apiKey is a variable ref, not a secret) Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 ++ .../src/core/operation-executor.ts | 4 ++-- .../src/lib/create-stack.ts | 2 +- .../src/import/modules/assets.ts | 2 -- .../src/import/modules/base-class.ts | 21 +++++-------------- 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.talismanrc b/.talismanrc index 126602cf1..ee3e65703 100644 --- a/.talismanrc +++ b/.talismanrc @@ -15,4 +15,6 @@ fileignoreconfig: checksum: 35451ca4c03359b06531a8461091f4a3a45c4dea1f8853081fb53e4ce28c1cc3 - filename: packages/contentstack-bulk-operations/test/unit/base-bulk-command.test.ts checksum: ca699a9d73757d44ded4d9d27beb4f460f911a7c88a14551a9402a3ac0c69873 +- filename: packages/contentstack-external-migrate/src/lib/create-stack.ts + checksum: 68a9510db6f2746ac5006c091d276c1ba619a9e15c76a3edae3967e4f9c2dd4e version: "" diff --git a/packages/contentstack-bulk-operations/src/core/operation-executor.ts b/packages/contentstack-bulk-operations/src/core/operation-executor.ts index 340b4b75e..3e7841a7f 100644 --- a/packages/contentstack-bulk-operations/src/core/operation-executor.ts +++ b/packages/contentstack-bulk-operations/src/core/operation-executor.ts @@ -164,7 +164,7 @@ export class OperationExecutor { private async executeEntryOperation(operation: OperationType, data: EntryPublishData): Promise { const { uid, content_type, locale, version, publish_details } = data; - const entry = this.stack.contentType(content_type).entry(uid); + const entry = this.stack.contentType(content_type).entry(uid).addHeader('api_version', '3.2'); switch (operation) { case OperationType.PUBLISH: @@ -197,7 +197,7 @@ export class OperationExecutor { private async executeAssetOperation(operation: OperationType, data: AssetPublishData): Promise { const { uid, locale, version, publish_details } = data; - const asset = this.stack.asset(uid); + const asset = this.stack.asset(uid).addHeader('api_version', '3.2'); switch (operation) { case OperationType.PUBLISH: diff --git a/packages/contentstack-external-migrate/src/lib/create-stack.ts b/packages/contentstack-external-migrate/src/lib/create-stack.ts index 2e7e02df0..7dfae8250 100644 --- a/packages/contentstack-external-migrate/src/lib/create-stack.ts +++ b/packages/contentstack-external-migrate/src/lib/create-stack.ts @@ -518,7 +518,7 @@ export async function scheduleEntryAction( ): Promise { await ensureFreshAuth(); const session = resolveSession(); - const headers = { ...session.authHeaders, api_key: apiKey, 'Content-Type': 'application/json', ...(opts.branch ? { branch: opts.branch } : {}) }; + const headers = { ...session.authHeaders, api_key: apiKey, 'Content-Type': 'application/json', api_version: '3.2', ...(opts.branch ? { branch: opts.branch } : {}) }; const isAsset = opts.contentTypeUid === 'sys_assets'; const url = isAsset ? `${session.cma}/v3/assets/${opts.entryUid}/${opts.action}` diff --git a/packages/contentstack-import/src/import/modules/assets.ts b/packages/contentstack-import/src/import/modules/assets.ts index 81588e993..fb875091b 100644 --- a/packages/contentstack-import/src/import/modules/assets.ts +++ b/packages/contentstack-import/src/import/modules/assets.ts @@ -420,8 +420,6 @@ export default class ImportAssets extends BaseClass { resolve: onSuccess, entity: 'publish-assets', includeParamOnCompletion: true, - // CS Assets publish requires api_version 3.2 (see base-class 'publish-assets'). - additionalInfo: { api_version: '3.2' }, }, concurrencyLimit: this.assetConfig.uploadAssetsConcurrency, }); diff --git a/packages/contentstack-import/src/import/modules/base-class.ts b/packages/contentstack-import/src/import/modules/base-class.ts index fe6710bf6..d83a6daa7 100644 --- a/packages/contentstack-import/src/import/modules/base-class.ts +++ b/packages/contentstack-import/src/import/modules/base-class.ts @@ -371,25 +371,13 @@ export default abstract class BaseClass { .replace(pick(apiData, [...this.modulesConfig.assets.validKeys, 'upload']) as AssetData) .then(onSuccess) .catch(onReject); - case 'publish-assets': { - const assetClient = this.stack.asset(uid); - // CS Assets (spaces) publish must go to api_version 3.2. The SDK's asset `publish()` takes no - // api_version arg and only forwards `stackHeaders` as request headers, so inject it into this - // per-call instance's headers (a fresh object — the shared stack headers are untouched). - // `additionalInfo.api_version` is set only by the AM publish path; legacy asset publish omits - // it and is unaffected. - const publishApiVersion = additionalInfo?.api_version; - if (publishApiVersion) { - (assetClient as any).stackHeaders = { - ...((assetClient as any).stackHeaders ?? {}), - api_version: publishApiVersion, - }; - } - return assetClient + case 'publish-assets': + return this.stack + .asset(uid) + .addHeader('api_version', '3.2') .publish(pick(apiData, ['publishDetails']) as PublishConfig) .then(onSuccess) .catch(onReject); - } case 'create-extensions': return this.stack .extension() @@ -507,6 +495,7 @@ export default abstract class BaseClass { return this.stack .contentType(additionalInfo.cTUid) .entry(apiData.entryUid) + .addHeader('api_version', '3.2') .publish({ publishDetails: { environments: apiData.environments, locales: apiData.locales }, locale: apiData.locales[0], From cd41eafce9b26404ca06e232a7739afe755dc054 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 15:24:35 +0530 Subject: [PATCH 02/13] feat(DX-9756): remove dead --api-version flag from bulk-entries api_version 3.2 is now hardcoded via addHeader() at the call site (DX-9754) so the flag is no longer needed on bulk-entries. - bulk-entries.ts: remove --api-version flag registration - messages/index.ts: remove API_VERSION constant (TAXONOMY_API_VERSION kept for bulk-taxonomies which still exposes the flag) - config-builder.ts: make apiVersion conditional on flag presence so the validateConfig() check only fires when bulk-taxonomies provides the flag - bulk-entries.test.ts: remove test asserting api-version flag presence - config-builder.test.ts: update default-values expectation to apiVersion: undefined Co-Authored-By: Claude Sonnet 4.6 --- .../src/commands/cm/stacks/bulk-entries.ts | 5 ----- packages/contentstack-bulk-operations/src/messages/index.ts | 2 -- .../src/utils/config-builder.ts | 2 +- .../test/unit/commands/bulk-entries.test.ts | 6 ------ .../test/unit/utils/config-builder.test.ts | 2 +- 5 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-entries.ts b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-entries.ts index e1954b49e..ab2a12c42 100644 --- a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-entries.ts +++ b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-entries.ts @@ -65,11 +65,6 @@ export default class BulkEntries extends BaseBulkCommand { description: messages.INCLUDE_VARIANTS, default: false, }), - - 'api-version': flags.string({ - description: messages.API_VERSION, - default: '3.2', - }), }; protected resourceType: ResourceType = ResourceType.ENTRY; diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index 9f82677a8..4f253df7f 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -411,8 +411,6 @@ const flagDescriptions = { '(optional) Revert publish operations from a log folder. Specify the folder path containing success logs. Works similar to retry-failed.', BULK_OPERATION_FOLDER: '(optional) Folder path to store operation logs. Creates separate files for success and failed operations. Default: bulk-operation', - API_VERSION: - 'Specifies the Content Management API version used for publishing. Use version `3.2` when publishing entries with nested references, otherwise, use the default version 3.2', TAXONOMY_API_VERSION: 'Content Management API version for taxonomy publish (default: `3.2`; required for the `items` + locales/environments body on POST /v3/taxonomies/publish).', TAXONOMY_ITEMS: diff --git a/packages/contentstack-bulk-operations/src/utils/config-builder.ts b/packages/contentstack-bulk-operations/src/utils/config-builder.ts index c53deb98c..ecc773857 100644 --- a/packages/contentstack-bulk-operations/src/utils/config-builder.ts +++ b/packages/contentstack-bulk-operations/src/utils/config-builder.ts @@ -245,7 +245,7 @@ export function buildConfig(flags: CommandFlags): BulkOperationConfig { folderUid: flags['folder-uid'], sourceEnv: flags['source-env'], publishMode: (flags['publish-mode'] as PublishMode) || PublishMode.BULK, - apiVersion: flags['api-version'] || '3', + ...(flags['api-version'] ? { apiVersion: flags['api-version'] } : {}), branch: flags.branch || 'main', filter: flags.filter, maxRetries: flags['max-retries'] || 3, diff --git a/packages/contentstack-bulk-operations/test/unit/commands/bulk-entries.test.ts b/packages/contentstack-bulk-operations/test/unit/commands/bulk-entries.test.ts index 85b1b6ce6..80c88f29c 100644 --- a/packages/contentstack-bulk-operations/test/unit/commands/bulk-entries.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/commands/bulk-entries.test.ts @@ -863,12 +863,6 @@ describe('BulkEntries Command', () => { expect(flags['include-variants'].default).to.be.false; }); - it('should have api-version flag', () => { - const flags = BulkEntries.flags; - - // api-version default may be '3' or '3.2' depending on configuration - expect(flags['api-version'].default).to.be.oneOf(['3', '3.2']); - }); }); describe('examples validation', () => { diff --git a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts index 384e7d00f..444f307ea 100644 --- a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts @@ -336,7 +336,7 @@ describe('Config Builder Utilities', () => { expect(config.environments).to.deep.equal([]); expect(config.locales).to.deep.equal([]); expect(config.publishMode).to.equal(PublishMode.BULK); - expect(config.apiVersion).to.equal('3'); // Default to 3 + expect(config.apiVersion).to.be.undefined; // No api-version flag → field not set expect(config.maxRetries).to.equal(3); }); From 445cb7d53d2a8114589201f2112a166d1fcc86e9 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 15:28:38 +0530 Subject: [PATCH 03/13] test(DX-9755): fix operation-executor mock and assert api_version header on publish/unpublish addHeader() was missing from the sinon mock causing 15 test failures after DX-9754 introduced .addHeader('api_version', '3.2') calls in the executor. - Add addHeader: sandbox.stub().returnsThis() to mockStack so the SDK chaining resolves correctly - Assert addHeader('api_version', '3.2') is called in all four cases: entry publish, entry unpublish, asset publish, asset unpublish Result: 839 passing, 0 failing (was 824 passing, 15 failing) Co-Authored-By: Claude Sonnet 4.6 --- .../test/unit/core/operation-executor.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/contentstack-bulk-operations/test/unit/core/operation-executor.test.ts b/packages/contentstack-bulk-operations/test/unit/core/operation-executor.test.ts index 5aea21f05..c24513871 100644 --- a/packages/contentstack-bulk-operations/test/unit/core/operation-executor.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/core/operation-executor.test.ts @@ -45,6 +45,7 @@ describe('OperationExecutor', () => { contentType: sandbox.stub().returnsThis(), entry: sandbox.stub().returnsThis(), asset: sandbox.stub().returnsThis(), + addHeader: sandbox.stub().returnsThis(), publish: sandbox.stub().resolves({ notice: 'Published successfully' }), unpublish: sandbox.stub().resolves({ notice: 'Unpublished successfully' }), }; @@ -143,6 +144,7 @@ describe('OperationExecutor', () => { expect(mockStack.contentType.calledWith('blog_post')).to.be.true; expect(mockStack.entry.calledWith('entry123')).to.be.true; + expect(mockStack.addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStack.publish.called).to.be.true; }); @@ -166,6 +168,7 @@ describe('OperationExecutor', () => { expect(mockStack.contentType.calledWith('blog_post')).to.be.true; expect(mockStack.entry.calledWith('entry123')).to.be.true; + expect(mockStack.addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStack.unpublish.called).to.be.true; }); @@ -211,6 +214,7 @@ describe('OperationExecutor', () => { await clock.tickAsync(100); expect(mockStack.asset.calledWith('asset123')).to.be.true; + expect(mockStack.addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStack.publish.called).to.be.true; }); @@ -232,6 +236,7 @@ describe('OperationExecutor', () => { await clock.tickAsync(100); expect(mockStack.asset.calledWith('asset123')).to.be.true; + expect(mockStack.addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStack.unpublish.called).to.be.true; }); }); From 0ea5e11b08adc568a418b7af52043af73d35eb47 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 15:35:06 +0530 Subject: [PATCH 04/13] chore: whitelist CHANGELOG.md in talismanrc (--api-version flag text triggers false positive) --- .talismanrc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.talismanrc b/.talismanrc index ee3e65703..36a1c9544 100644 --- a/.talismanrc +++ b/.talismanrc @@ -17,4 +17,6 @@ fileignoreconfig: checksum: ca699a9d73757d44ded4d9d27beb4f460f911a7c88a14551a9402a3ac0c69873 - filename: packages/contentstack-external-migrate/src/lib/create-stack.ts checksum: 68a9510db6f2746ac5006c091d276c1ba619a9e15c76a3edae3967e4f9c2dd4e +- filename: CHANGELOG.md + checksum: 88c7e1dee308fa4ae25e7815ead0842b1aac7ed669b02859cc8b25cba879595d version: "" From fb1c94e374330f08eb918092f0cd05ee38115c19 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 15:36:25 +0530 Subject: [PATCH 05/13] docs: document breaking change and NRP header fix in CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569097d77..bf2ef69a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Please refer to the Contentstack Command-line Interface release notes [here](htt +## @contentstack/cli-bulk-operations +### Version: 2.0.0-beta.5 +#### Date: Jul-21-2025 +##### Breaking Change: + - Removed the api version flag from `cm:stacks:bulk-entries`. The NRP header value is now hardcoded at the SDK call site, so the flag is no longer needed. Any scripts or CI pipelines that pass this flag must remove it — it will cause an unrecognized-flag error after this release. +##### Fix: + - Force NRP header to version 3.2 on all entry and asset publish/unpublish requests. The header is injected per-call and does not affect other CMA requests. + #### Date: Feb-09-2025 ## cli - Refactor Endpoints Integration using Utils SDK in cli-cm-config v1.9.0 From 517a21dc30d8fb762d63fd669e32124ef727e5ce Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 16:00:03 +0530 Subject: [PATCH 06/13] feat: harden NRP header on taxonomy publish and fix include-variants validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Taxonomy: - Drop --api-version flag from bulk-taxonomies; api_version 3.2 is now hardcoded via addHeader() on the taxonomy SDK instance, matching the same per-call pattern used for entries and assets - Remove DEFAULT_TAXONOMY_API_VERSION constant and apiVersion param chain from TaxonomyService — branch is now the direct second param - Remove TAXONOMY_API_VERSION message constant include-variants: - Remove broken validation that blocked --include-variants whenever --api-version was absent; the guard is redundant now that 3.2 is hardcoded on both BULK and SINGLE paths config-builder: - Remove dead api-version flag validation from validateCommandFlags - Remove dead config.apiVersion validation from validateConfig - Remove the conditional apiVersion spread from buildConfig Tests updated to assert addHeader behaviour and drop variant/api-version coupling tests. Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 + .../src/commands/cm/stacks/bulk-taxonomies.ts | 12 +--- .../src/messages/index.ts | 2 - .../src/services/taxonomy-service.ts | 49 ++++------------ .../src/utils/config-builder.ts | 16 ------ .../unit/services/taxonomy-service.test.ts | 56 +++++++++---------- .../test/unit/utils/config-builder.test.ts | 48 ---------------- 7 files changed, 43 insertions(+), 142 deletions(-) diff --git a/.talismanrc b/.talismanrc index 36a1c9544..b27ab5bad 100644 --- a/.talismanrc +++ b/.talismanrc @@ -19,4 +19,6 @@ fileignoreconfig: checksum: 68a9510db6f2746ac5006c091d276c1ba619a9e15c76a3edae3967e4f9c2dd4e - filename: CHANGELOG.md checksum: 88c7e1dee308fa4ae25e7815ead0842b1aac7ed669b02859cc8b25cba879595d +- filename: packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts + checksum: abc5ac707341760cf59d5b8b1c4e13cf2c79955e2735c33e2db3ec6bc48eddb6 version: "" diff --git a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts index cfee2fda7..64b5a3e24 100644 --- a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts +++ b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts @@ -30,9 +30,6 @@ export default class BulkTaxonomies extends BaseBulkCommand { // Multiple locales with a Management token alias '<%= config.bin %> <%= command.id %> --operation publish --environments staging --locales en-us,fr-fr --taxonomies taxonomy_a -a myAlias', - // Explicit CMA version for taxonomy publish (default is 3.2) - '<%= config.bin %> <%= command.id %> --operation publish --environments development --locales en-us --taxonomies products_tax --api-version 3.2 -k blt123', - // Publish taxonomies on a non-main branch '<%= config.bin %> <%= command.id %> --operation publish --branch feature --environments development --locales en-us --taxonomies brands_tax -k blt123', ]; @@ -42,10 +39,6 @@ export default class BulkTaxonomies extends BaseBulkCommand { taxonomies: flags.string({ description: messages.TAXONOMY_ITEMS, }), - 'api-version': flags.string({ - default: '3.2', - description: messages.TAXONOMY_API_VERSION, - }), } as any; protected resourceType: ResourceType = ResourceType.TAXONOMY; @@ -173,7 +166,6 @@ export default class BulkTaxonomies extends BaseBulkCommand { throw new Error($t(messages.UNSUPPORTED_OPERATION, { operation: operation ?? 'unknown' })); } - const apiVersion = this.parsedFlags['api-version'] || '3.2'; const locales = this.bulkOperationConfig.locales || []; const environments = this.bulkOperationConfig.environments || []; @@ -185,8 +177,8 @@ export default class BulkTaxonomies extends BaseBulkCommand { }; const response = operation === OperationType.UNPUBLISH - ? await taxonomyService.unpublish(payload, apiVersion, this.bulkOperationConfig.branch) - : await taxonomyService.publish(payload, apiVersion, this.bulkOperationConfig.branch); + ? await taxonomyService.unpublish(payload, this.bulkOperationConfig.branch) + : await taxonomyService.publish(payload, this.bulkOperationConfig.branch); const duration = Date.now() - startTime; const rawJobId = response.job_id; diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index 4f253df7f..e0a6ba970 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -411,8 +411,6 @@ const flagDescriptions = { '(optional) Revert publish operations from a log folder. Specify the folder path containing success logs. Works similar to retry-failed.', BULK_OPERATION_FOLDER: '(optional) Folder path to store operation logs. Creates separate files for success and failed operations. Default: bulk-operation', - TAXONOMY_API_VERSION: - 'Content Management API version for taxonomy publish (default: `3.2`; required for the `items` + locales/environments body on POST /v3/taxonomies/publish).', TAXONOMY_ITEMS: 'Comma-separated taxonomy UIDs to include in the job. If omitted, all taxonomies in the stack (current branch) are included. Example: products_tax,brands_tax', }; diff --git a/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts index a5b0eb991..21fdf9b04 100644 --- a/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts +++ b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts @@ -1,8 +1,6 @@ import type { ManagementStack, TaxonomyPublishJobResponse, TaxonomyPublishPayload } from '../interfaces'; import { OperationType } from '../interfaces'; -const DEFAULT_TAXONOMY_API_VERSION = '3.2'; - type TaxonomyPublishWithBranch = ( data: TaxonomyPublishPayload, apiVersion?: string, @@ -17,51 +15,28 @@ type TaxonomyOperationApi = { export class TaxonomyService { constructor(private stack: ManagementStack) {} - /** - * Publish one or more taxonomies (initiates a publish job). - */ - async publish( - data: TaxonomyPublishPayload, - apiVersion: string = DEFAULT_TAXONOMY_API_VERSION, - branch?: string - ): Promise { - return this.submit(OperationType.PUBLISH, data, apiVersion, branch); + async publish(data: TaxonomyPublishPayload, branch?: string): Promise { + return this.submit(OperationType.PUBLISH, data, branch); } - /** - * Unpublish one or more taxonomies (initiates an unpublish job). - */ - async unpublish( - data: TaxonomyPublishPayload, - apiVersion: string = DEFAULT_TAXONOMY_API_VERSION, - branch?: string - ): Promise { - return this.submit(OperationType.UNPUBLISH, data, apiVersion, branch); + async unpublish(data: TaxonomyPublishPayload, branch?: string): Promise { + return this.submit(OperationType.UNPUBLISH, data, branch); } private async submit( operation: OperationType, data: TaxonomyPublishPayload, - apiVersion: string, branch?: string ): Promise { - const taxonomies = this.stack.taxonomy() as unknown as TaxonomyOperationApi; - const params = - branch && branch !== 'main' - ? { - branch, - } - : undefined; - if (operation === OperationType.UNPUBLISH) { - if (params) { - return taxonomies.unpublish(data, apiVersion, params); - } - return taxonomies.unpublish(data, apiVersion); - } + const taxonomyInstance = this.stack.taxonomy() as any; + taxonomyInstance.addHeader('api_version', '3.2'); + const taxonomies = taxonomyInstance as unknown as TaxonomyOperationApi; - if (params) { - return taxonomies.publish(data, apiVersion, params); + const params = branch && branch !== 'main' ? { branch } : undefined; + + if (operation === OperationType.UNPUBLISH) { + return params ? taxonomies.unpublish(data, undefined, params) : taxonomies.unpublish(data); } - return taxonomies.publish(data, apiVersion); + return params ? taxonomies.publish(data, undefined, params) : taxonomies.publish(data); } } diff --git a/packages/contentstack-bulk-operations/src/utils/config-builder.ts b/packages/contentstack-bulk-operations/src/utils/config-builder.ts index ecc773857..0f877cb69 100644 --- a/packages/contentstack-bulk-operations/src/utils/config-builder.ts +++ b/packages/contentstack-bulk-operations/src/utils/config-builder.ts @@ -99,11 +99,6 @@ function validateConfig(config: BulkOperationConfig): string[] { } } - // API version validation - if (config.apiVersion && !['3', '3.2'].includes(config.apiVersion)) { - errors.push(`Invalid API version: ${config.apiVersion}. Supported versions: 3, 3.2`); - } - // Publish mode validation if (config.publishMode && config.publishMode !== PublishMode.BULK && config.publishMode !== PublishMode.SINGLE) { errors.push(`Invalid publish mode: ${String(config.publishMode)}. Must be 'bulk' or 'single'`); @@ -179,11 +174,6 @@ function validateCommandFlags(flags: CommandFlags): string[] { } } - // API version validation - if (flags['api-version'] && !['3', '3.2'].includes(flags['api-version'])) { - errors.push(`Invalid API version: ${flags['api-version']}. Supported versions: 3, 3.2`); - } - // Publish mode validation if (flags['publish-mode'] && !['bulk', 'single'].includes(flags['publish-mode'])) { errors.push(`Invalid publish mode: ${flags['publish-mode']}. Must be 'bulk' or 'single'`); @@ -199,11 +189,6 @@ function validateCommandFlags(flags: CommandFlags): string[] { errors.push('--source-alias can only be used with --source-env for cross-publish operations'); } - // Variants require api-version 3.2 - if (flags['include-variants'] && flags['api-version'] !== '3.2') { - errors.push('--include-variants requires --api-version 3.2'); - } - return errors; } @@ -245,7 +230,6 @@ export function buildConfig(flags: CommandFlags): BulkOperationConfig { folderUid: flags['folder-uid'], sourceEnv: flags['source-env'], publishMode: (flags['publish-mode'] as PublishMode) || PublishMode.BULK, - ...(flags['api-version'] ? { apiVersion: flags['api-version'] } : {}), branch: flags.branch || 'main', filter: flags.filter, maxRetries: flags['max-retries'] || 3, diff --git a/packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts b/packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts index 9fbbd87f7..278a5dd13 100644 --- a/packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts @@ -8,22 +8,27 @@ describe('TaxonomyService', () => { let sandbox: sinon.SinonSandbox; let publishStub: sinon.SinonStub; let unpublishStub: sinon.SinonStub; + let addHeaderStub: sinon.SinonStub; + let taxonomyInstance: any; beforeEach(() => { sandbox = sinon.createSandbox(); publishStub = sandbox.stub().resolves({ job_id: 'job_123', notice: 'notice' }); unpublishStub = sandbox.stub().resolves({ job_id: 'job_456', notice: 'notice' }); + taxonomyInstance = { + addHeader: sandbox.stub().returnsThis(), + publish: publishStub, + unpublish: unpublishStub, + }; + addHeaderStub = taxonomyInstance.addHeader; }); afterEach(() => { sandbox.restore(); }); - it('should call taxonomy().publish with data and api version', async () => { - const stack = { - taxonomy: () => ({ publish: publishStub }), - } as unknown as ManagementStack; - + it('should inject api_version 3.2 header via addHeader before publish', async () => { + const stack = { taxonomy: () => taxonomyInstance } as unknown as ManagementStack; const data = { locales: ['en-us'], environments: ['development'], @@ -31,19 +36,16 @@ describe('TaxonomyService', () => { }; const service = new TaxonomyService(stack); - const result = await service.publish(data, '3.2'); + const result = await service.publish(data); - expect(publishStub.calledOnce).to.equal(true); + expect(addHeaderStub.calledWith('api_version', '3.2')).to.be.true; + expect(publishStub.calledOnce).to.be.true; expect(publishStub.firstCall.args[0]).to.deep.equal(data); - expect(publishStub.firstCall.args[1]).to.equal('3.2'); expect(result.job_id).to.equal('job_123'); }); - it('should pass branch as third argument when branch is not main', async () => { - const stack = { - taxonomy: () => ({ publish: publishStub }), - } as unknown as ManagementStack; - + it('should pass branch as params when branch is not main', async () => { + const stack = { taxonomy: () => taxonomyInstance } as unknown as ManagementStack; const data = { locales: ['en-us'], environments: ['development'], @@ -51,16 +53,14 @@ describe('TaxonomyService', () => { }; const service = new TaxonomyService(stack); - await service.publish(data, '3.2', 'feature-branch'); + await service.publish(data, 'feature-branch'); - expect(publishStub.args[0][2]).to.deep.equal({ branch: 'feature-branch' }); + expect(addHeaderStub.calledWith('api_version', '3.2')).to.be.true; + expect(publishStub.firstCall.args[2]).to.deep.equal({ branch: 'feature-branch' }); }); it('should omit branch param for main', async () => { - const stack = { - taxonomy: () => ({ publish: publishStub }), - } as unknown as ManagementStack; - + const stack = { taxonomy: () => taxonomyInstance } as unknown as ManagementStack; const data = { locales: ['en-us'], environments: ['development'], @@ -68,16 +68,14 @@ describe('TaxonomyService', () => { }; const service = new TaxonomyService(stack); - await service.publish(data, '3.2', 'main'); + await service.publish(data, 'main'); - expect(publishStub.args[0].length).to.equal(2); + expect(addHeaderStub.calledWith('api_version', '3.2')).to.be.true; + expect(publishStub.firstCall.args.length).to.equal(1); }); - it('should call taxonomy().unpublish when operation is unpublish', async () => { - const stack = { - taxonomy: () => ({ publish: publishStub, unpublish: unpublishStub }), - } as unknown as ManagementStack; - + it('should inject api_version 3.2 header via addHeader before unpublish', async () => { + const stack = { taxonomy: () => taxonomyInstance } as unknown as ManagementStack; const data = { locales: ['en-us'], environments: ['development'], @@ -85,11 +83,11 @@ describe('TaxonomyService', () => { }; const service = new TaxonomyService(stack); - const result = await service.unpublish(data, '3.2', 'feature-branch'); + const result = await service.unpublish(data, 'feature-branch'); - expect(unpublishStub.calledOnce).to.equal(true); + expect(addHeaderStub.calledWith('api_version', '3.2')).to.be.true; + expect(unpublishStub.calledOnce).to.be.true; expect(unpublishStub.firstCall.args[0]).to.deep.equal(data); - expect(unpublishStub.firstCall.args[1]).to.equal('3.2'); expect(unpublishStub.firstCall.args[2]).to.deep.equal({ branch: 'feature-branch' }); expect(result.job_id).to.equal('job_456'); }); diff --git a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts index 444f307ea..0053a1b46 100644 --- a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts @@ -298,7 +298,6 @@ describe('Config Builder Utilities', () => { locales: ['en-us', 'fr-fr'], operation: 'publish', 'publish-mode': 'bulk', - 'api-version': '3', 'include-variants': true, 'source-env': 'production', 'max-retries': 5, @@ -316,7 +315,6 @@ describe('Config Builder Utilities', () => { expect(config.locales).to.deep.equal(['en-us', 'fr-fr']); expect(config.operation).to.equal('publish'); expect(config.publishMode).to.equal('bulk'); - expect(config.apiVersion).to.equal('3'); expect(config.includeVariants).to.be.true; expect(config.sourceEnv).to.equal('production'); expect(config.maxRetries).to.equal(5); @@ -482,52 +480,6 @@ describe('Config Builder Utilities', () => { expect(config.branch).to.equal('feature/new-branch'); }); - it('should build config with api-version 3.2', () => { - const flags: CommandFlags = { - alias: 'test-alias', - operation: 'publish', - environments: ['dev'], - locales: ['en-us'], - 'api-version': '3.2', - }; - - const config = buildConfig(flags); - - expect(config.apiVersion).to.equal('3.2'); - }); - }); - - describe('variant api-version dependency', () => { - it('should fail validation when include-variants is used without api-version 3.2', () => { - const flags: CommandFlags = { - alias: 'test-alias', - operation: 'publish', - environments: ['dev'], - locales: ['en-us'], - 'include-variants': true, - 'api-version': '3', - }; - - const result = validateFlags(flags); - - expect(result.valid).to.be.false; - expect(result.errors).to.include('--include-variants requires --api-version 3.2'); - }); - - it('should pass validation with include-variants and api-version 3.2', () => { - const flags: CommandFlags = { - alias: 'test-alias', - operation: 'publish', - environments: ['dev'], - locales: ['en-us'], - 'include-variants': true, - 'api-version': '3.2', - }; - - const result = validateFlags(flags); - - expect(result.valid).to.be.true; - }); }); describe('setupStackConfig', () => { From 52e855f5e898d0c4273d2b1afa736a3c622793fd Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 16:12:41 +0530 Subject: [PATCH 07/13] test: fix base-class mock and assert api_version header on publish-assets and publish-entries addHeader was missing from the asset and entry stubs in the import base-class test, causing both publish cases to throw at runtime after DX-9754 added .addHeader('api_version', '3.2') to the SDK call chain. - Add addHeader: sinon.stub().returnsThis() to asset() and entry() mocks - Assert addHeader('api_version', '3.2') is called before publish in both the publish-assets and publish-entries test cases Result: 1719 passing, 0 failing (was 1717 passing, 2 failing) Co-Authored-By: Claude Sonnet 4.6 --- .../test/unit/import/modules/base-class.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contentstack-import/test/unit/import/modules/base-class.test.ts b/packages/contentstack-import/test/unit/import/modules/base-class.test.ts index b95ef0997..2aaa01d74 100644 --- a/packages/contentstack-import/test/unit/import/modules/base-class.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/base-class.test.ts @@ -21,6 +21,7 @@ describe('BaseClass', () => { asset: sinon.stub().returns({ create: sinon.stub().resolves({ uid: 'asset-123' }), replace: sinon.stub().resolves({ uid: 'asset-123' }), + addHeader: sinon.stub().returnsThis(), publish: sinon.stub().resolves({ uid: 'asset-123' }), folder: sinon.stub().returns({ create: sinon.stub().resolves({ uid: 'folder-123' }), @@ -30,6 +31,7 @@ describe('BaseClass', () => { create: sinon.stub().resolves({ uid: 'ct-123' }), entry: sinon.stub().returns({ create: sinon.stub().resolves({ uid: 'entry-123' }), + addHeader: sinon.stub().returnsThis(), publish: sinon.stub().resolves({ uid: 'entry-123' }), delete: sinon.stub().resolves({ uid: 'entry-123' }), }), @@ -534,6 +536,7 @@ describe('BaseClass', () => { await testClass.makeAPICall(mockApiOptions); + expect(mockStackClient.asset().addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStackClient.asset().publish.calledOnce).to.be.true; expect(mockApiOptions.resolve.calledOnce).to.be.true; }); @@ -798,6 +801,7 @@ describe('BaseClass', () => { await testClass.makeAPICall(mockApiOptions); + expect(mockStackClient.contentType().entry().addHeader.calledWith('api_version', '3.2')).to.be.true; expect(mockStackClient.contentType().entry().publish.calledOnce).to.be.true; expect(mockApiOptions.resolve.calledOnce).to.be.true; }); From 2131f00f30dee3b4366b5e89f34da56b95306ab5 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 17:10:16 +0530 Subject: [PATCH 08/13] test: assert api_version 3.2 in bulk publish and unpublish payload body Co-Authored-By: Claude Sonnet 4.6 --- .../test/unit/services/bulk-operation-service.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts b/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts index 60e1d6881..a0d1620fe 100644 --- a/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts @@ -123,6 +123,7 @@ describe('BulkOperationService', () => { expect(jobId).to.equal('job123'); expect(mockPublish.called).to.be.true; + expect(mockPublish.firstCall.args[0].api_version).to.equal('3.2'); }); it('should submit bulk unpublish job', async () => { @@ -147,6 +148,7 @@ describe('BulkOperationService', () => { expect(jobId).to.equal('job456'); expect(mockUnpublish.called).to.be.true; + expect(mockUnpublish.firstCall.args[0].api_version).to.equal('3.2'); }); it('should handle unsupported operation', async () => { From b5774691e42856fb7e0b79b62c1822965e29815b Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 17:23:08 +0530 Subject: [PATCH 09/13] chore: remove dead api-version fields from CommandFlags and BulkOperationConfig interfaces Co-Authored-By: Claude Sonnet 4.6 --- packages/contentstack-bulk-operations/src/interfaces/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/interfaces/index.ts b/packages/contentstack-bulk-operations/src/interfaces/index.ts index f7a9c7596..3ec581910 100644 --- a/packages/contentstack-bulk-operations/src/interfaces/index.ts +++ b/packages/contentstack-bulk-operations/src/interfaces/index.ts @@ -65,7 +65,6 @@ export interface BulkOperationConfig { // API configuration publishMode?: PublishMode; - apiVersion?: string; // Filtering and selection branch?: string; @@ -215,7 +214,6 @@ export interface CommandFlags { 'source-alias'?: string; // API configuration - 'api-version'?: string; 'publish-mode'?: string; // Retry, reliability, and operations log From 292867aec031781140f0fc48f050cc4da423d294 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 17:24:47 +0530 Subject: [PATCH 10/13] test: remove dead apiVersion undefined assertion from config-builder test Co-Authored-By: Claude Sonnet 4.6 --- .../test/unit/utils/config-builder.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts index 0053a1b46..4638d0c1d 100644 --- a/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts @@ -334,7 +334,6 @@ describe('Config Builder Utilities', () => { expect(config.environments).to.deep.equal([]); expect(config.locales).to.deep.equal([]); expect(config.publishMode).to.equal(PublishMode.BULK); - expect(config.apiVersion).to.be.undefined; // No api-version flag → field not set expect(config.maxRetries).to.equal(3); }); From d12e46bd7629a39cb98c5b2f98f3d59b26618eb2 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 21 Jul 2026 18:39:02 +0530 Subject: [PATCH 11/13] chore: drop dead TaxonomyPublishWithBranch and TaxonomyOperationApi types Both types only existed to give a typed surface for publish/unpublish after casting through `as any`. Since the instance is already any, calling directly removes the unnecessary intermediate cast and the apiVersion?: string param that modelled the SDK signature. Co-Authored-By: Claude Sonnet 4.6 --- .../src/services/taxonomy-service.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts index 21fdf9b04..05c1e8a9b 100644 --- a/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts +++ b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts @@ -1,17 +1,6 @@ import type { ManagementStack, TaxonomyPublishJobResponse, TaxonomyPublishPayload } from '../interfaces'; import { OperationType } from '../interfaces'; -type TaxonomyPublishWithBranch = ( - data: TaxonomyPublishPayload, - apiVersion?: string, - params?: { branch?: string } -) => Promise; - -type TaxonomyOperationApi = { - publish: TaxonomyPublishWithBranch; - unpublish: TaxonomyPublishWithBranch; -}; - export class TaxonomyService { constructor(private stack: ManagementStack) {} @@ -30,13 +19,12 @@ export class TaxonomyService { ): Promise { const taxonomyInstance = this.stack.taxonomy() as any; taxonomyInstance.addHeader('api_version', '3.2'); - const taxonomies = taxonomyInstance as unknown as TaxonomyOperationApi; const params = branch && branch !== 'main' ? { branch } : undefined; if (operation === OperationType.UNPUBLISH) { - return params ? taxonomies.unpublish(data, undefined, params) : taxonomies.unpublish(data); + return params ? taxonomyInstance.unpublish(data, undefined, params) : taxonomyInstance.unpublish(data); } - return params ? taxonomies.publish(data, undefined, params) : taxonomies.publish(data); + return params ? taxonomyInstance.publish(data, undefined, params) : taxonomyInstance.publish(data); } } From c5bc3a735def4f49cf84450bb8af94bb496db6ad Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 22 Jul 2026 11:57:50 +0530 Subject: [PATCH 12/13] fix(bulk-assets): collect all locales and deduplicate asset UIDs in bulk payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prepareAssetBulkPayload was deriving locales from items[0].publish_details, which only contains the locale of the first item (always en-us when multiple locales are passed). Since fetchAssets creates one item per asset per locale, the ar (or any non-first) locale was silently dropped from the Bulk API payload. Also fixes duplicate asset UIDs: items had N-assets × M-locales entries, so the assets array contained each UID repeated M times. Now deduplicated by UID. Fix mirrors prepareEntryBulkPayload which already used Array.from(new Set(...)) for locales correctly. Co-Authored-By: Claude Sonnet 4.6 --- .../src/services/bulk-operation-service.ts | 14 ++- .../services/bulk-operation-service.test.ts | 119 +++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/services/bulk-operation-service.ts b/packages/contentstack-bulk-operations/src/services/bulk-operation-service.ts index 705d1a90c..3ce649836 100644 --- a/packages/contentstack-bulk-operations/src/services/bulk-operation-service.ts +++ b/packages/contentstack-bulk-operations/src/services/bulk-operation-service.ts @@ -245,13 +245,17 @@ export class BulkOperationService { } private prepareAssetBulkPayload(items: AssetPublishData[], operation: OperationType): any { - const assets = items.map((item) => ({ - uid: item.uid, - version: item.version, - })); + const seen = new Set(); + const assets = items.reduce>((acc, item) => { + if (!seen.has(item.uid)) { + seen.add(item.uid); + acc.push({ uid: item.uid, version: item.version }); + } + return acc; + }, []); const environments = items[0]?.publish_details?.map((pd) => pd.environment) || []; - const locales = items[0]?.publish_details?.map((pd) => pd.locale) || []; + const locales = Array.from(new Set(items.map((item) => item.locale))); return { assets, diff --git a/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts b/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts index a0d1620fe..63dda27af 100644 --- a/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/services/bulk-operation-service.test.ts @@ -362,7 +362,7 @@ describe('BulkOperationService', () => { expect(payload.entries[0].variants).to.be.undefined; }); - it('should prepare asset payload', () => { + it('should prepare asset payload for single locale', () => { const mockItems: AssetPublishData[] = [ { uid: 'asset1', @@ -380,6 +380,123 @@ describe('BulkOperationService', () => { expect(payload.assets).to.have.lengthOf(1); expect(payload.assets[0].uid).to.equal('asset1'); + expect(payload.locales).to.deep.equal(['en-us']); + expect(payload.environments).to.deep.equal(['production']); + }); + + it('should collect all locales from multi-locale asset items', () => { + // fetchAssets creates one item per asset per locale — simulate 2 assets × 2 locales + const mockItems: AssetPublishData[] = [ + { + uid: 'asset1', + version: 1, + locale: 'en-us', + publish_details: [ + { environment: 'beta', locale: 'en-us' }, + { environment: 'beta2', locale: 'en-us' }, + ], + }, + { + uid: 'asset2', + version: 1, + locale: 'en-us', + publish_details: [ + { environment: 'beta', locale: 'en-us' }, + { environment: 'beta2', locale: 'en-us' }, + ], + }, + { + uid: 'asset1', + version: 1, + locale: 'ar', + publish_details: [ + { environment: 'beta', locale: 'ar' }, + { environment: 'beta2', locale: 'ar' }, + ], + }, + { + uid: 'asset2', + version: 1, + locale: 'ar', + publish_details: [ + { environment: 'beta', locale: 'ar' }, + { environment: 'beta2', locale: 'ar' }, + ], + }, + ]; + + const payload = (bulkOperationService as any).prepareBulkPayload( + mockItems, + OperationType.PUBLISH, + ResourceType.ASSET + ); + + // Both locales must appear in the payload + expect(payload.locales).to.include('en-us'); + expect(payload.locales).to.include('ar'); + expect(payload.locales).to.have.lengthOf(2); + }); + + it('should deduplicate asset UIDs when items contain one entry per locale per asset', () => { + // 2 assets × 2 locales = 4 items, but payload should have only 2 unique asset UIDs + const mockItems: AssetPublishData[] = [ + { uid: 'asset1', version: 1, locale: 'en-us', publish_details: [{ environment: 'beta', locale: 'en-us' }] }, + { uid: 'asset2', version: 2, locale: 'en-us', publish_details: [{ environment: 'beta', locale: 'en-us' }] }, + { uid: 'asset1', version: 1, locale: 'ar', publish_details: [{ environment: 'beta', locale: 'ar' }] }, + { uid: 'asset2', version: 2, locale: 'ar', publish_details: [{ environment: 'beta', locale: 'ar' }] }, + ]; + + const payload = (bulkOperationService as any).prepareBulkPayload( + mockItems, + OperationType.PUBLISH, + ResourceType.ASSET + ); + + expect(payload.assets).to.have.lengthOf(2); + const uids = payload.assets.map((a: any) => a.uid); + expect(uids).to.deep.equal(['asset1', 'asset2']); + }); + + it('should collect all environments from multi-env asset items', () => { + const mockItems: AssetPublishData[] = [ + { + uid: 'asset1', + version: 1, + locale: 'en-us', + publish_details: [ + { environment: 'beta', locale: 'en-us' }, + { environment: 'beta2', locale: 'en-us' }, + { environment: 'beta3', locale: 'en-us' }, + ], + }, + ]; + + const payload = (bulkOperationService as any).prepareBulkPayload( + mockItems, + OperationType.PUBLISH, + ResourceType.ASSET + ); + + expect(payload.environments).to.deep.equal(['beta', 'beta2', 'beta3']); + }); + + it('should include all locales and deduplicated assets together for unpublish', () => { + const mockItems: AssetPublishData[] = [ + { uid: 'asset1', version: 1, locale: 'en-us', publish_details: [{ environment: 'beta', locale: 'en-us' }] }, + { uid: 'asset1', version: 1, locale: 'ar', publish_details: [{ environment: 'beta', locale: 'ar' }] }, + { uid: 'asset1', version: 1, locale: 'fr-fr', publish_details: [{ environment: 'beta', locale: 'fr-fr' }] }, + ]; + + const payload = (bulkOperationService as any).prepareBulkPayload( + mockItems, + OperationType.UNPUBLISH, + ResourceType.ASSET + ); + + expect(payload.assets).to.have.lengthOf(1); + expect(payload.assets[0].uid).to.equal('asset1'); + expect(payload.locales).to.have.lengthOf(3); + expect(payload.locales).to.include.members(['en-us', 'ar', 'fr-fr']); }); it('should handle items with no publish_details', () => { From 411be705fdfa9b0b460245331dc6bb69a43eed31 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 22 Jul 2026 17:54:06 +0530 Subject: [PATCH 13/13] test(external-migrate): add scheduleEntryAction api_version header assertions Covers all four publish/unpublish paths (entry publish, entry unpublish, asset publish via sys_assets, asset unpublish via sys_assets) and verifies api_version: '3.2' is present in headers on every call. Also asserts branch header inclusion and omission. Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 + .../test/lib/create-stack.test.ts | 93 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 packages/contentstack-external-migrate/test/lib/create-stack.test.ts diff --git a/.talismanrc b/.talismanrc index b27ab5bad..67937a3a2 100644 --- a/.talismanrc +++ b/.talismanrc @@ -17,6 +17,8 @@ fileignoreconfig: checksum: ca699a9d73757d44ded4d9d27beb4f460f911a7c88a14551a9402a3ac0c69873 - filename: packages/contentstack-external-migrate/src/lib/create-stack.ts checksum: 68a9510db6f2746ac5006c091d276c1ba619a9e15c76a3edae3967e4f9c2dd4e +- filename: packages/contentstack-external-migrate/test/lib/create-stack.test.ts + checksum: 45af4e45e4baadf7e418a11a46a6243b8ffabafdbf32d0269d89c5d3db837a13 - filename: CHANGELOG.md checksum: 88c7e1dee308fa4ae25e7815ead0842b1aac7ed669b02859cc8b25cba879595d - filename: packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts diff --git a/packages/contentstack-external-migrate/test/lib/create-stack.test.ts b/packages/contentstack-external-migrate/test/lib/create-stack.test.ts new file mode 100644 index 000000000..0b09b75ad --- /dev/null +++ b/packages/contentstack-external-migrate/test/lib/create-stack.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { scheduleEntryAction } from '../../src/lib/create-stack'; + +const { mockPost } = vi.hoisted(() => ({ mockPost: vi.fn() })); + +vi.mock('axios', () => ({ default: { post: mockPost } })); + +vi.mock('@contentstack/cli-utilities', () => ({ + configHandler: { + get: vi.fn().mockImplementation((key: string) => { + if (key === 'authorisationType') return 'BASIC'; + if (key === 'authtoken') return 'test-authtoken'; + if (key === 'region') return 'NA'; + return undefined; + }), + }, + authHandler: { + checkExpiryAndRefresh: vi.fn().mockResolvedValue(undefined), + }, +})); + +describe('scheduleEntryAction', () => { + const API_KEY = 'blt-test-api-key'; + const ENTRY_OPTS = { + contentTypeUid: 'blog', + entryUid: 'entry123', + action: 'publish' as const, + environment: 'development', + locale: 'en-us', + scheduledAt: '2026-08-01T10:00:00.000Z', + }; + + beforeEach(() => { + mockPost.mockReset(); + mockPost.mockResolvedValue({ data: {} }); + }); + + it('sends api_version: 3.2 header on entry publish', async () => { + await scheduleEntryAction(API_KEY, ENTRY_OPTS); + + expect(mockPost).toHaveBeenCalledOnce(); + const [url, , { headers }] = mockPost.mock.calls[0]; + expect(url).toContain('/v3/content_types/blog/entries/entry123/publish'); + expect(headers).toMatchObject({ api_version: '3.2' }); + }); + + it('sends api_version: 3.2 header on entry unpublish', async () => { + await scheduleEntryAction(API_KEY, { ...ENTRY_OPTS, action: 'unpublish' }); + + const [url, , { headers }] = mockPost.mock.calls[0]; + expect(url).toContain('/v3/content_types/blog/entries/entry123/unpublish'); + expect(headers).toMatchObject({ api_version: '3.2' }); + }); + + it('sends api_version: 3.2 header on asset publish (sys_assets)', async () => { + await scheduleEntryAction(API_KEY, { + ...ENTRY_OPTS, + contentTypeUid: 'sys_assets', + entryUid: 'asset456', + }); + + const [url, , { headers }] = mockPost.mock.calls[0]; + expect(url).toContain('/v3/assets/asset456/publish'); + expect(headers).toMatchObject({ api_version: '3.2' }); + }); + + it('sends api_version: 3.2 header on asset unpublish (sys_assets)', async () => { + await scheduleEntryAction(API_KEY, { + ...ENTRY_OPTS, + contentTypeUid: 'sys_assets', + entryUid: 'asset456', + action: 'unpublish', + }); + + const [url, , { headers }] = mockPost.mock.calls[0]; + expect(url).toContain('/v3/assets/asset456/unpublish'); + expect(headers).toMatchObject({ api_version: '3.2' }); + }); + + it('includes branch in headers alongside api_version when branch option is provided', async () => { + await scheduleEntryAction(API_KEY, { ...ENTRY_OPTS, branch: 'feature-branch' }); + + const [, , { headers }] = mockPost.mock.calls[0]; + expect(headers).toMatchObject({ api_version: '3.2', branch: 'feature-branch' }); + }); + + it('omits branch from headers when no branch option is given', async () => { + await scheduleEntryAction(API_KEY, ENTRY_OPTS); + + const [, , { headers }] = mockPost.mock.calls[0]; + expect(headers).not.toHaveProperty('branch'); + }); +});