From d1dfdce6565d394486aa2d86769784029226bb0e Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 23 Oct 2026 11:57:02 +0200 Subject: [PATCH] SP-3446: Add --staging option to t2tc package export Export the staging state of the given package keys (including branch packages) through the staging batch export endpoint, and fetch staging variable values for packages exported from staging. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/t2tc-commands.md | 5 ++ src/commands/t2tc/api/t2tc-package-api.ts | 10 ++++ .../t2tc/batch-export-import.constants.ts | 3 +- src/commands/t2tc/module.ts | 6 ++- src/commands/t2tc/t2tc-command.service.ts | 4 +- src/commands/t2tc/t2tc-package.service.ts | 32 +++++++++++-- tests/commands/t2tc/module.spec.ts | 33 +++++++++++++ .../commands/t2tc/t2tc-package-export.spec.ts | 48 ++++++++++++++++++- 8 files changed, 132 insertions(+), 9 deletions(-) diff --git a/docs/user-guide/t2tc-commands.md b/docs/user-guide/t2tc-commands.md index b31abe55..740f02ad 100644 --- a/docs/user-guide/t2tc-commands.md +++ b/docs/user-guide/t2tc-commands.md @@ -93,6 +93,11 @@ content-cli t2tc package export -p --packageKeys key1 ... keyN The `--keysByVersion` option can be used to export packages by specific version. You can specify multiple packages with version separated by spaces, in the format of 'packageKey.version'. The `--withDependencies` option can be used to also export dependencies of the given packages. The `--unzip` option can be used to unzip the exported packages into the current working directory. +The `--staging` option exports the current staging state of the given `--packageKeys` instead of their latest deployed version. Branch package keys (`@`) are supported. Staging packages appear as `_staging.zip`, use `staging` as their version in `manifest.json` and `variables.json`, and their staging variable values are exported. With `--withDependencies`, dependencies are exported at the published versions the staging configuration references. `--staging` cannot be combined with `--keysByVersion`. + +```bash +content-cli t2tc package export -p --packageKeys projectKey@branchKey --staging --withDependencies +``` Depending on the `--unzip` option used, a zip file, or a directory containing the exported packages, will be created in the current working directory containing: diff --git a/src/commands/t2tc/api/t2tc-package-api.ts b/src/commands/t2tc/api/t2tc-package-api.ts index 27e4cf0b..15cea3e1 100644 --- a/src/commands/t2tc/api/t2tc-package-api.ts +++ b/src/commands/t2tc/api/t2tc-package-api.ts @@ -74,6 +74,16 @@ export class T2tcPackageApi { }); } + public async exportStagingPackages(packageKeys: string[], withDependencies: boolean): Promise { + const queryParams = new URLSearchParams(); + packageKeys.forEach(packageKey => queryParams.append("packageKeys", packageKey)); + queryParams.set("withDependencies", withDependencies.toString()); + + return this.httpClient().getFile(`/package-manager/api/core/staging/packages/export/batch?${queryParams.toString()}`).catch(e => { + throw new FatalError(`Problem exporting staging packages: ${e}`); + }); + } + public async exportPackagesByVersions(packageKeysWithVersion: string[], withDependencies: boolean): Promise { const queryParams = new URLSearchParams(); packageKeysWithVersion.forEach(packageKeyByVersion => queryParams.append("packageKeysWithVersion", packageKeyByVersion)); diff --git a/src/commands/t2tc/batch-export-import.constants.ts b/src/commands/t2tc/batch-export-import.constants.ts index 534543c6..92ca2144 100644 --- a/src/commands/t2tc/batch-export-import.constants.ts +++ b/src/commands/t2tc/batch-export-import.constants.ts @@ -7,5 +7,6 @@ export enum BatchExportImportConstants { ZIP_EXTENSION = ".zip", JSON_EXTENSION = ".json", NODES_FOLDER_NAME = "nodes/", - SCENARIO_NODE = "SCENARIO" + SCENARIO_NODE = "SCENARIO", + STAGING_VERSION = "staging" } diff --git a/src/commands/t2tc/module.ts b/src/commands/t2tc/module.ts index 7da3374d..f89aa688 100644 --- a/src/commands/t2tc/module.ts +++ b/src/commands/t2tc/module.ts @@ -33,6 +33,7 @@ class Module extends IModule { .option("--unzip", "Unzip the exported file", "") .option("--gitProfile ", "Git profile which you want to use for the Git operations") .option("--gitBranch ", "Git branch in which you want to push the exported file") + .option("--staging", "Export the staging state of the given --packageKeys (including branch packages) instead of the latest deployed version", false) .action(this.batchExportPackages); t2tcPackageCommand.command("import") @@ -81,8 +82,11 @@ class Module extends IModule { if (options.gitProfile && !options.gitBranch) { throw new Error("Please specify a branch using --gitBranch when using a Git profile."); } + if (options.staging && options.keysByVersion) { + throw new Error("Staging parameter is only supported with --packageKeys, not --keysByVersion."); + } options.withDependencies = options.withDependencies ?? false; - await new T2tcCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch, options.unzip); + await new T2tcCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch, options.unzip, options.staging); } private async batchImportPackages(context: Context, command: Command, options: OptionValues): Promise { diff --git a/src/commands/t2tc/t2tc-command.service.ts b/src/commands/t2tc/t2tc-command.service.ts index eb75e313..fd665c2d 100644 --- a/src/commands/t2tc/t2tc-command.service.ts +++ b/src/commands/t2tc/t2tc-command.service.ts @@ -39,8 +39,8 @@ export class T2tcCommandService { } } - public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { - return this.t2tcPackageService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies, gitBranch, unzip); + public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean, staging: boolean = false): Promise { + return this.t2tcPackageService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies, gitBranch, unzip, staging); } public batchImportPackages(file: string, directory: string, overwrite: boolean, gitBranch: string, performValidation: boolean = false): Promise { diff --git a/src/commands/t2tc/t2tc-package.service.ts b/src/commands/t2tc/t2tc-package.service.ts index 35301ede..31fcc985 100644 --- a/src/commands/t2tc/t2tc-package.service.ts +++ b/src/commands/t2tc/t2tc-package.service.ts @@ -16,6 +16,7 @@ import { parse, stringify } from "../../core/utils/json"; import { PackageApi } from "../studio/api/package-api"; import { T2tcPackageApi } from "./api/t2tc-package-api"; import { VariableApi } from "../configuration-management/api/variable-api"; +import { StagingPackageVariablesApi } from "../configuration-management/api/staging-package-variables-api"; import { fixConnectionVariables } from "../configuration-management/connection-variable.helper"; import { StudioService } from "./studio.service"; import { GitService } from "../../core/git-profile/git/git.service"; @@ -26,6 +27,7 @@ export class T2tcPackageService { private t2tcPackageApi: T2tcPackageApi; private variableApi: VariableApi; + private stagingPackageVariablesApi: StagingPackageVariablesApi; private studioPackageApi: PackageApi; private studioService: StudioService; @@ -35,6 +37,7 @@ export class T2tcPackageService { constructor(context: Context) { this.t2tcPackageApi = new T2tcPackageApi(context); this.variableApi = new VariableApi(context); + this.stagingPackageVariablesApi = new StagingPackageVariablesApi(context); this.studioPackageApi = new PackageApi(context); this.studioService = new StudioService(context); @@ -72,9 +75,11 @@ export class T2tcPackageService { }); } - public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { + public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean, staging: boolean = false): Promise { let exportedPackagesData: Buffer; - if (packageKeys) { + if (staging) { + exportedPackagesData = await this.t2tcPackageApi.exportStagingPackages(packageKeys, withDependencies); + } else if (packageKeys) { exportedPackagesData = await this.t2tcPackageApi.exportPackages(packageKeys, withDependencies); } else { exportedPackagesData = await this.t2tcPackageApi.exportPackagesByVersions(packageKeysByVersion, withDependencies); @@ -184,10 +189,15 @@ export class T2tcPackageService { return versionsByPackageKey; } - private getVersionedVariablesForPackagesWithKeys(versionsByPackageKey: Map): Promise { + private async getVersionedVariablesForPackagesWithKeys(versionsByPackageKey: Map): Promise { const variableExportRequest: PackageKeyAndVersionPair[] = []; + const stagingPackageKeys: string[] = []; versionsByPackageKey?.forEach((versions, key) => { versions?.forEach(version => { + if (version === BatchExportImportConstants.STAGING_VERSION) { + stagingPackageKeys.push(key); + return; + } variableExportRequest.push({ packageKey: key, version: version, @@ -195,7 +205,21 @@ export class T2tcPackageService { }) }); - return this.variableApi.findVariablesWithValuesByPackageKeysAndVersion(variableExportRequest) + const versionedVariables = await this.variableApi.findVariablesWithValuesByPackageKeysAndVersion(variableExportRequest); + const stagingVariables = stagingPackageKeys.length + ? await this.getStagingVariablesForPackagesWithKeys(stagingPackageKeys) + : []; + + return [...versionedVariables, ...stagingVariables]; + } + + private async getStagingVariablesForPackagesWithKeys(packageKeys: string[]): Promise { + const stagingVariables = await this.stagingPackageVariablesApi.findAllByPackageKeys(packageKeys); + return stagingVariables.map(stagingVariable => ({ + packageKey: stagingVariable.packageKey, + version: BatchExportImportConstants.STAGING_VERSION, + variables: stagingVariable.variables ?? [], + })); } private buildBodyForImport(configs: AdmZip, sourcePath: string, variablesManifests: VariableManifestTransport[]): FormData { diff --git a/tests/commands/t2tc/module.spec.ts b/tests/commands/t2tc/module.spec.ts index 270adfdb..bca5a738 100644 --- a/tests/commands/t2tc/module.spec.ts +++ b/tests/commands/t2tc/module.spec.ts @@ -154,6 +154,39 @@ describe("T2TC Module", () => { undefined, false, undefined, + true, + undefined + ); + }); + + it("throws when --staging is combined with --keysByVersion", async () => { + const options: OptionValues = { + keysByVersion: ["package1.1.0.0"], + staging: true, + }; + + await expect( + (module as any).batchExportPackages(testContext, mockCommand, options) + ).rejects.toThrow("Staging parameter is only supported with --packageKeys, not --keysByVersion."); + + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); + }); + + it("delegates to T2tcCommandService with staging enabled", async () => { + const options: OptionValues = { + packageKeys: ["project@my-branch"], + withDependencies: true, + staging: true, + }; + + await (module as any).batchExportPackages(testContext, mockCommand, options); + + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( + ["project@my-branch"], + undefined, + true, + undefined, + undefined, true ); }); diff --git a/tests/commands/t2tc/t2tc-package-export.spec.ts b/tests/commands/t2tc/t2tc-package-export.spec.ts index 7cf81c0d..5c708ddf 100644 --- a/tests/commands/t2tc/t2tc-package-export.spec.ts +++ b/tests/commands/t2tc/t2tc-package-export.spec.ts @@ -1210,4 +1210,50 @@ describe("Config export", () => { expect(getDownloadedFileName()).not.toBeNull(); }) -}) \ No newline at end of file + it("Should export staging packages from the staging endpoint with staging variables", async () => { + const manifest: PackageManifestTransport[] = []; + manifest.push(ConfigUtils.buildManifestForKeyAndFlavorAndVersion("project@my-branch", "TEST", null, { + [BatchExportImportConstants.STAGING_VERSION]: [], + } as unknown as Map)); + manifest.push(ConfigUtils.buildManifestForKeyAndFlavorAndVersion("dependency-key", "TEST", "1.0.0", { + "1.0.0": [], + } as unknown as Map)); + const exportedPackagesZip = ConfigUtils.buildBatchExportZip(manifest, []); + + const stagingVariable = { + key: "staging-variable", + type: PackageManagerVariableType.PLAIN_TEXT, + value: "staging-value", + metadata: {}, + }; + const versionedVariables: VariableManifestTransport[] = [{ + packageKey: "dependency-key", + version: "1.0.0", + variables: [], + }]; + + mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/staging/packages/export/batch?packageKeys=project%40my-branch&withDependencies=true", exportedPackagesZip.toBuffer()); + mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch/variables-with-assignments", versionedVariables); + mockAxiosPost("https://myTeam.celonis.cloud/pacman/api/core/staging/packages/variables/by-package-keys", [{ + packageKey: "project@my-branch", + variables: [stagingVariable], + }]); + + await new T2tcCommandService(testContext).batchExportPackages(["project@my-branch"], undefined, true, null, null, true); + + const versionedVariablesRequest = parse(mockedPostRequestBodyByUrl.get("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch/variables-with-assignments")); + expect(versionedVariablesRequest).toEqual([{packageKey: "dependency-key", version: "1.0.0"}]); + + const stagingVariablesRequest = parse(mockedPostRequestBodyByUrl.get("https://myTeam.celonis.cloud/pacman/api/core/staging/packages/variables/by-package-keys")); + expect(stagingVariablesRequest).toEqual(["project@my-branch"]); + + const actualZip = new AdmZip(getDownloadedFileName()); + const exportedVariables: VariableManifestTransport[] = parse(actualZip.getEntry(BatchExportImportConstants.VARIABLES_FILE_NAME).getData().toString()); + expect(exportedVariables).toContainEqual({ + packageKey: "project@my-branch", + version: BatchExportImportConstants.STAGING_VERSION, + variables: [stagingVariable], + }); + expect(exportedVariables).toContainEqual(versionedVariables[0]); + }) +})