Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
266a4d2
Setup `git-profile` interface and profile loading
ZgjimHaziri Jul 14, 2025
5e4d637
Add options to export/import commands
ZgjimHaziri Jul 14, 2025
f9bc5d0
Implement export/import integration
ZgjimHaziri Jul 14, 2025
da018e5
Fix git issues with non-existing branch and merge conflicts
ZgjimHaziri Jul 14, 2025
77aad45
Merge branch 'master' into git-integration-poc
ZgjimHaziri Jul 14, 2025
2d6bf6c
TA-3911: Initial refactoring from review
ZgjimHaziri Jul 18, 2025
8c3a3d0
TA-3911: Use specific version for `simple-git`
ZgjimHaziri Jul 22, 2025
67cb469
TA-3911: Rename git to VCS
ZgjimHaziri Jul 22, 2025
1ac5db0
TA-3911: Latest changes after testing
ZgjimHaziri Jul 31, 2025
175be3d
TA-4066: Merge branch `master` into TA-4066-implement-git-integration
ZgjimHaziri Aug 5, 2025
946b538
TA-4066: Rename vcs back to git
ZgjimHaziri Aug 7, 2025
f5e01eb
TA-4066: Rename folders
ZgjimHaziri Aug 7, 2025
2fc5d28
TA-4066: Mark commands and options as beta
ZgjimHaziri Aug 7, 2025
2682aa9
TA-4066: Bump minor version
ZgjimHaziri Aug 7, 2025
e982c0f
TA-4066: Fix issues with pushing to branch
ZgjimHaziri Aug 7, 2025
73895fd
TA-4066: Fix test syntax
ZgjimHaziri Aug 7, 2025
1082dbe
TA-4066: Make constants readonly
ZgjimHaziri Aug 8, 2025
babb14f
TA-4066: Simplify profile listing by making it synchronous
ZgjimHaziri Aug 8, 2025
ff84e0a
TA-4066: Remove default value definition in non-last parameter
ZgjimHaziri Aug 8, 2025
4caad6c
Merge branch 'master' into TA-4066-implement-git-integration
ZgjimHaziri Aug 11, 2025
25bb869
TA-4066: Fix test setup based on changes
ZgjimHaziri Aug 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@celonis/content-cli",
"version": "1.0.7",
"version": "1.1.0",
"description": "CLI Tool to help manage content in Celonis Platform",
"main": "content-cli.js",
"bin": {
Expand All @@ -26,6 +26,7 @@
"openid-client": "5.6.1",
"hpagent": "1.2.0",
"semver": "7.6.3",
"simple-git": "3.28.0",
"valid-url": "1.0.9",
"winston": "3.17.0",
"yaml": "2.7.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@ import { parse, stringify } from "../../core/utils/json";
import { PackageApi } from "../studio/api/package-api";
import { BatchImportExportApi } from "./api/batch-import-export-api";
import { StudioService } from "./studio.service";
import { GitService } from "../../core/git-profile/git/git.service";
import * as fs from "node:fs";

export class BatchImportExportService {

private batchImportExportApi: BatchImportExportApi;

private studioPackageApi: PackageApi;
private studioService: StudioService;
private gitService: GitService;

constructor(context: Context) {
this.batchImportExportApi = new BatchImportExportApi(context);

this.studioPackageApi = new PackageApi(context);
this.studioService = new StudioService(context);
this.gitService = new GitService(context);
}

public async listActivePackages(flavors: string[]): Promise<void> {
Expand All @@ -51,7 +55,7 @@ export class BatchImportExportService {
this.exportListOfPackages(packagesToExport);
}

public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean = false): Promise<void> {
public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean = false, gitBranch: string): Promise<void> {
let exportedPackagesData: Buffer;
if (packageKeys) {
exportedPackagesData = await this.batchImportExportApi.exportPackages(packageKeys, withDependencies);
Expand Down Expand Up @@ -89,10 +93,16 @@ export class BatchImportExportService {
}
});

const fileDownloadedMessage = "File downloaded successfully. New filename: ";
const filename = `export_${uuidv4()}.zip`;
exportedPackagesZip.writeZip(filename);
logger.info(fileDownloadedMessage + filename);
if (gitBranch) {
const extractedDirectory = fileService.extractExportedZipWithNestedZips(exportedPackagesZip);
await this.gitService.pushToBranch(extractedDirectory, gitBranch);
logger.info("Successfully exported packages to branch: " + gitBranch);
} else {
const fileDownloadedMessage = "File downloaded successfully. New filename: ";
const filename = `export_${uuidv4()}.zip`;
exportedPackagesZip.writeZip(filename);
logger.info(fileDownloadedMessage + filename);
}
}

public async batchExportPackagesMetadata(packageKeys: string[], jsonResponse: boolean): Promise<void> {
Expand All @@ -107,8 +117,16 @@ export class BatchImportExportService {
}
}

public async batchImportPackages(file: string, overwrite: boolean): Promise<void> {
let configs = new AdmZip(file);
public async batchImportPackages(file: string, overwrite: boolean, gitBranch: string): Promise<void> {
let fileToBeImported: string;
if (gitBranch) {
fileToBeImported = await this.gitService.pullFromBranch(gitBranch);
fileToBeImported = fileService.zipDirectoryInBatchExportFormat(fileToBeImported);
} else {
fileToBeImported = file;
}

let configs = new AdmZip(fileToBeImported);
const studioManifests = this.parseEntryData(configs, BatchExportImportConstants.STUDIO_FILE_NAME) as StudioPackageManifest[];
const variablesManifests: VariableManifestTransport[] = this.parseEntryData(configs, BatchExportImportConstants.VARIABLES_FILE_NAME) as VariableManifestTransport[];

Expand All @@ -119,6 +137,10 @@ export class BatchImportExportService {
const postPackageImportData = await this.batchImportExportApi.importPackages(formData, overwrite);
await this.studioService.processImportedPackages(configs, existingStudioPackages, studioManifests);

if (gitBranch) {
fs.rmSync(fileToBeImported);
}

const reportFileName = "config_import_report_" + uuidv4() + ".json";
fileService.writeToFileWithGivenName(JSON.stringify(postPackageImportData), reportFileName);
logger.info("Config import report file: " + reportFileName);
Expand Down
14 changes: 10 additions & 4 deletions src/commands/configuration-management/config-command.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@ export class ConfigCommandService {
}
}

public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean = false): Promise<void> {
return this.batchImportExportService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies);
public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean = false, gitBranch: string): Promise<void> {
Comment thread
ksalihu marked this conversation as resolved.
Outdated
return this.batchImportExportService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies, gitBranch);
}

