From 64f2f2356ed73657abbc3b5aae5feaf465f9a963 Mon Sep 17 00:00:00 2001 From: Jing Sun Date: Tue, 26 May 2026 10:06:25 +0200 Subject: [PATCH 1/2] add baseVersion parameter to config diff --- docs/user-guide/config-commands.md | 21 +++ .../configuration-management/api/diff-api.ts | 5 +- .../config-command.service.ts | 4 +- .../configuration-management/diff.service.ts | 8 +- .../configuration-management/module.ts | 6 +- .../config-diff.spec.ts | 132 ++++++++---------- .../configuration-management/module.spec.ts | 67 +++++++++ 7 files changed, 158 insertions(+), 85 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index ebb59d21..ecf602fd 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -715,3 +715,24 @@ To export the node dependencies as a JSON file, use the `--json` option: content-cli config nodes dependencies list --packageKey --nodeKey --packageVersion --json content-cli config nodes dependencies list --packageKey --nodeKey --json ``` + +## Diff local zip with deployed version/specific version/staging + +To compare local zipped packages with online packages use: +```bash +content-cli config diff --file +``` + +As with other commands, use `--json` to export the diff to a file. +To diff against a specific version use the `--baseVersion` parameter. When omitted it will diff against the current deployed version. +To diff against staging use `--baseVersion STAGING`. + +```bash +content-cli config diff --file --baseVersion +``` + +To diff against the current deployed version and only return whether there are any changes, use the `--hasChanges` flag. + +```bash +content-cli config diff --file --hasChanges +``` \ No newline at end of file diff --git a/src/commands/configuration-management/api/diff-api.ts b/src/commands/configuration-management/api/diff-api.ts index 79ca0bcb..4a06003a 100644 --- a/src/commands/configuration-management/api/diff-api.ts +++ b/src/commands/configuration-management/api/diff-api.ts @@ -11,9 +11,10 @@ export class DiffApi { this.httpClient = () => context.httpClient; } - public async diffPackages(data: FormData): Promise { + public async diffPackages(baseVersion: string, data: FormData): Promise { + const paramString = baseVersion ? "?" + new URLSearchParams({"baseVersion": baseVersion}).toString() : ""; return this.httpClient().postFile( - "/package-manager/api/core/packages/diff/configuration", + `/package-manager/api/core/packages/diff/configuration${paramString}`, data ); } diff --git a/src/commands/configuration-management/config-command.service.ts b/src/commands/configuration-management/config-command.service.ts index b319be48..510d44cd 100644 --- a/src/commands/configuration-management/config-command.service.ts +++ b/src/commands/configuration-management/config-command.service.ts @@ -86,8 +86,8 @@ export class ConfigCommandService { return this.batchImportExportService.batchImportPackages(sourcePath, overwrite, gitBranch, performValidation); } - public diffPackages(file: string, hasChanges: boolean, jsonResponse: boolean): Promise { - return this.diffService.diffPackages(file, hasChanges, jsonResponse); + public diffPackages(file: string, hasChanges: boolean, baseVersion: string, jsonResponse: boolean): Promise { + return this.diffService.diffPackages(file, hasChanges, baseVersion, jsonResponse); } private async listPackagesByVariableValue(jsonResponse: boolean, flavors: string[], variableValue: string, variableType: string, includeBranches: boolean): Promise { diff --git a/src/commands/configuration-management/diff.service.ts b/src/commands/configuration-management/diff.service.ts index 23f676e7..3b3466da 100644 --- a/src/commands/configuration-management/diff.service.ts +++ b/src/commands/configuration-management/diff.service.ts @@ -16,11 +16,11 @@ export class DiffService { this.diffApi = new DiffApi(context); } - public async diffPackages(file: string, hasChanges: boolean, jsonResponse: boolean): Promise { + public async diffPackages(file: string, hasChanges: boolean, baseVersion: string, jsonResponse: boolean): Promise { if (hasChanges) { await this.hasChanges(file, jsonResponse); } else { - await this.diffPackagesAndReturnDiff(file, jsonResponse); + await this.diffPackagesAndReturnDiff(baseVersion, file, jsonResponse); } } @@ -36,10 +36,10 @@ export class DiffService { } } - private async diffPackagesAndReturnDiff(file: string, jsonResponse: boolean): Promise { + private async diffPackagesAndReturnDiff(baseVersion: string, file: string, jsonResponse: boolean): Promise { const packages = new AdmZip(file); const formData = this.buildBodyForDiff(packages); - const returnedHasChangesData = await this.diffApi.diffPackages(formData); + const returnedHasChangesData = await this.diffApi.diffPackages(baseVersion, formData); if (jsonResponse) { this.exportListOfPackageDiffs(returnedHasChangesData); diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index 8a80e0c2..a957a8da 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -71,6 +71,7 @@ class Module extends IModule { configCommand.command("diff") .description("Command to diff configs of packages") .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") + .option("--baseVersion ", "Compare against a given version or STAGING, not compatible with --hasChanges") .option("--json", "Return the response as a JSON file") .requiredOption("-f, --file ", "Exported packages file (relative or absolute path)") .action(this.diffPackages); @@ -251,7 +252,10 @@ class Module extends IModule { } private async diffPackages(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).diffPackages(options.file, options.hasChanges, options.json); + if (options.hasChanges && options.baseVersion) { + throw new Error("You cannot use hasChanges and baseVersion at the same time."); + } + await new ConfigCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); } private async validatePackage(context: Context, command: Command, options: OptionValues): Promise { diff --git a/tests/commands/configuration-management/config-diff.spec.ts b/tests/commands/configuration-management/config-diff.spec.ts index 925c8840..93f9f604 100644 --- a/tests/commands/configuration-management/config-diff.spec.ts +++ b/tests/commands/configuration-management/config-diff.spec.ts @@ -15,6 +15,47 @@ import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; import { ConfigUtils } from "../../utls/config-utils"; +function mockZipDiff(expectedUrl: string): PackageDiffTransport[] { + const manifest: PackageManifestTransport[] = []; + manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO")); + + const firstPackageNode = ConfigUtils.buildPackageNode("package-key", {metadata: {description: "test"}, variables: [], dependencies: []}); + const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST"); + const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0"); + const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]); + + mockReadFileSync(exportedPackagesZip.toBuffer()); + mockCreateReadStream(exportedPackagesZip.toBuffer()); + + const diffResponse: PackageDiffTransport[] = [{ + packageKey: "package-key", + packageChanges: [ + { + op: "add", + path: "/test", + from: "bbbb", + value: JSON.parse("123"), + fromValue: null + }], + nodesWithChanges: [{ + nodeKey: firstChildNode.key, + name: firstChildNode.name, + type: firstChildNode.type, + changeType: NodeConfigurationChangeType.ADDED, + changes: [{ + op: "add", + path: "/test", + from: "bbb", + value: JSON.parse("234"), + fromValue: null + }] + }] + }]; + + mockAxiosPost(expectedUrl, diffResponse); + return diffResponse; +} + describe("Config diff", () => { beforeEach(() => { @@ -40,7 +81,7 @@ describe("Config diff", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, false); + await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, null, false); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -49,45 +90,20 @@ describe("Config diff", () => { }); it("Should show diff on terminal with hasChanges set to false and jsonResponse false", async () => { - const manifest: PackageManifestTransport[] = []; - manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO")); - - const firstPackageNode = ConfigUtils.buildPackageNode("package-key", {metadata: {description: "test"}, variables: [], dependencies: []}); - const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST"); - const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0"); - const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]); + const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration"); - mockReadFileSync(exportedPackagesZip.toBuffer()); - mockCreateReadStream(exportedPackagesZip.toBuffer()); + await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, null, false); - const diffResponse: PackageDiffTransport[] = [{ - packageKey: "package-key", - packageChanges: [ - { - op: "add", - path: "/test", - from: "bbbb", - value: JSON.parse("123"), - fromValue: null - }], - nodesWithChanges: [{ - nodeKey: firstChildNode.key, - name: firstChildNode.name, - type: firstChildNode.type, - changeType: NodeConfigurationChangeType.ADDED, - changes: [{ - op: "add", - path: "/test", - from: "bbb", - value: JSON.parse("234"), - fromValue: null - }] - }] - }]; + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain( + JSON.stringify(diffResponse, null, 2) + ); + }); - mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration", diffResponse); + it("Should compare with specified version", async () => { + const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration?baseVersion=1.0.0"); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, false); + await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, "1.0.0", false); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -96,45 +112,9 @@ describe("Config diff", () => { }); it("Should generate a json file with diff info when hasChanges is set to false and jsonResponse is set to true", async () => { - const manifest: PackageManifestTransport[] = []; - manifest.push(ConfigUtils.buildManifestForKeyAndFlavor("package-key", "STUDIO")); - - const firstPackageNode = ConfigUtils.buildPackageNode("package-key", {metadata: {description: "test"}, variables: [], dependencies: []}); - const firstChildNode = ConfigUtils.buildChildNode("key-1", "package-key", "TEST"); - const firstPackageZip = ConfigUtils.buildExportPackageZip(firstPackageNode, [firstChildNode], "1.0.0"); - const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, [firstPackageZip]); + const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration"); - mockReadFileSync(exportedPackagesZip.toBuffer()); - mockCreateReadStream(exportedPackagesZip.toBuffer()); - - const diffResponse: PackageDiffTransport[] = [{ - packageKey: "package-key", - packageChanges: [ - { - op: "add", - path: "/test", - from: "bbbb", - value: JSON.parse("123"), - fromValue: null - }], - nodesWithChanges: [{ - nodeKey: firstChildNode.key, - name: firstChildNode.name, - type: firstChildNode.type, - changeType: NodeConfigurationChangeType.ADDED, - changes: [{ - op: "add", - path: "/test", - from: "bbb", - value: JSON.parse("234"), - fromValue: null - }] - }] - }]; - - mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration", diffResponse); - - await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, true); + await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, null, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -142,7 +122,7 @@ describe("Config diff", () => { const exportedPackageDiffTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as PackageDiffTransport[]; expect(exportedPackageDiffTransport.length).toBe(1); - const exportedFirstPackageDiffTransport = exportedPackageDiffTransport.filter(diffTransport => diffTransport.packageKey === firstPackageNode.key); + const exportedFirstPackageDiffTransport = exportedPackageDiffTransport.filter(diffTransport => diffTransport.packageKey === "package-key"); expect(exportedFirstPackageDiffTransport).toEqual(diffResponse); }); @@ -165,7 +145,7 @@ describe("Config diff", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, true); + await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, null, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 39441346..77fef513 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -32,6 +32,7 @@ describe("Configuration Management Module - Action Validations", () => { listVariables: jest.fn().mockResolvedValue(undefined), batchExportPackages: jest.fn().mockResolvedValue(undefined), batchImportPackages: jest.fn().mockResolvedValue(undefined), + diffPackages: jest.fn().mockResolvedValue(undefined), } as any; mockNodeDependencyService = { @@ -769,5 +770,71 @@ describe("Configuration Management Module - Action Validations", () => { ); }); }); + + describe("diffPackages", () => { + it("should call diffPackages using minimal parameters", async () => { + const options: OptionValues = { + file: "package.zip", + }; + + await (module as any).diffPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + "package.zip", undefined, undefined, undefined + ); + }); + + it("should pass json parameter", async () => { + const options: OptionValues = { + file: "package.zip", + json: true, + }; + + await (module as any).diffPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + "package.zip", undefined, undefined, true + ); + }); + + it("should pass hasChanges parameter", async () => { + const options: OptionValues = { + file: "package.zip", + hasChanges: true, + }; + + await (module as any).diffPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + "package.zip", true, undefined, undefined + ); + }); + + it("should pass baseVersion parameter", async () => { + const options: OptionValues = { + file: "package.zip", + baseVersion: "1.0.0", + }; + + await (module as any).diffPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + "package.zip", undefined, "1.0.0", undefined + ); + }); + + it("should throw error when hasChanges and baseVersion are used together", async () => { + const options: OptionValues = { + file: "package.zip", + hasChanges: true, + baseVersion: "STAGING" + }; + + await expect( + (module as any).diffPackages(testContext, mockCommand, options) + ).rejects.toThrow("You cannot use hasChanges and baseVersion at the same time."); + + }); + }); }); From 4923848b71eecabe50bd11d3cecaea71565712f4 Mon Sep 17 00:00:00 2001 From: Jing Sun Date: Thu, 28 May 2026 09:52:19 +0200 Subject: [PATCH 2/2] allow hasChanges with baseVersion to be used in conjunction with config diff --- src/commands/configuration-management/api/diff-api.ts | 5 +++-- src/commands/configuration-management/diff.service.ts | 6 +++--- src/commands/configuration-management/module.ts | 3 --- tests/commands/configuration-management/module.spec.ts | 9 +++++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/commands/configuration-management/api/diff-api.ts b/src/commands/configuration-management/api/diff-api.ts index 4a06003a..e3739e91 100644 --- a/src/commands/configuration-management/api/diff-api.ts +++ b/src/commands/configuration-management/api/diff-api.ts @@ -19,9 +19,10 @@ export class DiffApi { ); } - public async hasChanges(data: FormData): Promise { + public async hasChanges(baseVersion: string, data: FormData): Promise { + const paramString = baseVersion ? "?" + new URLSearchParams({"baseVersion": baseVersion}).toString() : ""; return this.httpClient().postFile( - "/package-manager/api/core/packages/diff/configuration/has-changes", + `/package-manager/api/core/packages/diff/configuration/has-changes${paramString}`, data ); } diff --git a/src/commands/configuration-management/diff.service.ts b/src/commands/configuration-management/diff.service.ts index 3b3466da..d0246bd5 100644 --- a/src/commands/configuration-management/diff.service.ts +++ b/src/commands/configuration-management/diff.service.ts @@ -18,16 +18,16 @@ export class DiffService { public async diffPackages(file: string, hasChanges: boolean, baseVersion: string, jsonResponse: boolean): Promise { if (hasChanges) { - await this.hasChanges(file, jsonResponse); + await this.hasChanges(baseVersion, file, jsonResponse); } else { await this.diffPackagesAndReturnDiff(baseVersion, file, jsonResponse); } } - private async hasChanges(file: string, jsonResponse: boolean): Promise { + private async hasChanges(baseVersion: string, file: string, jsonResponse: boolean): Promise { const packages = new AdmZip(file); const formData = this.buildBodyForDiff(packages); - const returnedHasChangesData = await this.diffApi.hasChanges(formData); + const returnedHasChangesData = await this.diffApi.hasChanges(baseVersion, formData); if (jsonResponse) { this.exportListOfPackageDiffMetadata(returnedHasChangesData); diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index a957a8da..40b8063d 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -252,9 +252,6 @@ class Module extends IModule { } private async diffPackages(context: Context, command: Command, options: OptionValues): Promise { - if (options.hasChanges && options.baseVersion) { - throw new Error("You cannot use hasChanges and baseVersion at the same time."); - } await new ConfigCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); } diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 77fef513..e22a1285 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -823,17 +823,18 @@ describe("Configuration Management Module - Action Validations", () => { ); }); - it("should throw error when hasChanges and baseVersion are used together", async () => { + it("should pass both parameters when hasChanges and baseVersion are used together", async () => { const options: OptionValues = { file: "package.zip", hasChanges: true, baseVersion: "STAGING" }; - await expect( - (module as any).diffPackages(testContext, mockCommand, options) - ).rejects.toThrow("You cannot use hasChanges and baseVersion at the same time."); + await (module as any).diffPackages(testContext, mockCommand, options); + expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + "package.zip", true, "STAGING", undefined + ); }); }); });