diff --git a/.talismanrc b/.talismanrc index 126602cf1..67937a3a2 100644 --- a/.talismanrc +++ b/.talismanrc @@ -15,4 +15,12 @@ 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 +- 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 + checksum: abc5ac707341760cf59d5b8b1c4e13cf2c79955e2735c33e2db3ec6bc48eddb6 version: "" 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 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/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/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-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 diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index 9f82677a8..e0a6ba970 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -411,10 +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: '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/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/src/services/taxonomy-service.ts b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts index a5b0eb991..05c1e8a9b 100644 --- a/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts +++ b/packages/contentstack-bulk-operations/src/services/taxonomy-service.ts @@ -1,67 +1,30 @@ import type { ManagementStack, TaxonomyPublishJobResponse, TaxonomyPublishPayload } from '../interfaces'; import { OperationType } from '../interfaces'; -const DEFAULT_TAXONOMY_API_VERSION = '3.2'; - -type TaxonomyPublishWithBranch = ( - data: TaxonomyPublishPayload, - apiVersion?: string, - params?: { branch?: string } -) => Promise; - -type TaxonomyOperationApi = { - publish: TaxonomyPublishWithBranch; - unpublish: TaxonomyPublishWithBranch; -}; - 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 params = branch && branch !== 'main' ? { branch } : undefined; - if (params) { - return taxonomies.publish(data, apiVersion, params); + if (operation === OperationType.UNPUBLISH) { + return params ? taxonomyInstance.unpublish(data, undefined, params) : taxonomyInstance.unpublish(data); } - return taxonomies.publish(data, apiVersion); + return params ? taxonomyInstance.publish(data, undefined, params) : taxonomyInstance.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 c53deb98c..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, - apiVersion: flags['api-version'] || '3', 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/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; }); }); 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..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 @@ -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 () => { @@ -360,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', @@ -378,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', () => { 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 384e7d00f..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 @@ -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); @@ -336,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.equal('3'); // Default to 3 expect(config.maxRetries).to.equal(3); }); @@ -482,52 +479,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', () => { 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-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'); + }); +}); 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], 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; });