public batchExportPackagesMetadata(packageKeys: string[], jsonResponse: boolean): Promise<void> {
return this.batchImportExportService.batchExportPackagesMetadata(packageKeys, jsonResponse);
}

public batchImportPackages(file: string, overwrite: boolean): Promise<void> {
return this.batchImportExportService.batchImportPackages(file, overwrite);
public batchImportPackages(file: string, overwrite: boolean, gitBranch: string): Promise<void> {
if (file && gitBranch) {
throw new Error("You cannot use both file and gitBranch options at the same time. Only one import source can be defined.");
}
if (!file && !gitBranch) {
throw new Error("You must provide either a file or a gitBranch option to import packages.");
}
return this.batchImportExportService.batchImportPackages(file, overwrite, gitBranch);
}

public diffPackages(file: string, hasChanges: boolean, jsonResponse: boolean): Promise<void> {
Expand Down
10 changes: 7 additions & 3 deletions src/commands/configuration-management/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class Module extends IModule {
.option("--packageKeys <packageKeys...>", "Keys of packages to export. Exports the latest deployed version only")
.option("--keysByVersion <keysByVersion...>", "Keys of packages to export by version")
.option("--withDependencies", "Include variables and dependencies", "")
.betaOption("--gitProfile <gitProfile>", "Git profile which you want to use for the Git operations")
.betaOption("--gitBranch <gitBranch>", "Git branch in which you want to push the exported file")
.action(this.batchExportPackages);

const metadataCommand = configCommand.command("metadata")
Expand All @@ -42,7 +44,9 @@ class Module extends IModule {
configCommand.command("import")
.description("Command to import package configs")
.option("--overwrite", "Flag to allow overwriting of packages")
.requiredOption("-f, --file <file>", "Exported packages file (relative path)")
.betaOption("--gitProfile <gitProfile>", "Git profile which you want to use for the Git operations")
.betaOption("--gitBranch <gitBranch>", "Git branch from which you want to pull the exported file and import")
.option("-f, --file <file>", "Exported packages file (relative path)")
.action(this.batchImportPackages);

configCommand.command("diff")
Expand Down Expand Up @@ -79,15 +83,15 @@ class Module extends IModule {
if ((options.packageKeys && options.keysByVersion) || (!options.packageKeys && !options.keysByVersion)) {
throw new Error("Please provide either --packageKeys or --keysByVersion, but not both.");
}
await new ConfigCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies);
await new ConfigCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch);
}

private async batchExportPackagesMetadata(context: Context, command: Command, options: OptionValues): Promise<void> {
await new ConfigCommandService(context).batchExportPackagesMetadata(options.packageKeys, options.json);
}

private async batchImportPackages(context: Context, command: Command, options: OptionValues): Promise<void> {
await new ConfigCommandService(context).batchImportPackages(options.file, options.overwrite);
await new ConfigCommandService(context).batchImportPackages(options.file, options.overwrite, options.gitBranch);
}

private async diffPackages(context: Context, command: Command, options: OptionValues): Promise<void> {
Expand Down
60 changes: 60 additions & 0 deletions src/commands/git-profile/git-profile-command.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { QuestionService } from "../../core/utils/question.service";
import { FatalError, logger } from "../../core/utils/logger";
import { GitProfileService } from "../../core/git-profile/git-profile.service";
import { AuthenticationType, GitProfile } from "../../core/git-profile/git-profile.interface";

export class GitProfileCommandService {
private gitProfileService = new GitProfileService();

public async createProfile(setAsDefault: boolean): Promise<void> {
const profile: GitProfile = {} as GitProfile;
const questions = new QuestionService();
try {
profile.name = await questions.ask("Name of the Git profile to create: ");
profile.username = await questions.ask("Your Git username: ");
profile.repository = await questions.ask("Your repository (format: repoOwner/repoName): ");
const type = await questions.ask("Authentication type: PAT (1), SSH token (2): " );
switch (type) {
case "1":
profile.authenticationType = AuthenticationType.PAT;
profile.token = await questions.ask("Your Personal Access Token (PAT): " );
break;
case "2":
profile.authenticationType = AuthenticationType.SSH;
break;
default:
logger.error(new FatalError("Invalid type"));
break;
}
// possibly check if the user has sent a valid token that has access in the repository

this.gitProfileService.storeProfile(profile);
if (setAsDefault) {
await this.makeDefaultProfile(profile.name);
}
} finally {
await questions.close();
}
logger.info("Git Profile created successfully!");
}

public async listProfiles(): Promise<void> {
this.gitProfileService.readAllProfiles().then((profiles: string[]) => {
const defaultProfile = this.gitProfileService.getDefaultProfile();
if (profiles) {
profiles.forEach(profile => {
if (defaultProfile && defaultProfile === profile) {
logger.info(profile + " (default)");
} else {
logger.info(profile);
}
});
}
});
}

public async makeDefaultProfile(profile: string): Promise<void> {
await this.gitProfileService.makeDefaultProfile(profile);
logger.info("Default Git profile: " + profile);
}
}
53 changes: 53 additions & 0 deletions src/commands/git-profile/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Commands to create and list access profiles.
*/

import { Command, OptionValues } from "commander";
import { Configurator, IModule } from "../../core/command/module-handler";
import { Context } from "../../core/command/cli-context";
import { GitProfileCommandService } from "./git-profile-command.service";
import { logger } from "../../core/utils/logger";

class Module extends IModule {

public register(context: Context, configurator: Configurator): void {
const gitCommand = configurator.command("git")
.beta()
.description("Commands related to Git settings");

const gitProfileCommand = gitCommand.command("profile")
.beta()
.description("Manage Git profiles required to use git-related operations.");

gitProfileCommand.command("list")
.description("Command to list all stored Git profiles")
.beta()
.action(this.listProfiles);

gitProfileCommand.command("create")
.description("Command to create a new Git profile")
.option("--setAsDefault", "Set this Git profile as default")
.beta()
.action(this.createProfile);

gitProfileCommand.command("default <profile>")
.description("Command to set a Git profile as default")
.beta()
.action(this.defaultProfile);
}

private async defaultProfile(context: Context, command: Command): Promise<void> {
const profile = command.args[0];
await new GitProfileCommandService().makeDefaultProfile(profile);
}

private async createProfile(context: Context, command: Command, options: OptionValues): Promise<void> {
await new GitProfileCommandService().createProfile(options.setAsDefault);
}

private async listProfiles(context: Context, command: Command): Promise<void> {
await new GitProfileCommandService().listProfiles();
}
}

export = Module;
4 changes: 2 additions & 2 deletions src/content-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ async function run(): Promise<void> {
await context.init();

const moduleHandler = new ModuleHandler(program, context);

configureRootCommands(moduleHandler.configurator);

moduleHandler.discoverAndRegisterModules(__dirname, program.opts().dev);

try {
program.parse(process.argv);
} catch (error) {
Expand Down
25 changes: 25 additions & 0 deletions src/core/command/cli-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { HttpClient } from "../http/http-client";
import {ProfileService} from "../profile/profile.service";
import {FatalError, logger} from "../utils/logger";
import {Profile} from "../profile/profile.interface";
import { GitProfileService } from "../git-profile/git-profile.service";
import { GitProfile } from "../git-profile/git-profile.interface";

/**
* The execution context object is passed to the modules to access
Expand All @@ -13,14 +15,18 @@ export class Context {

public _httpClient: HttpClient;
public profile: Profile;
public gitProfile: GitProfile;

private log = logger;
private profileName: string | undefined;
private gitProfileName: string | undefined;

private profileService = new ProfileService();
private gitProfileService = new GitProfileService();

constructor(options: any) {
this.profileName = options.profile;
this.gitProfileName = options.gitProfile;
}

public get httpClient(): HttpClient {
Expand All @@ -32,6 +38,7 @@ export class Context {

public async init(): Promise<void> {
await this.loadProfile(this.profileName);
await this.loadGitProfile(this.gitProfileName);

if (this.profile) {
// only if a profile is available, it makes sense to provide an initialized
Expand Down Expand Up @@ -59,4 +66,22 @@ export class Context {
}
}

private async loadGitProfile(gitProfileName: string | undefined): Promise<void> {
if (!gitProfileName) {
this.log.debug("Git Profile name not specified, using default profile");
gitProfileName = this.gitProfileService.getDefaultProfile();
if (!gitProfileName) {
this.log.debug("A default Git profile is not configured.");
}
}
try {
this.gitProfile = await this.gitProfileService.findProfile(gitProfileName);
this.gitProfileName = gitProfileName;
this.log.debug(`Using Git profile ${gitProfileName}`);
} catch (err) {
this.gitProfile = undefined;
this.gitProfileName = undefined;
}
}

}
15 changes: 15 additions & 0 deletions src/core/git-profile/git-profile.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface GitProfile {
name: string;
username?: string;
repository: string; // in the format "owner/repository"
token?: string;
authenticationType: AuthenticationType;
}

export type AuthenticationType = "SSH" | "Personal Access Token";

// tslint:disable-next-line:variable-name
export const AuthenticationType: { [key: string]: AuthenticationType } = {
SSH: "SSH",
PAT: "Personal Access Token",
};
Loading