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
12 changes: 12 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,16 @@ fileignoreconfig:
checksum: bc4a53f96be6a10786e00133245c7bdc43c965c8a98b753e3879e1110cf9c601
- filename: packages/contentstack-asset-management/test/unit/utils/cs-assets-api-adapter.test.ts
checksum: 63c6bff4d51842d8fa3cce88545259d0a2c3cfe71df95d303d993f692cee883b
- filename: packages/contentstack-audit/src/modules/assets.ts
checksum: 3a2f3d338ab7d417ad24fbf3d8f25e51e3098a440857ebbc2156e72e208a284e
- filename: packages/contentstack-audit/test/unit/modules/assets.test.ts
checksum: ee776f2d485c89ecf67aec9e61e37350e5754988bfca4574bbaef3c732f014b1
- filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json
checksum: ffaaee9269a6e833cd3dbe337ddc5c060cce948519873c7a2777754519a31b52
- filename: packages/contentstack-audit/test/unit/mock/am-contents/stack/stack.json
checksum: c4ac3ff80eb38b1c58eb0db21a232bd84f856d4647093542296199036945083c
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_one/assets/chunk0-assets.json
checksum: 9fc8b266ef58ecce8d3c4182632d1f7d088dfbce81b3fcea46b087cde3129b91
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_clean/assets/chunk0-assets.json
checksum: f02886c04ea1097ebc5965fa9a63a878adb9437d56a5b1797be1967fae35f9e4
version: '1.0'
5 changes: 4 additions & 1 deletion packages/contentstack-audit/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ const auditMsg = {
SCAN_CR_SUCCESS_MSG: 'Successfully completed the scanning of custom role with UID {uid} and name {name}.',
SCAN_CS_SUCCESS_MSG: 'Successfully completed the scanning of studio project with UID {uid} and name {name}.',
SCAN_ASSET_SUCCESS_MSG: `Successfully completed the scanning of Asset with UID '{uid}'.`,
SCAN_ASSET_WARN_MSG: `The locale '{locale}' or environment '{environment}' are not present for asset with uid '{uid}'`,
SCAN_ASSET_ENV_MISSING: `Environment '{environment}' is not present for asset '{uid}' (locale '{locale}')`,
SCAN_ASSET_LOCALE_MISSING: `Locale '{locale}' is not present for asset '{uid}' (environment '{environment}')`,
SCAN_ASSET_ENV_AND_LOCALE_MISSING: `Neither locale '{locale}' nor environment '{environment}' is present for asset '{uid}'`,
ASSET_PD_SOURCE_STACK_UNKNOWN: `Source stack API key not found at '{path}'; asset publish details tagged with an api_key were skipped and left unchanged.`,
ENTRY_PUBLISH_DETAILS: `Removing the publish details for entry '{uid}' of ct '{ctuid}' in locale '{locale}' as locale '{publocale}' or environment '{environment}' does not exist`,
CT_REFERENCE_FIELD: `The mentioned Reference field is not Array field reference is '{reference_to}' having display name '{display_name}''`,
ASSET_NOT_EXIST: `The publish_details either does not exist or is not an array for asset uid '{uid}'`,
Expand Down
87 changes: 82 additions & 5 deletions packages/contentstack-audit/src/modules/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default class Assets extends BaseClass {
protected missingEnvLocales: Record<string, any> = {};
public moduleName: keyof typeof auditConfig.moduleConfig;
private fixOverwriteConfirmed: boolean | null = null;
/** API key of the exported stack; null when `stack/stack.json` is missing or unreadable. */
private sourceStackApiKey: string | null = null;
/** Keeps the unknown-source-stack warning to one per run. */
private unverifiablePdWarned = false;
private resolvedBasePaths: Array<{ path: string; spaceId: string | null }> = [];
/** Map space dir name → the per-space multibar row label, or empty when single-space. */
private spaceProcessNames: Map<string, string> = new Map();
Expand Down Expand Up @@ -186,6 +190,32 @@ export default class Assets extends BaseClass {
this.environments = existsSync(environmentPath) ? keys(JSON.parse(readFileSync(environmentPath, 'utf8'))) : [];
log.debug(`Total environments loaded: ${this.environments.length}`, this.config.auditContext);
log.debug(`Environment names: ${this.environments.join(', ')}`, this.config.auditContext);

this.sourceStackApiKey = this.resolveSourceStackApiKey();
log.debug(
this.sourceStackApiKey
? `Source stack API key resolved from ${this.stackJsonPath()}`
: `Source stack API key not resolved from ${this.stackJsonPath()}`,
this.config.auditContext,
);
}

private stackJsonPath(): string {
return join(this.config.basePath, 'stack', 'stack.json');
}

/** Reads the exported stack's API key, used to scope Asset Management publish details to this stack. */
private resolveSourceStackApiKey(): string | null {
const stackJsonPath = this.stackJsonPath();
if (!existsSync(stackJsonPath)) return null;

try {
const stackData = JSON.parse(readFileSync(stackJsonPath, 'utf8'));
return stackData?.api_key || stackData?.stackHeaders?.api_key || null;
} catch (error) {
log.debug(`Could not read ${stackJsonPath}: ${error}`, this.config.auditContext);
return null;
}
}

/**
Expand Down Expand Up @@ -298,31 +328,55 @@ export default class Assets extends BaseClass {
let indexer = fsUtility.indexFileContent;
log.debug(`Found ${Object.keys(indexer).length} asset files to process`, this.config.auditContext);

let skippedCrossStack = 0;

for (const fileIndex in indexer) {
log.debug(`Processing asset file: ${indexer[fileIndex]}`, this.config.auditContext);
const assets = (await fsUtility.readChunkFiles.next()) as Record<string, EntryStruct>;
this.assets = assets;
log.debug(`Loaded ${Object.keys(assets).length} assets from file`, this.config.auditContext);

// Fix mode only prompts for and rewrites chunks that actually changed.
let chunkChanged = false;

for (const assetUid in assets) {
log.debug(`Processing asset: ${assetUid}`, this.config.auditContext);

if (this.assets[assetUid]?.publish_details && !Array.isArray(this.assets[assetUid].publish_details)) {
log.debug(`Asset ${assetUid} has invalid publish_details format`, this.config.auditContext);
cliux.print($t(auditMsg.ASSET_NOT_EXIST, { uid: assetUid }), { color: 'red' });
this.assets[assetUid].publish_details = [];
chunkChanged = true;
}

const publishDetails = this.assets[assetUid]?.publish_details;
log.debug(`Asset ${assetUid} has ${publishDetails?.length || 0} publish details`, this.config.auditContext);

if (Array.isArray(this.assets[assetUid].publish_details)) {
const originalPublishDetailsCount = this.assets[assetUid].publish_details.length;

this.assets[assetUid].publish_details = this.assets[assetUid].publish_details.filter((pd: any) => {
log.debug(
`Checking publish detail: locale=${pd?.locale}, environment=${pd?.environment}`,
this.config.auditContext,
);

// An Asset Management asset is shared across stacks and carries publish details for each stack
// it was published into, tagged with that stack's api_key. Only this export's stack can be
// validated here; entries belonging to other stacks (or unattributable ones, when the source
// stack api_key is unknown) are left untouched.
if (pd?.api_key && pd.api_key !== this.sourceStackApiKey) {
if (!this.sourceStackApiKey && !this.unverifiablePdWarned) {
this.unverifiablePdWarned = true;
log.warn(
$t(auditMsg.ASSET_PD_SOURCE_STACK_UNKNOWN, { path: this.stackJsonPath() }),
this.config.auditContext,
);
}
skippedCrossStack++;
return true;
}

if (this.locales?.includes(pd?.locale) && this.environments?.includes(pd?.environment)) {
log.debug(
`Publish detail valid for asset ${assetUid}: locale=${pd.locale}, environment=${pd.environment}`,
Expand All @@ -334,10 +388,17 @@ export default class Assets extends BaseClass {
`Publish detail invalid for asset ${assetUid}: locale=${pd.locale}, environment=${pd.environment}`,
this.config.auditContext,
);
cliux.print(
$t(auditMsg.SCAN_ASSET_WARN_MSG, { uid: assetUid, locale: pd.locale, environment: pd.environment }),
{ color: 'yellow' },
);
const localeMissing = !this.locales?.includes(pd?.locale);
const environmentMissing = !this.environments?.includes(pd?.environment);
const warnMsg =
localeMissing && environmentMissing
? auditMsg.SCAN_ASSET_ENV_AND_LOCALE_MISSING
: localeMissing
? auditMsg.SCAN_ASSET_LOCALE_MISSING
: auditMsg.SCAN_ASSET_ENV_MISSING;
cliux.print($t(warnMsg, { uid: assetUid, locale: pd.locale, environment: pd.environment }), {
color: 'yellow',
});
if (!Object.keys(this.missingEnvLocales).includes(assetUid)) {
log.debug(`Creating new missing reference entry for asset ${assetUid}`, this.config.auditContext);
this.missingEnvLocales[assetUid] = [
Expand All @@ -363,6 +424,10 @@ export default class Assets extends BaseClass {
return false;
}
});

if (this.assets[assetUid].publish_details.length !== originalPublishDetailsCount) {
chunkChanged = true;
}
}

log.info($t(auditMsg.SCAN_ASSET_SUCCESS_MSG, { uid: assetUid }), this.config.auditContext);
Expand All @@ -384,11 +449,23 @@ export default class Assets extends BaseClass {
}
}

if (this.fix) {
if (this.fix && chunkChanged) {
await this.writeFixContent(`${spacePath}/${indexer[fileIndex]}`, this.assets);
} else if (this.fix) {
log.debug(
`No changes for ${indexer[fileIndex]} - skipping write and fix confirmation`,
this.config.auditContext,
);
}
}

if (skippedCrossStack) {
log.info(
`Skipped ${skippedCrossStack} publish detail(s) of other stacks in ${spaceId ?? spacePath}`,
this.config.auditContext,
);
}

// Per-space row finished — close it so the multibar shows ✓ Complete
// and the next space (if any) starts cleanly.
if (spaceProcessName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env_own_dev": {
"name": "development",
"uid": "env_own_dev"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"locale-uid-1": {
"code": "en-us",
"fallback_locale": null,
"uid": "locale-uid-1",
"name": "English - United States"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "1": "chunk0-assets.json" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"am_asset_tagged": {
"uid": "am_asset_tagged",
"title": "Api key tagged publish details, unverifiable without stack.json",
"publish_details": [
{ "locale": "en-us", "environment": "env_own_missing", "api_key": "blt_own_stack" },
{ "locale": "de-de", "environment": "env_other_dev", "api_key": "blt_other_stack_one" }
]
},
"am_asset_legacy_pd": {
"uid": "am_asset_legacy_pd",
"title": "Legacy publish details without api_key stay auditable",
"publish_details": [
{ "locale": "en-us", "environment": "env_own_dev" },
{ "locale": "en-us", "environment": "env_own_missing" }
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"env_own_dev": {
"name": "development",
"uid": "env_own_dev"
},
"env_own_prod": {
"name": "production",
"uid": "env_own_prod"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"locale-uid-1": {
"code": "en-us",
"fallback_locale": null,
"uid": "locale-uid-1",
"name": "English - United States"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "1": "chunk0-assets.json" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"am_asset_clean": {
"uid": "am_asset_clean",
"title": "Nothing to fix here",
"publish_details": [
{ "locale": "en-us", "environment": "env_own_prod", "api_key": "blt_own_stack" },
{ "locale": "en-us", "environment": "env_other_prod", "api_key": "blt_other_stack_two" }
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "1": "chunk0-assets.json" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"am_asset_other_stacks_only_a": {
"uid": "am_asset_other_stacks_only_a",
"title": "Published only into other stacks, unknown env and locale here",
"publish_details": [
{ "locale": "en-us", "environment": "env_other_dev", "api_key": "blt_other_stack_one" },
{ "locale": "de-de", "environment": "env_other_prod", "api_key": "blt_other_stack_two" }
]
},
"am_asset_other_stacks_only_b": {
"uid": "am_asset_other_stacks_only_b",
"title": "Published only into other stacks, multiple environments",
"publish_details": [
{ "locale": "fr-fr", "environment": "env_other_dev", "api_key": "blt_other_stack_one" },
{ "locale": "fr-fr", "environment": "env_other_stage", "api_key": "blt_other_stack_one" },
{ "locale": "ja-jp", "environment": "env_other_prod", "api_key": "blt_other_stack_two" }
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "1": "chunk0-assets.json" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"am_asset_cross_only": {
"uid": "am_asset_cross_only",
"title": "Published only into other stacks",
"publish_details": [
{ "locale": "en-us", "environment": "env_other_dev", "api_key": "blt_other_stack_one" },
{ "locale": "fr-fr", "environment": "env_other_prod", "api_key": "blt_other_stack_two" }
]
},
"am_asset_mixed": {
"uid": "am_asset_mixed",
"title": "Own stack plus other stacks",
"publish_details": [
{ "locale": "en-us", "environment": "env_own_dev", "api_key": "blt_own_stack" },
{ "locale": "en-us", "environment": "env_other_dev", "api_key": "blt_other_stack_one" },
{ "locale": "en-us", "environment": "env_own_missing", "api_key": "blt_own_stack" }
]
},
"am_asset_bad_locale": {
"uid": "am_asset_bad_locale",
"title": "Own stack with unknown locale",
"publish_details": [{ "locale": "de-de", "environment": "env_own_prod", "api_key": "blt_own_stack" }]
},
"am_asset_bad_both": {
"uid": "am_asset_bad_both",
"title": "Own stack with unknown locale and environment",
"publish_details": [{ "locale": "de-de", "environment": "env_own_missing", "api_key": "blt_own_stack" }]
},
"am_asset_legacy_pd": {
"uid": "am_asset_legacy_pd",
"title": "Legacy publish details without api_key",
"publish_details": [
{ "locale": "en-us", "environment": "env_own_dev" },
{ "locale": "en-us", "environment": "env_own_missing" }
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"uid": "blt_own_stack_uid",
"name": "AM Mock Stack",
"api_key": "blt_own_stack",
"master_locale": "en-us",
"stackHeaders": {
"api_key": "blt_own_stack"
}
}
Loading
Loading