Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 21 additions & 1 deletion packages/contentstack-bulk-operations/src/base-am-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Command } from '@contentstack/cli-command';
import { handleAndLogError } from '@contentstack/cli-utilities';
import {
handleAndLogError,
configHandler,
loadChalk,
CLIProgressManager,
clearProgressModuleSetting,
} from '@contentstack/cli-utilities';

import { fillMissingCsAssetsFlags } from './utils';
import type { CsAssetsFlags } from './interfaces';
Expand All @@ -16,6 +22,12 @@ export abstract class BaseCsAssetsCommand extends Command {

protected async init(): Promise<void> {
await super.init();

// Suppress timestamped console logs + load chalk (same UX as the other bulk commands).
// Must run before the first log call. CS Assets keeps its own printCsAssetsSummary output.
configHandler.set('log.progressSupportedModule', 'bulk-operations');
await loadChalk();

const { flags } = await this.parse(this.constructor as typeof BaseCsAssetsCommand);
this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' };
this.parsedFlags = (await fillMissingCsAssetsFlags(flags)) as CsAssetsFlags;
Expand All @@ -25,5 +37,13 @@ export abstract class BaseCsAssetsCommand extends Command {
handleAndLogError(error);
}

protected async finally(_error?: Error): Promise<void> {
// Clear progress state so the module flag never leaks into a later command in the process.
// (CS Assets doesn't initialize a global summary, so printGlobalSummary is a no-op here.)
CLIProgressManager.printGlobalSummary();
CLIProgressManager.clearGlobalSummary();
clearProgressModuleSetting();
}

abstract run(): Promise<void>;
}
118 changes: 110 additions & 8 deletions packages/contentstack-bulk-operations/src/base-bulk-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import chalk from 'chalk';
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, getLogPath, handleAndLogError, FlagInput } from '@contentstack/cli-utilities';
import {
flags,
log,
createLogContext,
getLogPath,
handleAndLogError,
FlagInput,
getChalk,
loadChalk,
configHandler,
CLIProgressManager,
clearProgressModuleSetting,
} from '@contentstack/cli-utilities';

