Skip to content
Open
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
14 changes: 2 additions & 12 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: 069d87fc69d059bd53fa46d98916e831641ea4889cebcc9b9b30884ac67dab30
- filename: packages/contentstack-branches/README.md
checksum: ad32bd365db7f085cc2ea133d69748954606131ec6157a272a3471aea60011c2
- filename: packages/contentstack-branches/src/branch/diff-handler.ts
checksum: 3cd4d26a2142cab7cbf2094c9251e028467d17d6a1ed6daf22f21975133805f1
- filename: packages/contentstack-branches/src/commands/cm/branches/merge-status.ts
checksum: 6e5b959ddcc5ff68e03c066ea185fcf6c6e57b1819069730340af35aad8a93a8
- filename: packages/contentstack-branches/src/utils/create-branch.ts
checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f
- filename: packages/contentstack-branches/src/branch/merge-handler.ts
checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133
- filename: packages/contentstack-import/src/utils/import-config-handler.ts
checksum: 3194f537cee8041f07a7ea91cdc6351c84e400766696d9c3cf80b98f99961f76
version: '1.0'
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class ExportSpaces {
log.debug(`Exporting Asset Management 2.0 (${linkedWorkspaces.length} space(s))`, context);
log.debug(`Spaces: ${linkedWorkspaces.map((ws) => ws.space_uid).join(', ')}`, context);

const spacesRootPath = pResolve(exportDir, branchName || 'main', 'spaces');
const spacesRootPath = pResolve(exportDir, 'spaces');
await mkdir(spacesRootPath, { recursive: true });
log.debug(`Spaces root path: ${spacesRootPath}`, context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,41 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
return '?' + parts.join('&');
}

/**
* Format response body or payload for error logging. Safely stringifies and truncates.
*/
private formatResponseBodyForError(data: unknown, maxLen: number = 500): string {
if (data === null || data === undefined) return '';
try {
const str = typeof data === 'string' ? data : JSON.stringify(data);
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
} catch {
return '';
}
}

/**
* Normalize AM API failures into a consistent error message with optional cause and body snippet.
*/
private normalizeAmGetFailure(details: {
path: string;
fullPath: string;
status?: number;
cause?: unknown;
bodySnippet?: string;
}): Error {
const { path, status, cause, bodySnippet } = details;
let message = `AM API GET failed: path ${path}`;
if (status) message += ` (status ${status})`;
if (cause && cause instanceof Error) {
message += ` - ${cause.message}`;
} else if (cause) {
message += ` - ${String(cause)}`;
}
if (bodySnippet) message += `\nResponse: ${bodySnippet}`;
return new Error(message);
}

/**
* GET a space-level endpoint (e.g. /api/spaces/{uid}). Builds path + query string and performs the request.
*/
Expand All @@ -73,11 +108,29 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
const queryString = this.buildQueryString(safeParams);
const fullPath = path + queryString;
log.debug(`GET ${fullPath}`, this.config.context);
const response = await this.apiClient.get<T>(fullPath);
if (response.status < 200 || response.status >= 300) {
throw new Error(`Asset Management API error: status ${response.status}, path ${path}`);

try {
const response = await this.apiClient.get<T>(fullPath);
if (response.status < 200 || response.status >= 300) {
const bodySnippet = this.formatResponseBodyForError(response.data);
throw this.normalizeAmGetFailure({
path,
fullPath,
status: response.status,
bodySnippet: bodySnippet || undefined,
});
}
return response.data as T;
} catch (error) {
if (error instanceof Error && error.message.includes('AM API GET failed')) {
throw error;
}
throw this.normalizeAmGetFailure({
path,
fullPath,
cause: error,
});
}
return response.data as T;
}

async init(): Promise<void> {
Expand Down Expand Up @@ -196,32 +249,60 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? '';
const headers = await this.getPostHeaders({ 'Content-Type': 'application/json', ...extraHeaders });
log.debug(`POST ${path}`, this.config.context);
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`AM API POST error: status ${response.status}, path ${path}, body: ${text}`);

try {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
const bodySnippet = this.formatResponseBodyForError(text);
throw new Error(
`AM API POST failed: status ${response.status} path ${path}${
bodySnippet ? `\nResponse: ${bodySnippet}` : ''
}`,
);
}
return response.json() as Promise<T>;
} catch (error) {
if (error instanceof Error && error.message.includes('AM API POST failed')) {
throw error;
}
throw new Error(`AM API POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`);
}
return response.json() as Promise<T>;
}

