From 6d89b18c92a16d3764c43553b2ed2fbc3fbb8c34 Mon Sep 17 00:00:00 2001 From: "l.ajvazi" Date: Thu, 28 Aug 2025 16:43:35 +0200 Subject: [PATCH 1/4] TA-4197: Add deployment commands --- .github/CODEOWNERS | 1 + DOCUMENTATION.md | 68 ++++++++++ package.json | 2 +- src/commands/deployment/deployment-api.ts | 123 ++++++++++++++++++ .../deployment/deployment.interfaces.ts | 53 ++++++++ src/commands/deployment/deployment.service.ts | 108 +++++++++++++++ src/commands/deployment/module.ts | 91 +++++++++++++ .../deployment/deployment-create.spec.ts | 48 +++++++ .../deployment/deployment-list-active.spec.ts | 99 ++++++++++++++ .../deployment-list-deployables.spec.ts | 61 +++++++++ .../deployment-list-history.spec.ts | 61 +++++++++ .../deployment-list-targets.spec.ts | 57 ++++++++ 12 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 src/commands/deployment/deployment-api.ts create mode 100644 src/commands/deployment/deployment.interfaces.ts create mode 100644 src/commands/deployment/deployment.service.ts create mode 100644 src/commands/deployment/module.ts create mode 100644 tests/commands/deployment/deployment-create.spec.ts create mode 100644 tests/commands/deployment/deployment-list-active.spec.ts create mode 100644 tests/commands/deployment/deployment-list-deployables.spec.ts create mode 100644 tests/commands/deployment/deployment-list-history.spec.ts create mode 100644 tests/commands/deployment/deployment-list-targets.spec.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a121244..1dd3e0d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ /src/core @celonis/astro /src/commands/configuration-management/ @celonis/astro /src/commands/profile/ @celonis/astro +/src/commands/deployment/ @celonis/astro /src/commands/action-flows/ @celonis/process-automation /tests/commands/action-flows/ @celonis/process-automation /src/commands/analysis/ @celonis/process-analytics diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index bb4d79f8..fde67a2f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -569,6 +569,74 @@ This mapping should be saved and then used during import. Since the format of the variables.json file on import is the same JSON structure as the list variables result, you can either map the values to the variables.json file for each variable, or replace the variables.json file with the result of the listing & mapping altogether. If the mapping of variables is skipped, you should delete the variables.json file before importing. +### Deployment commands +The **deployment** command group allows you to create deployments, list their history, check active deployments, and retrieve deployables and targets. + +#### Create Deployment +Create a new deployment for a given package version in a target, based on the deployable type. +``` +content-cli deployment create --packageKey myPackage --packageVersion 1.0.0 --deployableType app-package --targetId targetId --profile my-profile-name +``` +The command will return the created deployment, or an error if the deployment could not be created. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +#### List Deployment History +List the deployment history, optionally filtered by package, target, type, status, or creator. +``` +content-cli deployment list history --profile my-profile-name +``` + +Additional filtering options: +- `--packageKey ` – Filter deployment history by package key +- `--targetId ` – Filter deployment history by target ID +- `--deployableType ` – Filter deployment history by deployable type +- `--status ` – Filter deployment history by status +- `--createdBy ` – Filter deployment history by creator + +It is also possible to use the '--json' option for writing the extended response to a file that gets created in the working directory. + +#### List Active Deployments +Get the active deployment(s) for a given target or package. + +For getting the active deployment for a target the `--targetId` option should be used: +``` +content-cli deployment list active --targetId targetId --profile my-profile-name +``` + +For getting the active deployment for a package the `--packageKey` option should be used: +``` +content-cli deployment list active --packageKey myPackage --profile my-profile-name +``` + +The `--targetIds` option can be used to filter the response by multiple target IDs. The option should be used with a comma-separated list of target IDs. + +The response of listing active deployments for a package will return all active deployments for that package across all targets. +The response is paginated, and the page size can be controlled with the `--limit` and `--offset` options (defaults to 100 and 0 respectively). + +For both options, it is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +Note: You must provide either `--packageKey` or `--targetId`, not both. + +#### List Deployables + +List all available deployables. + +``` +content-cli deployment list deployables --flavor STUDIO --profile my-profile-name +``` + +The `--flavor` option can be used to filter the deployables by flavor. The available flavors are: **STUDIO** and **OCDM**. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +#### List Targets +List all targets for a given deployable type (and optionally a package key). + +``` +content-cli deployment list targets --deployableType app-package --profile my-profile-name +``` + +The `--packageKey` option can be used to get targets for a specific package. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. ### Action Flows commands diff --git a/package.json b/package.json index 54489ddc..186c47b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@celonis/content-cli", - "version": "1.1.3", + "version": "1.1.4", "description": "CLI Tool to help manage content in Celonis Platform", "main": "content-cli.js", "bin": { diff --git a/src/commands/deployment/deployment-api.ts b/src/commands/deployment/deployment-api.ts new file mode 100644 index 00000000..655adcbf --- /dev/null +++ b/src/commands/deployment/deployment-api.ts @@ -0,0 +1,123 @@ +import { HttpClient } from "../../core/http/http-client"; +import { Context } from "../../core/command/cli-context"; +import { + CreateDeploymentRequest, + DeployableTransport, + DeploymentStatus, + DeploymentTransport, + GetDeploymentsRequest, + TargetTransport, +} from "./deployment.interfaces"; +import { FatalError } from "../../core/utils/logger"; + +export class DeploymentApi { + private httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async createDeployment(request: CreateDeploymentRequest): Promise { + if (!request.packageKey || request.packageKey.trim() === "") { + throw new FatalError("Package key is required to create a deployment."); + } + if (!request.packageVersion || request.packageVersion.trim() === "") { + throw new FatalError("Package version is required to create a deployment."); + } + if (!request.deployableType || request.deployableType.trim() === "") { + throw new FatalError("Deployable type is required to create a deployment."); + } + if (!request.targetId || request.targetId.trim() === "") { + throw new FatalError("Target ID is required to create a deployment."); + } + + return this.httpClient() + .post("/pacman/api/deployments", request) + .catch((e) => { + throw new FatalError(`Problem creating deployment: ${e}`); + }); + } + + public async getDeployments(filter: GetDeploymentsRequest): Promise { + const queryParams = new URLSearchParams(); + + if (filter.packageKey) { + queryParams.set("packageKey", filter.packageKey); + } + if (filter.targetId) { + queryParams.set("targetId", filter.targetId); + } + if (filter.deployableType) { + queryParams.set("deployableType", filter.deployableType); + } + if (filter.status && filter.status !== DeploymentStatus.UNKNOWN) { + queryParams.set("status", filter.status); + } + if (filter.createdBy) { + queryParams.set("createdBy", filter.createdBy); + } + + return this.httpClient() + .get(`/pacman/api/deployments/history?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting deployments: ${e}`); + }); + } + + public async getActiveDeploymentsForPackage( + packageKey: string, + targetIds?: string[], + limit = 100, + offset = 0 + ): Promise { + const queryParams = new URLSearchParams(); + queryParams.set("limit", limit.toString()); + queryParams.set("offset", offset.toString()); + + if (targetIds && targetIds.length > 0) { + queryParams.set("targetIds", targetIds.join(",")); + } + + return this.httpClient() + .get(`/pacman/api/deployments/packages/${packageKey}/active?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting active deployments for package ${packageKey}: ${e}`); + }); + } + + public async getActiveDeploymentForTarget(targetId: string): Promise { + return this.httpClient() + .get(`/pacman/api/deployments/targets/${targetId}/active`) + .catch((e) => { + throw new FatalError(`Problem getting active deployment for target ${targetId}: ${e}`); + }); + } + + public async getTargets(deployableType: string, packageKey?: string): Promise { + const queryParams = new URLSearchParams(); + queryParams.set("deployableType", deployableType); + + if (packageKey) { + queryParams.set("packageKey", packageKey); + } + + return this.httpClient() + .get(`/pacman/api/deployments/targets?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting targets: ${e}`); + }); + } + + public async getDeployables(flavor?: string): Promise { + const queryParams = new URLSearchParams(); + if (flavor) { + queryParams.set("flavor", flavor); + } + + return this.httpClient() + .get(`/pacman/api/deployments/deployables?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting deployables: ${e}`); + }); + } +} \ No newline at end of file diff --git a/src/commands/deployment/deployment.interfaces.ts b/src/commands/deployment/deployment.interfaces.ts new file mode 100644 index 00000000..3fffc3c5 --- /dev/null +++ b/src/commands/deployment/deployment.interfaces.ts @@ -0,0 +1,53 @@ +export interface DeploymentTransport { + id: string; + packageKey: string; + packageVersion: string; + deployableType: string; + target: TargetTransport; + status: DeploymentStatus; + statusMessage: string; + createdBy: string; + createdAt: string; + deployedAt: string; +} + +export interface TargetTransport { + id: string; + name: string; +} + +export interface DeployableTransport { + name: string; + type: string; + targetType: string; + flavor: string; +} + +export enum DeploymentStatus { + UNKNOWN = "unknown", + PENDING = "PENDING", + IN_PROGRESS = "IN_PROGRESS", + SUCCESS = "SUCCESS", + FAILED = "FAILED", +} + +export interface CreateDeploymentRequest { + packageKey: string; + packageVersion: string; + deployableType: string; + targetId: string; +} + +export interface GetDeploymentsRequest { + packageKey: string; + targetId: string; + deployableType: string; + status: DeploymentStatus; + createdBy: string; +} + +export function fromString(value: string): DeploymentStatus { + return (Object.values(DeploymentStatus) as string[]).includes(value) + ? (value as DeploymentStatus) + : DeploymentStatus.UNKNOWN; +} \ No newline at end of file diff --git a/src/commands/deployment/deployment.service.ts b/src/commands/deployment/deployment.service.ts new file mode 100644 index 00000000..b5b50fb3 --- /dev/null +++ b/src/commands/deployment/deployment.service.ts @@ -0,0 +1,108 @@ +import { DeploymentApi } from "./deployment-api"; +import { CreateDeploymentRequest, fromString, GetDeploymentsRequest } from "./deployment.interfaces"; +import { Context } from "../../core/command/cli-context"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; +import { v4 as uuidv4 } from "uuid"; + +export class DeploymentService { + private deploymentApi: DeploymentApi; + + constructor(context: Context) { + this.deploymentApi = new DeploymentApi(context); + } + + public async createDeployment(packageKey: string, packageVersion: string, deployableType: string, targetId: string, jsonResponse: boolean): Promise { + const request: CreateDeploymentRequest = { + packageKey, + packageVersion, + deployableType, + targetId + }; + + const deployment = await this.deploymentApi.createDeployment(request); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployment), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + logger.info(`Deployment created with ID: ${deployment.id}, Status: ${deployment.status}`); + } + } + + public async getDeployments(jsonResponse: boolean, packageKey?: string, targetId?: string, deployableType?: string, status?: string, createdBy?: string): Promise { + const filter: GetDeploymentsRequest = { + packageKey, + targetId, + deployableType, + status: fromString(status), + createdBy + }; + const deployments = await this.deploymentApi.getDeployments(filter); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployments), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployments.forEach(deployment => { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Created at: ${new Date(deployment.createdAt).toISOString()}`); + }); + } + } + + public async getActiveDeploymentForTarget(targetId: string, jsonResponse: boolean): Promise { + const deployment = await this.deploymentApi.getActiveDeploymentForTarget(targetId); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployment), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + } + } + + public async getActiveDeploymentsForPackage(packageKey: string, jsonResponse: boolean, targetIds?: string[], limit?: number, offset?:number): Promise { + const deployments = await this.deploymentApi.getActiveDeploymentsForPackage(packageKey, targetIds, limit, offset); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployments), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployments.forEach(deployment => { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${deployment.deployedAt}`); + }); + } + } + + public async getTargets(jsonResponse: boolean, deployableType: string, packageKey?: string): Promise { + const targets = await this.deploymentApi.getTargets(deployableType, packageKey); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(targets), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + targets.forEach(target => { + logger.info(`ID: ${target.id}, Name: ${target.name}`); + }); + } + } + + public async getDeployables(jsonResponse: boolean, flavor?: string): Promise { + const deployables = await this.deploymentApi.getDeployables(flavor); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployables), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployables.forEach(deployable => { + logger.info(`Name: ${deployable.name}, Type: ${deployable.type}`); + }); + } + } +} \ No newline at end of file diff --git a/src/commands/deployment/module.ts b/src/commands/deployment/module.ts new file mode 100644 index 00000000..a19cdc26 --- /dev/null +++ b/src/commands/deployment/module.ts @@ -0,0 +1,91 @@ +import { Configurator, IModule } from "../../core/command/module-handler"; +import { Context } from "../../core/command/cli-context"; +import { Command, OptionValues } from "commander"; +import { DeploymentService } from "./deployment.service"; + +class Module extends IModule { + + public register(context: Context, configurator: Configurator): void { + const deploymentCommand = configurator.command("deployment"); + + deploymentCommand.command("create") + .description("Create a new deployment") + .requiredOption("--packageKey ", "Identifier of the package to deploy") + .requiredOption("--packageVersion ", "Version of the package to deploy") + .requiredOption("--deployableType ", "The type of the deployable") + .requiredOption("--targetId ", "Identifier of the target to deploy to") + .option("--json", "Return the response as a JSON file") + .action(this.createDeployment); + + const listCommand = deploymentCommand.command("list"); + + listCommand.command("history") + .description("List deployment history") + .option("--packageKey ", "Filter deployment history by package key") + .option("--targetId ", "Filter deployment history by target ID") + .option("--deployableType ", "Filter deployment history by deployable type") + .option("--status ", "Filter deployment history by status") + .option("--createdBy ", "Filter deployment history by creator") + .option("--json", "Return the response as a JSON file") + .action(this.listDeploymentHistory); + + listCommand.command("active") + .description("Get the active deployment(s) for a given target or package") + .description("You can use the command to list the active deployment(s) for a specific target or for a specific package.") + .description("The targetIds filter is available only for getting the active deployments for a given package.") + .description("Listing active deployments for a package returns paginated results. Use the limit and offset parameters to navigate through the results.") + .option("--packageKey ", "Identifier of the package to get the active deployment for") + .option("--targetId ", "Identifier of the target to get the active deployment for") + .option("--targetIds ", "Comma-separated list of target IDs to get the active deployments for (only with packageKey)") + .option("--limit ", "Limit the number of results returned") + .option("--offset ", "Offset for pagination") + .option("--json", "Return the response as a JSON file") + .action(this.listActiveDeployments); + + listCommand.command("deployables") + .description("List all deployables") + .option("--flavor ", "Filter deployables by flavor") + .option("--json", "Return the response as a JSON file") + .action(this.listDeployables); + + listCommand.command("targets") + .description("List all targets for a given deployable type and package key") + .requiredOption("--deployableType ", "The type of the deployable") + .option("--packageKey ", "Identifier of the package to list targets for") + .option("--json", "Return the response as a JSON file") + .action(this.listTargets); + } + + private async createDeployment(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).createDeployment(options.packageKey, options.packageVersion, options.deployableType, options.targetId, options.json); + } + + private async listDeploymentHistory(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getDeployments(options.json, options.packageKey, options.targetId, options.deployableType, options.status, options.createdBy); + } + + private async listActiveDeployments(context: Context, command: Command, options: OptionValues): Promise { + if (options.packageKey && options.targetId) { + throw new Error("Please provide either --packageKey or --targetId, not both."); + } + + if (options.packageKey) { + const targetIds: string[] = options.targetIds ? options.targetIds.split(",") : []; + await new DeploymentService(context).getActiveDeploymentsForPackage(options.packageKey, options.json, targetIds, options.limit, options.offset); + } else if (options.targetId) { + await new DeploymentService(context).getActiveDeploymentForTarget(options.targetId, options.json); + } else { + throw new Error("Either --packageKey or --targetId must be provided."); + } + } + + private async listDeployables(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getDeployables(options.json, options.flavor); + } + + private async listTargets(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getTargets(options.json, options.deployableType, options.packageKey); + } +} + +export = Module; \ No newline at end of file diff --git a/tests/commands/deployment/deployment-create.spec.ts b/tests/commands/deployment/deployment-create.spec.ts new file mode 100644 index 00000000..2485a723 --- /dev/null +++ b/tests/commands/deployment/deployment-create.spec.ts @@ -0,0 +1,48 @@ +import { DeploymentStatus, DeploymentTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosPost } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment create", () => { + const deployment: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.IN_PROGRESS, + createdAt: new Date().toISOString(), + createdBy: "user-id", + deployedAt: new Date().toISOString(), + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should return created deployment", async () => { + mockAxiosPost("https://myTeam.celonis.cloud/pacman/api/deployments", deployment); + + await new DeploymentService(testContext).createDeployment(deployment.packageKey, deployment.packageVersion, deployment.deployableType, deployment.target.id, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`Deployment created with ID: ${deployment.id}, Status: ${deployment.status}`); + }); + + it("Should return created deployment as JSON", async () => { + mockAxiosPost("https://myTeam.celonis.cloud/pacman/api/deployments", deployment); + + await new DeploymentService(testContext).createDeployment(deployment.packageKey, deployment.packageVersion, deployment.deployableType, deployment.target.id, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport; + + expect(deploymentTransport).toEqual(deployment); + }); +}) \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-active.spec.ts b/tests/commands/deployment/deployment-list-active.spec.ts new file mode 100644 index 00000000..610ff30a --- /dev/null +++ b/tests/commands/deployment/deployment-list-active.spec.ts @@ -0,0 +1,99 @@ +import { DeploymentStatus, DeploymentTransport} from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list active", () => { + const deployment: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.SUCCESS, + createdAt: new Date().toISOString(), + createdBy: "user-id", + deployedAt: new Date().toISOString(), + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should list active deployment for target", async () => { + const targetId = "target-id"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/targets/${targetId}/active`, deployment); + + await new DeploymentService(testContext).getActiveDeploymentForTarget(targetId, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployment for target as JSON", async () => { + const targetId = "target-id"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/targets/${targetId}/active`, deployment); + + await new DeploymentService(testContext).getActiveDeploymentForTarget(targetId, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport; + + expect(deploymentTransport).toEqual(deployment); + }); + + it("Should list active deployments for package", async () => { + const packageKey = "package-key"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package with targetIds filter", async () => { + const packageKey = "package-key"; + const targetIds = ["target-id-1", "target-id-2"]; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0&targetIds=${targetIds.join("%2C")}`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false, targetIds); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package with pagination filters", async () => { + const packageKey = "package-key"; + const limit = 99; + const offset = 1; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=${limit}&offset=${offset}`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false, undefined, limit, offset); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package as JSON", async () => { + const packageKey = "package-key"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport[]; + expect(deploymentTransports.length).toBe(1); + + expect(deploymentTransports[0]).toEqual(deployment); + }); +}); \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-deployables.spec.ts b/tests/commands/deployment/deployment-list-deployables.spec.ts new file mode 100644 index 00000000..33648489 --- /dev/null +++ b/tests/commands/deployment/deployment-list-deployables.spec.ts @@ -0,0 +1,61 @@ +import { DeployableTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list deployables", () => { + const appPackage: DeployableTransport = { + name: "App package", + type: "app-package", + targetType: "apps-portal", + flavor: "STUDIO", + }; + + const pigData: DeployableTransport = { + name: "Objects and Events", + type: "pig-data", + targetType: "data-pool", + flavor: "OCDM", + } + + it("Should list deployables", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?", [appPackage, pigData]); + + await new DeploymentService(testContext).getDeployables(false); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(`Name: ${appPackage.name}, Type: ${appPackage.type}`); + expect(loggingTestTransport.logMessages[1].message).toContain(`Name: ${pigData.name}, Type: ${pigData.type}`); + }); + + it("Should list deployables with flavor filter", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?flavor=STUDIO", [appPackage]); + + await new DeploymentService(testContext).getDeployables(false, "STUDIO"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`Name: ${appPackage.name}, Type: ${appPackage.type}`); + }); + + it("Should list deployables as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?", [appPackage, pigData]); + + await new DeploymentService(testContext).getDeployables(true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deployableTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeployableTransport[]; + expect(deployableTransports.length).toBe(2); + + const appPackageTransport = deployableTransports.filter(transport => transport.name === appPackage.name)[0]; + const pigDataTransport = deployableTransports.filter(transport => transport.name === pigData.name)[0]; + + expect(appPackageTransport).toEqual(appPackage); + expect(pigDataTransport).toEqual(pigData); + }); +}) \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-history.spec.ts b/tests/commands/deployment/deployment-list-history.spec.ts new file mode 100644 index 00000000..96f1060f --- /dev/null +++ b/tests/commands/deployment/deployment-list-history.spec.ts @@ -0,0 +1,61 @@ +import { + DeploymentStatus, + DeploymentTransport, +} from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployments list history", () => { + const deploymentTransport: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.IN_PROGRESS, + createdAt: new Date().toISOString(), + createdBy: "user-id", + deployedAt: new Date().toISOString(), + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should list deployment history", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + + it("Should list deployment history with filter params", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?packageKey=package-key&targetId=target-id&deployableType=app-package&status=IN_PROGRESS&createdBy=user-id", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false, "package-key", "target-id", "app-package", "IN_PROGRESS", "user-id"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + + it("Should list deployment history as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport[]; + expect(deploymentTransports.length).toBe(1); + + expect(deploymentTransports[0]).toEqual(deploymentTransport); + }); +}); \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-targets.spec.ts b/tests/commands/deployment/deployment-list-targets.spec.ts new file mode 100644 index 00000000..2cae472d --- /dev/null +++ b/tests/commands/deployment/deployment-list-targets.spec.ts @@ -0,0 +1,57 @@ +import { DeployableTransport, TargetTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list targets", () => { + const firstTarget: TargetTransport = { + id: "target-1", + name: "First target", + }; + + const secondTarget: TargetTransport = { + id: "target-2", + name: "Second target", + } + + it("Should list targets", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package", [firstTarget, secondTarget]); + + await new DeploymentService(testContext).getTargets(false, "app-package"); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${firstTarget.id}, Name: ${firstTarget.name}`); + expect(loggingTestTransport.logMessages[1].message).toContain(`ID: ${secondTarget.id}, Name: ${secondTarget.name}`); + }); + + it("Should list targets with package key filter", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package&packageKey=package-key", [firstTarget]); + + await new DeploymentService(testContext).getTargets(false, "app-package", "package-key"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${firstTarget.id}, Name: ${firstTarget.name}`); + }); + + it("Should list targets as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package", [firstTarget, secondTarget]); + + await new DeploymentService(testContext).getTargets(true, "app-package"); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const targetTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeployableTransport[]; + expect(targetTransports.length).toBe(2); + + const firstTargetTransport = targetTransports.filter(transport => transport.name === firstTarget.name)[0]; + const secondTargetTransport = targetTransports.filter(transport => transport.name === secondTarget.name)[0]; + + expect(firstTargetTransport).toEqual(firstTarget); + expect(secondTargetTransport).toEqual(secondTarget); + }); +}) \ No newline at end of file From 524b9a3b98137318515c76105f55cf36b970a2af Mon Sep 17 00:00:00 2001 From: Laberion Ajvazi Date: Tue, 2 Sep 2025 18:53:43 +0200 Subject: [PATCH 2/4] TA-4197: Add beta tag, update docs, add pagination to list history --- DOCUMENTATION.md | 16 +++++---- package.json | 2 +- src/commands/deployment/deployment-api.ts | 34 +++++++++---------- src/commands/deployment/deployment.service.ts | 13 +++++-- src/commands/deployment/module.ts | 23 +++++++++---- .../deployment/deployment-list-active.spec.ts | 4 +-- .../deployment-list-history.spec.ts | 15 ++++++-- 7 files changed, 66 insertions(+), 41 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index fde67a2f..c66fce2e 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -569,13 +569,13 @@ This mapping should be saved and then used during import. Since the format of the variables.json file on import is the same JSON structure as the list variables result, you can either map the values to the variables.json file for each variable, or replace the variables.json file with the result of the listing & mapping altogether. If the mapping of variables is skipped, you should delete the variables.json file before importing. -### Deployment commands +### Deployment commands (beta) The **deployment** command group allows you to create deployments, list their history, check active deployments, and retrieve deployables and targets. #### Create Deployment Create a new deployment for a given package version in a target, based on the deployable type. ``` -content-cli deployment create --packageKey myPackage --packageVersion 1.0.0 --deployableType app-package --targetId targetId --profile my-profile-name +content-cli deployment create --packageKey myPackage --packageVersion 1.0.0 --deployableType app-package --targetId targetId ``` The command will return the created deployment, or an error if the deployment could not be created. It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. @@ -583,7 +583,7 @@ It is also possible to use the `--json` option for writing the extended response #### List Deployment History List the deployment history, optionally filtered by package, target, type, status, or creator. ``` -content-cli deployment list history --profile my-profile-name +content-cli deployment list history ``` Additional filtering options: @@ -593,6 +593,8 @@ Additional filtering options: - `--status ` – Filter deployment history by status - `--createdBy ` – Filter deployment history by creator +The response is paginated, and the page size can be controlled with the `--limit` and `--offset` options (defaults to 100 and 0 respectively). + It is also possible to use the '--json' option for writing the extended response to a file that gets created in the working directory. #### List Active Deployments @@ -600,12 +602,12 @@ Get the active deployment(s) for a given target or package. For getting the active deployment for a target the `--targetId` option should be used: ``` -content-cli deployment list active --targetId targetId --profile my-profile-name +content-cli deployment list active --targetId targetId ``` For getting the active deployment for a package the `--packageKey` option should be used: ``` -content-cli deployment list active --packageKey myPackage --profile my-profile-name +content-cli deployment list active --packageKey myPackage ``` The `--targetIds` option can be used to filter the response by multiple target IDs. The option should be used with a comma-separated list of target IDs. @@ -622,7 +624,7 @@ Note: You must provide either `--packageKey` or `--targetId`, not both. List all available deployables. ``` -content-cli deployment list deployables --flavor STUDIO --profile my-profile-name +content-cli deployment list deployables --flavor STUDIO ``` The `--flavor` option can be used to filter the deployables by flavor. The available flavors are: **STUDIO** and **OCDM**. @@ -632,7 +634,7 @@ It is also possible to use the `--json` option for writing the extended response List all targets for a given deployable type (and optionally a package key). ``` -content-cli deployment list targets --deployableType app-package --profile my-profile-name +content-cli deployment list targets --deployableType app-package ``` The `--packageKey` option can be used to get targets for a specific package. diff --git a/package.json b/package.json index 186c47b1..66e7f91d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@celonis/content-cli", - "version": "1.1.4", + "version": "1.2.0", "description": "CLI Tool to help manage content in Celonis Platform", "main": "content-cli.js", "bin": { diff --git a/src/commands/deployment/deployment-api.ts b/src/commands/deployment/deployment-api.ts index 655adcbf..822d93ec 100644 --- a/src/commands/deployment/deployment-api.ts +++ b/src/commands/deployment/deployment-api.ts @@ -18,19 +18,6 @@ export class DeploymentApi { } public async createDeployment(request: CreateDeploymentRequest): Promise { - if (!request.packageKey || request.packageKey.trim() === "") { - throw new FatalError("Package key is required to create a deployment."); - } - if (!request.packageVersion || request.packageVersion.trim() === "") { - throw new FatalError("Package version is required to create a deployment."); - } - if (!request.deployableType || request.deployableType.trim() === "") { - throw new FatalError("Deployable type is required to create a deployment."); - } - if (!request.targetId || request.targetId.trim() === "") { - throw new FatalError("Target ID is required to create a deployment."); - } - return this.httpClient() .post("/pacman/api/deployments", request) .catch((e) => { @@ -38,8 +25,9 @@ export class DeploymentApi { }); } - public async getDeployments(filter: GetDeploymentsRequest): Promise { + public async getDeployments(filter: GetDeploymentsRequest, limit?: string, offset?: string): Promise { const queryParams = new URLSearchParams(); + this.addPaginationParams(queryParams, limit, offset); if (filter.packageKey) { queryParams.set("packageKey", filter.packageKey); @@ -67,12 +55,11 @@ export class DeploymentApi { public async getActiveDeploymentsForPackage( packageKey: string, targetIds?: string[], - limit = 100, - offset = 0 + limit?: string, + offset?: string ): Promise { const queryParams = new URLSearchParams(); - queryParams.set("limit", limit.toString()); - queryParams.set("offset", offset.toString()); + this.addPaginationParams(queryParams, limit, offset); if (targetIds && targetIds.length > 0) { queryParams.set("targetIds", targetIds.join(",")); @@ -120,4 +107,15 @@ export class DeploymentApi { throw new FatalError(`Problem getting deployables: ${e}`); }); } + + private addPaginationParams(queryParams: URLSearchParams, limit?: string, offset?: string): void { + const defaultLimit = 100; + const defaultOffset = 0; + + const parsedLimit = limit !== undefined && !isNaN(Number(limit)) ? Number(limit) : defaultLimit; + queryParams.set("limit", parsedLimit.toString()); + + const parsedOffset = offset !== undefined && !isNaN(Number(offset)) ? Number(offset) : defaultOffset; + queryParams.set("offset", parsedOffset.toString()); + } } \ No newline at end of file diff --git a/src/commands/deployment/deployment.service.ts b/src/commands/deployment/deployment.service.ts index b5b50fb3..02c25b69 100644 --- a/src/commands/deployment/deployment.service.ts +++ b/src/commands/deployment/deployment.service.ts @@ -31,7 +31,14 @@ export class DeploymentService { } } - public async getDeployments(jsonResponse: boolean, packageKey?: string, targetId?: string, deployableType?: string, status?: string, createdBy?: string): Promise { + public async getDeployments(jsonResponse: boolean + ,packageKey?: string + ,targetId?: string + ,deployableType?: string + ,status?: string + ,createdBy?: string + ,limit?: string + ,offset?: string): Promise { const filter: GetDeploymentsRequest = { packageKey, targetId, @@ -39,7 +46,7 @@ export class DeploymentService { status: fromString(status), createdBy }; - const deployments = await this.deploymentApi.getDeployments(filter); + const deployments = await this.deploymentApi.getDeployments(filter, limit, offset); if (jsonResponse) { const filename = uuidv4() + ".json"; @@ -64,7 +71,7 @@ export class DeploymentService { } } - public async getActiveDeploymentsForPackage(packageKey: string, jsonResponse: boolean, targetIds?: string[], limit?: number, offset?:number): Promise { + public async getActiveDeploymentsForPackage(packageKey: string, jsonResponse: boolean, targetIds?: string[], limit?: string, offset?: string): Promise { const deployments = await this.deploymentApi.getActiveDeploymentsForPackage(packageKey, targetIds, limit, offset); if (jsonResponse) { diff --git a/src/commands/deployment/module.ts b/src/commands/deployment/module.ts index a19cdc26..fe4dcda8 100644 --- a/src/commands/deployment/module.ts +++ b/src/commands/deployment/module.ts @@ -6,9 +6,10 @@ import { DeploymentService } from "./deployment.service"; class Module extends IModule { public register(context: Context, configurator: Configurator): void { - const deploymentCommand = configurator.command("deployment"); + const deploymentCommand = configurator.command("deployment").beta(); deploymentCommand.command("create") + .beta() .description("Create a new deployment") .requiredOption("--packageKey ", "Identifier of the package to deploy") .requiredOption("--packageVersion ", "Version of the package to deploy") @@ -17,23 +18,27 @@ class Module extends IModule { .option("--json", "Return the response as a JSON file") .action(this.createDeployment); - const listCommand = deploymentCommand.command("list"); + const listCommand = deploymentCommand.command("list").beta(); listCommand.command("history") + .beta() .description("List deployment history") .option("--packageKey ", "Filter deployment history by package key") .option("--targetId ", "Filter deployment history by target ID") .option("--deployableType ", "Filter deployment history by deployable type") .option("--status ", "Filter deployment history by status") .option("--createdBy ", "Filter deployment history by creator") + .option("--limit ", "Limit the number of results returned") + .option("--offset ", "Offset for pagination") .option("--json", "Return the response as a JSON file") .action(this.listDeploymentHistory); listCommand.command("active") - .description("Get the active deployment(s) for a given target or package") - .description("You can use the command to list the active deployment(s) for a specific target or for a specific package.") - .description("The targetIds filter is available only for getting the active deployments for a given package.") - .description("Listing active deployments for a package returns paginated results. Use the limit and offset parameters to navigate through the results.") + .beta() + .description("Get the active deployment(s) for a given target or package.\n"+ + "You can use the command to list the active deployment(s) for a specific target or for a specific package.\n" + + "The targetIds filter is available only for getting the active deployments for a given package. \n" + + "Listing active deployments for a package returns paginated results. Use the limit and offset parameters to navigate through the results.") .option("--packageKey ", "Identifier of the package to get the active deployment for") .option("--targetId ", "Identifier of the target to get the active deployment for") .option("--targetIds ", "Comma-separated list of target IDs to get the active deployments for (only with packageKey)") @@ -43,12 +48,14 @@ class Module extends IModule { .action(this.listActiveDeployments); listCommand.command("deployables") + .beta() .description("List all deployables") .option("--flavor ", "Filter deployables by flavor") .option("--json", "Return the response as a JSON file") .action(this.listDeployables); listCommand.command("targets") + .beta() .description("List all targets for a given deployable type and package key") .requiredOption("--deployableType ", "The type of the deployable") .option("--packageKey ", "Identifier of the package to list targets for") @@ -61,7 +68,9 @@ class Module extends IModule { } private async listDeploymentHistory(context: Context, command: Command, options: OptionValues): Promise { - await new DeploymentService(context).getDeployments(options.json, options.packageKey, options.targetId, options.deployableType, options.status, options.createdBy); + await new DeploymentService(context).getDeployments( + options.json, options.packageKey, options.targetId, options.deployableType + , options.status, options.createdBy, options.limit, options.offset); } private async listActiveDeployments(context: Context, command: Command, options: OptionValues): Promise { diff --git a/tests/commands/deployment/deployment-list-active.spec.ts b/tests/commands/deployment/deployment-list-active.spec.ts index 610ff30a..a7313002 100644 --- a/tests/commands/deployment/deployment-list-active.spec.ts +++ b/tests/commands/deployment/deployment-list-active.spec.ts @@ -71,8 +71,8 @@ describe("Deployment list active", () => { it("Should list active deployments for package with pagination filters", async () => { const packageKey = "package-key"; - const limit = 99; - const offset = 1; + const limit = "99"; + const offset = "1"; mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=${limit}&offset=${offset}`, [deployment]); await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false, undefined, limit, offset); diff --git a/tests/commands/deployment/deployment-list-history.spec.ts b/tests/commands/deployment/deployment-list-history.spec.ts index 96f1060f..bd7a3b38 100644 --- a/tests/commands/deployment/deployment-list-history.spec.ts +++ b/tests/commands/deployment/deployment-list-history.spec.ts @@ -27,7 +27,7 @@ describe("Deployments list history", () => { }; it("Should list deployment history", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?", [deploymentTransport]); + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0", [deploymentTransport]); await new DeploymentService(testContext).getDeployments(false); @@ -36,7 +36,7 @@ describe("Deployments list history", () => { }); it("Should list deployment history with filter params", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?packageKey=package-key&targetId=target-id&deployableType=app-package&status=IN_PROGRESS&createdBy=user-id", [deploymentTransport]); + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0&packageKey=package-key&targetId=target-id&deployableType=app-package&status=IN_PROGRESS&createdBy=user-id", [deploymentTransport]); await new DeploymentService(testContext).getDeployments(false, "package-key", "target-id", "app-package", "IN_PROGRESS", "user-id"); @@ -44,8 +44,17 @@ describe("Deployments list history", () => { expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); }); + it("Should list deployment history with pagination params", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=3&offset=3", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false, undefined, undefined, undefined, undefined, undefined, "3", "3"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + it("Should list deployment history as JSON", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?", [deploymentTransport]); + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0", [deploymentTransport]); await new DeploymentService(testContext).getDeployments(true); From cb79e5628a5743bc84397c6d5062c82f2170f7a0 Mon Sep 17 00:00:00 2001 From: Laberion Ajvazi Date: Thu, 4 Sep 2025 18:04:52 +0200 Subject: [PATCH 3/4] TA-4197: Show deployment date as readable string --- src/commands/deployment/deployment.service.ts | 2 +- tests/commands/deployment/deployment-list-active.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/deployment/deployment.service.ts b/src/commands/deployment/deployment.service.ts index 02c25b69..a50ba4f2 100644 --- a/src/commands/deployment/deployment.service.ts +++ b/src/commands/deployment/deployment.service.ts @@ -80,7 +80,7 @@ export class DeploymentService { logger.info(FileService.fileDownloadedMessage + filename); } else { deployments.forEach(deployment => { - logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${deployment.deployedAt}`); + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); }); } } diff --git a/tests/commands/deployment/deployment-list-active.spec.ts b/tests/commands/deployment/deployment-list-active.spec.ts index a7313002..59ca0541 100644 --- a/tests/commands/deployment/deployment-list-active.spec.ts +++ b/tests/commands/deployment/deployment-list-active.spec.ts @@ -13,9 +13,9 @@ describe("Deployment list active", () => { packageVersion: "1.0.0", deployableType: "app-package", status: DeploymentStatus.SUCCESS, - createdAt: new Date().toISOString(), + createdAt: new Date() + "", createdBy: "user-id", - deployedAt: new Date().toISOString(), + deployedAt: new Date().getDate() + "", statusMessage: "Deployment in progress", target: { id: "target-id", From 8046588503318c1054c6efea8e117a2b2d7feadc Mon Sep 17 00:00:00 2001 From: Laberion Ajvazi Date: Thu, 4 Sep 2025 18:06:42 +0200 Subject: [PATCH 4/4] TA-4197: Show deployment date as readable string --- tests/commands/deployment/deployment-list-active.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/deployment/deployment-list-active.spec.ts b/tests/commands/deployment/deployment-list-active.spec.ts index 59ca0541..838bf2fb 100644 --- a/tests/commands/deployment/deployment-list-active.spec.ts +++ b/tests/commands/deployment/deployment-list-active.spec.ts @@ -13,7 +13,7 @@ describe("Deployment list active", () => { packageVersion: "1.0.0", deployableType: "app-package", status: DeploymentStatus.SUCCESS, - createdAt: new Date() + "", + createdAt: new Date().getDate() + "", createdBy: "user-id", deployedAt: new Date().getDate() + "", statusMessage: "Deployment in progress",