import config from './config';
import messages, { $t } from './messages';
Expand Down Expand Up @@ -33,6 +44,7 @@ import {
generateBulkPublishStatusUrl,
validateBranch,
validateEnvironments,
aggregateBatchResults,
} from './utils';
import {
OperationType,
Expand Down Expand Up @@ -141,6 +153,10 @@ export abstract class BaseBulkCommand extends Command {
protected async init(): Promise<void> {
await super.init();

// Load chalk (ESM) up-front. The progress-module flag is set in buildConfig()
// (config layer, mirrors export-config-handler) before the first log call.
await loadChalk();

let { flags } = await this.parse(this.constructor as typeof BaseBulkCommand);

this.parsedFlags = flags;
Expand Down Expand Up @@ -220,6 +236,9 @@ export abstract class BaseBulkCommand extends Command {
await this.initializeComponents();

await this.handleRevertOrRetry(mergedFlags);
// This early exit bypasses finally(); print the run summary and clear the progress-module
// flag here so it doesn't leak into the persisted config / later commands.
this.finalizeProgressSummary();
process.exit(0);
}

Expand Down Expand Up @@ -268,6 +287,13 @@ export abstract class BaseBulkCommand extends Command {
* Build operation configuration
*/
protected async buildConfiguration(flags: any): Promise<void> {
// Enable the progress-bar UI + suppress the timestamped console logs for the whole command.
// Set here (command lifecycle, runs before the first log call) rather than in the pure
// buildConfig() util so it isn't triggered by direct/unit-test callers of buildConfig.
// Cleared on every exit path: finally() on normal runs, and finalizeProgressSummary() before
// the validation exit(1) below and the revert/retry exit(0).
configHandler.set('log.progressSupportedModule', 'bulk-operations');

this.bulkOperationConfig = buildConfig(flags);

// buildConfig splits comma-separated oclif `multiple` values; mirror onto flags so
Expand All @@ -280,6 +306,9 @@ export abstract class BaseBulkCommand extends Command {

const validation = validateFlags(this.bulkOperationConfig);
if (!validation.valid) {
// The progress-module flag was set above; this early exit bypasses finally(), so clear it
// here to avoid leaking the setting into the persisted config / later commands.
this.finalizeProgressSummary();
process.exit(1);
}

Expand Down Expand Up @@ -365,20 +394,91 @@ export abstract class BaseBulkCommand extends Command {
this.logger.debug($t(messages.EXECUTING_OPERATION, { count: items.length }), this.loggerContext);
const startTime = Date.now();

// Initialize the run-level summary + header once (progress-manager UX, same as import).
this.beginOperationSummary(items.length);

let result: BulkOperationResult;
try {
logOperationInfo(items, this.logger);

const publishMode = this.bulkOperationConfig.publishMode || PublishMode.BULK;
this.logger.debug(`Using ${publishMode.toUpperCase()} mode for operation`, this.loggerContext);

if (publishMode === PublishMode.SINGLE) {
return await this.executeSingleMode(items, startTime);
}

return await this.executeBulkMode(items, startTime);
result =
publishMode === PublishMode.SINGLE
? await this.executeSingleMode(items, startTime)
: await this.executeBulkMode(items, startTime);
} catch (error: any) {
return handleOperationError(error, items, startTime);
result = handleOperationError(error, items, startTime);
}

this.recordModuleSummary(result, items.length);
return result;
}

/**
* Initialize the run-level summary + header once. The label includes the branch, which
* defaults to 'main' from the --branch flag, so it normally reads e.g. "BULK PUBLISH-main" —
* the same "<OP>-<branch>" title shape export/import produce (SummaryManager uses the passed
* operationName verbatim). The branch is dropped from the label only in the edge case where
* it is unset. Shared with bulk-taxonomies via inheritance.
*/
protected beginOperationSummary(itemCount: number): void {
const operationLabel = (this.bulkOperationConfig?.operation || 'operation').toString().toUpperCase();
const branchName = this.bulkOperationConfig?.branch || '';
CLIProgressManager.initializeGlobalSummary(
branchName ? `BULK ${operationLabel}-${branchName}` : `BULK ${operationLabel}`,
branchName,
$t(messages.EXECUTING_OPERATION, { count: itemCount })
);
}

/**
* Record a per-module row in the summary so the final summary shows Module Details for this
* command (entry/asset/taxonomy).
*
* SINGLE mode processes items individually and returns real success/failed counts, so those
* are used directly. BULK mode submits async jobs — buildBulkModeResult reports success/failed
* as 0 because the publish runs server-side — so we count submission-level failures from
* batchResults (recorded as status:'failed') and treat the remaining submitted items as
* success. The printed status URL remains the source of truth for the real publish outcome.
*/
protected recordModuleSummary(result: BulkOperationResult, submittedCount: number): void {
const showConsoleLogs = Boolean(configHandler.get('log')?.showConsoleLogs);
const publishMode = this.bulkOperationConfig?.publishMode || PublishMode.BULK;
const total = result?.total || submittedCount || 0;

let success: number;
let failed: number;
if (publishMode === PublishMode.SINGLE) {
failed = result?.failed || 0;
success = typeof result?.success === 'number' ? result.success : Math.max(total - failed, 0);
} else {
// BULK: derive failures from batch submissions (result.failed is always 0 here).
failed = aggregateBatchResults(this.batchResults).totalFailed;
success = Math.max(total - failed, 0);
}

// Clamp so counts never over/under-report relative to total.
failed = Math.min(Math.max(failed, 0), total);
success = Math.min(Math.max(success, 0), total - failed);

const progress = CLIProgressManager.createSimple(this.resourceType, total, showConsoleLogs);
Comment thread
harshitha-cstk marked this conversation as resolved.
for (let i = 0; i < success; i++) progress.tick(true);
for (let i = 0; i < failed; i++) progress.tick(false);
progress.complete(failed === 0);
}

/**
* Print the run-level summary once and clear progress state. Idempotent: subclasses call
* finally() explicitly AND oclif calls it again, so clearing the summary after printing makes
* the second invocation a no-op. Also clears the progress-module flag so it never leaks into
* a later command in the same process (mirrors export/import/clone).
*/
protected finalizeProgressSummary(): void {
CLIProgressManager.printGlobalSummary();
CLIProgressManager.clearGlobalSummary();
clearProgressModuleSetting();
}

/**
Expand Down Expand Up @@ -444,6 +544,7 @@ export abstract class BaseBulkCommand extends Command {
* Called at the end of run() method in subclasses
*/
protected printOperationSummary(result: BulkOperationResult): void {
const chalk = getChalk();
const publishMode = this.bulkOperationConfig.publishMode || PublishMode.BULK;

console.log('');
Expand Down Expand Up @@ -555,6 +656,7 @@ export abstract class BaseBulkCommand extends Command {
abstract run(): Promise<void>;

protected async finally(_error: Error | undefined): Promise<void> {
this.finalizeProgressSummary();
await this.cleanup();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, handleAndLogError } from '@contentstack/cli-utilities';
import { flags, log, createLogContext, handleAndLogError, loadChalk } from '@contentstack/cli-utilities';

import messages, { $t } from '../../../messages';
import { BaseBulkCommand } from '../../../base-bulk-command';
Expand Down Expand Up @@ -58,6 +58,10 @@ export default class BulkTaxonomies extends BaseBulkCommand {
// Call oclif Command init without running BaseBulkCommand.init (taxonomy uses its own prompts).
await (Command.prototype as unknown as { init(this: Command): Promise<void> }).init.call(this);

// Load chalk (ESM) up-front. The progress-module flag is set in buildConfig() (invoked by
// buildConfiguration below), before the first log call.
await loadChalk();

let { flags: parsed } = await this.parse(BulkTaxonomies);

if (parsed.revert || parsed['retry-failed']) {
Expand Down Expand Up @@ -161,6 +165,9 @@ export default class BulkTaxonomies extends BaseBulkCommand {
this.logger.debug($t(messages.EXECUTING_OPERATION, { count: items.length }), this.loggerContext);
const startTime = Date.now();

// Initialize the run-level summary + header once (inherited from BaseBulkCommand).
this.beginOperationSummary(items.length);

const operation = this.bulkOperationConfig.operation;
if (operation !== OperationType.PUBLISH && operation !== OperationType.UNPUBLISH) {
throw new Error($t(messages.UNSUPPORTED_OPERATION, { operation: operation ?? 'unknown' }));
Expand Down Expand Up @@ -190,12 +197,17 @@ export default class BulkTaxonomies extends BaseBulkCommand {
this.logger.info(String(response.notice));
}

return {
const result: BulkOperationResult = {
success: 0,
failed: 0,
total: items.length,
duration,
jobIds: jobId ? [jobId] : [],
};

// Record the taxonomy module row in the summary (inherited from BaseBulkCommand).
this.recordModuleSummary(result, items.length);

return result;
}
}
Loading