private async postMultipart<T>(path: string, form: FormData, extraHeaders: Record<string, string> = {}): Promise<T> {
const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? '';
const headers = await this.getPostHeaders(extraHeaders);
log.debug(`POST (multipart) ${path}`, this.config.context);
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers,
body: form,
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`AM API multipart POST error: status ${response.status}, path ${path}, body: ${text}`);

try {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers,
body: form,
});
if (!response.ok) {
const text = await response.text().catch(() => '');
const bodySnippet = this.formatResponseBodyForError(text);
throw new Error(
`AM API multipart POST failed: status ${response.status} path ${path}${
bodySnippet ? `\nResponse: ${bodySnippet}` : ''
}`,
);
}
return response.json() as Promise<T>;
} catch (error) {
if (error instanceof Error && error.message.includes('AM API multipart POST failed')) {
throw error;
}
throw new Error(
`AM API multipart POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`,
);
}
return response.json() as Promise<T>;
}

// ---------------------------------------------------------------------------
Expand Down
11 changes: 5 additions & 6 deletions packages/contentstack-export/src/export/module-exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,11 @@ class ModuleExporter {
this.exportConfig.context,
);
} catch (error) {
handleAndLogError(
error,
{ ...this.exportConfig.context, branch: targetBranch?.uid },
messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }),
);
throw new Error(messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }));
const originalMessage = (error as Error)?.message ?? '';
const errorMessage =
originalMessage || messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid });
handleAndLogError(error, { ...this.exportConfig.context, branch: targetBranch?.uid }, errorMessage);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we are using both "handleAndLogError" and throw error as well in the same block

throw new Error(errorMessage);
}
}

Expand Down
25 changes: 20 additions & 5 deletions packages/contentstack-export/src/export/modules/assets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import map from 'lodash/map';
import { getChalk } from '@contentstack/cli-utilities';
import { cliux, getChalk } from '@contentstack/cli-utilities';
import chunk from 'lodash/chunk';
import first from 'lodash/first';
import merge from 'lodash/merge';
Expand Down Expand Up @@ -34,6 +34,7 @@ import {
MODULE_NAMES,
getOrgUid,
} from '../../utils';
import { handle } from '@oclif/core';

export default class ExportAssets extends BaseClass {
private assetsRootPath: string;
Expand Down Expand Up @@ -61,13 +62,27 @@ export default class ExportAssets extends BaseClass {
if (linkedWorkspaces.length > 0) {
const assetManagementUrl = this.exportConfig.region?.assetManagementUrl;
if (!assetManagementUrl) {
this.completeProgress(
false,
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
handleAndLogError(
new Error(
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
),
{
...this.exportConfig.context,
message:
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
},
);
throw new Error(
this.completeProgressWithMessage({
moduleName: 'Asset Management 2.0',
customWarningMessage:
'Asset Management 2.0 export was skipped: assetManagementUrl is not configured. AM 2.0 assets will not be exported.',
context: this.exportConfig.context,
});
cliux.print(
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
{ color: 'yellow' },
);
return;
}
log.debug(
`Exporting with AM 2.0: ${assetManagementUrl} (linked_workspaces from exportConfig)`,
Expand Down
24 changes: 9 additions & 15 deletions packages/contentstack-import/src/utils/import-config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const setupConfig = async (importCmdFlags: any): Promise<ImportConfig> => {

const spacesDir = path.join(config.contentDir, 'spaces');
const stackSettingsPath = path.join(config.contentDir, 'stack', 'settings.json');
const stackJsonPath = path.join(config.contentDir, 'stack', 'stack.json');

if (existsSync(spacesDir) && existsSync(stackSettingsPath)) {
try {
Expand All @@ -135,22 +136,15 @@ const setupConfig = async (importCmdFlags: any): Promise<ImportConfig> => {
config.assetManagementEnabled = true;
config.assetManagementUrl = configHandler.get('region')?.assetManagementUrl;

const branchesJsonCandidates = [
path.join(config.contentDir, 'branches.json'),
path.join(config.contentDir, '..', 'branches.json'),
];
for (const branchesJsonPath of branchesJsonCandidates) {
if (existsSync(branchesJsonPath)) {
try {
const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8'));
const apiKey = branches?.[0]?.stackHeaders?.api_key;
if (apiKey) {
config.source_stack = apiKey;
}
} catch {
// branches.json unreadable — URL mapping will be skipped
if (existsSync(stackJsonPath)) {
try {
const stackData = JSON.parse(readFileSync(stackJsonPath, 'utf8'));
const apiKey = stackData?.api_key || stackData?.stackHeaders?.api_key;
if (apiKey) {
config.source_stack = apiKey;
}
break;
} catch {
// stack.json unreadable — source stack API key will not be set
}
}
}
Expand Down
Loading