diff --git a/__tests__/changeset-formatter.test.ts b/__tests__/changeset-formatter.test.ts index 7e673d8..6cba7ea 100644 --- a/__tests__/changeset-formatter.test.ts +++ b/__tests__/changeset-formatter.test.ts @@ -426,6 +426,16 @@ describe('Change Set Formatter', () => { ResourceType: 'AWS::DynamoDB::Table', Replacement: 'True', Scope: ['Properties'], + BeforeContext: JSON.stringify({ + Properties: { + BillingMode: 'PROVISIONED' + } + }), + AfterContext: JSON.stringify({ + Properties: { + BillingMode: 'PAY_PER_REQUEST' + } + }), Details: [ { Target: { @@ -458,42 +468,21 @@ describe('Change Set Formatter', () => { ) expect(markdown).toContain('**Physical ID:** `my-table-123`') expect(markdown).toContain('⚠️ **This resource will be replaced**') - expect(markdown).toContain( - '**BillingMode:** `PROVISIONED` → `PAY_PER_REQUEST`' - ) + expect(markdown).toContain('```diff') expect(markdown).toContain('⚠️ Requires recreation: Always') }) - test('diffs Tags arrays correctly', () => { + test('displays AfterContext for Add actions in console output', () => { const changesSummary = JSON.stringify({ changes: [ { Type: 'Resource', ResourceChange: { - Action: 'Modify', - LogicalResourceId: 'MyParameter', - ResourceType: 'AWS::SSM::Parameter', - Replacement: 'False', - Scope: ['Properties'], - Details: [ - { - Target: { - Attribute: 'Properties', - Name: 'Tags', - RequiresRecreation: 'Never', - BeforeValue: JSON.stringify([ - { Key: 'Version', Value: 'v1' }, - { Key: 'Team', Value: 'DevOps' }, - { Key: 'Environment', Value: 'test' } - ]), - AfterValue: JSON.stringify([ - { Key: 'Version', Value: 'v2' }, - { Key: 'UpdateType', Value: 'InPlace' }, - { Key: 'Environment', Value: 'production' } - ]) - } - } - ] + Action: 'Add', + LogicalResourceId: 'NewBucket', + ResourceType: 'AWS::S3::Bucket', + AfterContext: + '{"BucketName":"my-bucket","Versioning":{"Status":"Enabled"}}' } } ], @@ -501,43 +490,53 @@ describe('Change Set Formatter', () => { truncated: false }) - const markdown = generateChangeSetMarkdown(changesSummary) + displayChangeSet(changesSummary, 1, true) - expect(markdown).toContain('**Tags:**') - expect(markdown).toContain('**Tags.Environment:** `test` → `production`') - expect(markdown).toContain('**Tags.Team:** `DevOps` → (removed)') - expect(markdown).toContain('**Tags.UpdateType:** (added) → `InPlace`') - expect(markdown).toContain('**Tags.Version:** `v1` → `v2`') + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('Properties:') + ) + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('BucketName') + ) }) - test('diffs nested objects correctly', () => { + test('displays BeforeContext for Remove actions in console output', () => { const changesSummary = JSON.stringify({ changes: [ { Type: 'Resource', ResourceChange: { - Action: 'Modify', - LogicalResourceId: 'MyResource', + Action: 'Remove', + LogicalResourceId: 'OldBucket', + ResourceType: 'AWS::S3::Bucket', + BeforeContext: '{"BucketName":"old-bucket"}' + } + } + ], + totalChanges: 1, + truncated: false + }) + + displayChangeSet(changesSummary, 1, true) + + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('Properties:') + ) + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('BucketName') + ) + }) + + test('handles invalid JSON in AfterContext gracefully', () => { + const changesSummary = JSON.stringify({ + changes: [ + { + Type: 'Resource', + ResourceChange: { + Action: 'Add', + LogicalResourceId: 'NewResource', ResourceType: 'AWS::Custom::Resource', - Replacement: 'False', - Scope: ['Properties'], - Details: [ - { - Target: { - Attribute: 'Properties', - Name: 'Config', - RequiresRecreation: 'Never', - BeforeValue: JSON.stringify({ - Setting: 'old', - Nested: { Value: 'a' } - }), - AfterValue: JSON.stringify({ - Setting: 'new', - Nested: { Value: 'b' } - }) - } - } - ] + AfterContext: 'invalid-json{' } } ], @@ -545,34 +544,23 @@ describe('Change Set Formatter', () => { truncated: false }) - const markdown = generateChangeSetMarkdown(changesSummary) + displayChangeSet(changesSummary, 1, true) - expect(markdown).toContain('**Config.Setting:** `old` → `new`') - expect(markdown).toContain('**Config.Nested.Value:** `a` → `b`') + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('invalid-json{') + ) }) - test('handles generic arrays as JSON strings', () => { + test('handles invalid JSON in BeforeContext gracefully', () => { const changesSummary = JSON.stringify({ changes: [ { Type: 'Resource', ResourceChange: { - Action: 'Modify', - LogicalResourceId: 'MyResource', + Action: 'Remove', + LogicalResourceId: 'OldResource', ResourceType: 'AWS::Custom::Resource', - Replacement: 'False', - Scope: ['Properties'], - Details: [ - { - Target: { - Attribute: 'Properties', - Name: 'Items', - RequiresRecreation: 'Never', - BeforeValue: JSON.stringify(['a', 'b']), - AfterValue: JSON.stringify(['a', 'c']) - } - } - ] + BeforeContext: 'invalid-json{' } } ], @@ -580,42 +568,35 @@ describe('Change Set Formatter', () => { truncated: false }) - const markdown = generateChangeSetMarkdown(changesSummary) + displayChangeSet(changesSummary, 1, true) - expect(markdown).toContain('**Items:**') - expect(markdown).toContain('["a","b"]') - expect(markdown).toContain('["a","c"]') + expect(core.info).toHaveBeenCalledWith( + expect.stringContaining('invalid-json{') + ) }) - test('handles array additions and removals', () => { + test('generates diff view for resources with BeforeContext/AfterContext', () => { const changesSummary = JSON.stringify({ changes: [ { Type: 'Resource', ResourceChange: { Action: 'Modify', - LogicalResourceId: 'MyResource', - ResourceType: 'AWS::Custom::Resource', + LogicalResourceId: 'MyTopic', + ResourceType: 'AWS::SNS::Topic', Replacement: 'False', - Scope: ['Properties'], - Details: [ - { - Target: { - Attribute: 'Properties', - Name: 'NewList', - RequiresRecreation: 'Never', - AfterValue: JSON.stringify(['x', 'y']) - } - }, - { - Target: { - Attribute: 'Properties', - Name: 'OldList', - RequiresRecreation: 'Never', - BeforeValue: JSON.stringify(['a', 'b']) - } + BeforeContext: JSON.stringify({ + Properties: { + DisplayName: 'old-name', + Tags: [{ Key: 'Env', Value: 'dev' }] } - ] + }), + AfterContext: JSON.stringify({ + Properties: { + DisplayName: 'new-name', + Tags: [{ Key: 'Env', Value: 'prod' }] + } + }) } } ], @@ -625,36 +606,38 @@ describe('Change Set Formatter', () => { const markdown = generateChangeSetMarkdown(changesSummary) - expect(markdown).toContain('**NewList:** (added) → `["x","y"]`') - expect(markdown).toContain('**OldList:** `["a","b"]` → (removed)') + expect(markdown).toContain('```diff') + expect(markdown).toContain('-') + expect(markdown).toContain('+') }) - test('handles primitive value additions and removals', () => { + test('shows recreation warnings in diff view', () => { const changesSummary = JSON.stringify({ changes: [ { Type: 'Resource', ResourceChange: { Action: 'Modify', - LogicalResourceId: 'MyResource', - ResourceType: 'AWS::Custom::Resource', - Replacement: 'False', - Scope: ['Properties'], + LogicalResourceId: 'MyParam', + ResourceType: 'AWS::SSM::Parameter', + Replacement: 'True', + BeforeContext: JSON.stringify({ + Properties: { + Name: '/old/path', + Value: 'old' + } + }), + AfterContext: JSON.stringify({ + Properties: { + Name: '/new/path', + Value: 'new' + } + }), Details: [ { Target: { - Attribute: 'Properties', - Name: 'NewProp', - RequiresRecreation: 'Never', - AfterValue: 'new-value' - } - }, - { - Target: { - Attribute: 'Properties', - Name: 'OldProp', - RequiresRecreation: 'Never', - BeforeValue: 'old-value' + Name: 'Name', + RequiresRecreation: 'Always' } } ] @@ -667,8 +650,8 @@ describe('Change Set Formatter', () => { const markdown = generateChangeSetMarkdown(changesSummary) - expect(markdown).toContain('**NewProp:** (added) → `new-value`') - expect(markdown).toContain('**OldProp:** `old-value` → (removed)') + expect(markdown).toContain('```diff') + expect(markdown).toContain('⚠️ Requires recreation: Always') }) test('displays AfterContext for Add actions in console output', () => { diff --git a/dist/index.js b/dist/index.js index 09f0b82..e69f67d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -59734,73 +59734,60 @@ exports.displayChangeSet = displayChangeSet; exports.generateChangeSetMarkdown = generateChangeSetMarkdown; const core = __importStar(__nccwpck_require__(7484)); /** - * Recursively diff JSON values and return formatted changes + * Generate a git-style diff view of JSON objects with recreation warnings */ -function diffJson(before, after, path = '') { - var _a, _b; - const lines = []; - // Handle arrays - if (Array.isArray(before) || Array.isArray(after)) { - const beforeArr = Array.isArray(before) ? before : []; - const afterArr = Array.isArray(after) ? after : []; - // Special case: array of {Key, Value} objects (Tags) - if (((_a = beforeArr[0]) === null || _a === void 0 ? void 0 : _a.Key) || ((_b = afterArr[0]) === null || _b === void 0 ? void 0 : _b.Key)) { - const beforeMap = new Map(beforeArr.map((t) => [t.Key, t.Value])); - const afterMap = new Map(afterArr.map((t) => [t.Key, t.Value])); - const allKeys = new Set([...beforeMap.keys(), ...afterMap.keys()]); - for (const key of Array.from(allKeys).sort()) { - const b = beforeMap.get(key); - const a = afterMap.get(key); - const prefix = path ? `${path}.${key}` : key; - if (b && a && b !== a) - lines.push(` - **${prefix}:** \`${b}\` → \`${a}\``); - else if (a && !b) - lines.push(` - **${prefix}:** (added) → \`${a}\``); - else if (b && !a) - lines.push(` - **${prefix}:** \`${b}\` → (removed)`); - } - return lines; - } - // Generic array - show as JSON if different - if (JSON.stringify(beforeArr) !== JSON.stringify(afterArr)) { - if (beforeArr.length && afterArr.length) { - lines.push(` - **${path || 'value'}:** \`${JSON.stringify(beforeArr)}\` → \`${JSON.stringify(afterArr)}\``); - } - else if (afterArr.length) { - lines.push(` - **${path || 'value'}:** (added) → \`${JSON.stringify(afterArr)}\``); - } - else if (beforeArr.length) { - lines.push(` - **${path || 'value'}:** \`${JSON.stringify(beforeArr)}\` → (removed)`); +function generateJsonDiff(before, after, details) { + const beforeJson = before ? JSON.stringify(before, null, 2) : '{}'; + const afterJson = after ? JSON.stringify(after, null, 2) : '{}'; + if (beforeJson === afterJson) { + return '```json\n' + beforeJson + '\n```\n'; + } + // Build map of properties that require recreation + const recreationMap = new Map(); + if (details) { + for (const detail of details) { + const target = detail.Target; + if ((target === null || target === void 0 ? void 0 : target.Name) && + target.RequiresRecreation && + target.RequiresRecreation !== 'Never') { + recreationMap.set(target.Name, target.RequiresRecreation); + } + } + } + const beforeLines = beforeJson.split('\n'); + const afterLines = afterJson.split('\n'); + const diff = []; + let i = 0; + let j = 0; + while (i < beforeLines.length || j < afterLines.length) { + const beforeLine = beforeLines[i]; + const afterLine = afterLines[j]; + if (beforeLine === afterLine) { + diff.push(' ' + beforeLine); + i++; + j++; + } + else if (i < beforeLines.length && !afterLines.includes(beforeLines[i])) { + diff.push('-' + beforeLine); + i++; + } + else if (j < afterLines.length) { + let line = '+' + afterLine; + // Check if this line contains a property that requires recreation + for (const [propName, recreationType] of recreationMap) { + if (afterLine.includes(`"${propName}"`)) { + line += ` ⚠️ Requires recreation: ${recreationType}`; + break; + } } + diff.push(line); + j++; } - return lines; - } - // Handle objects - if ((before && typeof before === 'object') || - (after && typeof after === 'object')) { - const beforeObj = before; - const afterObj = after; - const allKeys = new Set([ - ...Object.keys(beforeObj || {}), - ...Object.keys(afterObj || {}) - ]); - for (const key of allKeys) { - const newPath = path ? `${path}.${key}` : key; - lines.push(...diffJson(beforeObj === null || beforeObj === void 0 ? void 0 : beforeObj[key], afterObj === null || afterObj === void 0 ? void 0 : afterObj[key], newPath)); + else { + i++; } - return lines; - } - // Primitives - if (before !== after) { - const prefix = path || 'value'; - if (before && after) - lines.push(` - **${prefix}:** \`${before}\` → \`${after}\``); - else if (after) - lines.push(` - **${prefix}:** (added) → \`${after}\``); - else if (before) - lines.push(` - **${prefix}:** \`${before}\` → (removed)`); } - return lines; + return '```diff\n' + diff.join('\n') + '\n```\n'; } /** * ANSI color codes @@ -60113,91 +60100,30 @@ function generateChangeSetMarkdown(changesSummary) { else if (rc.Action === 'Modify' && rc.Replacement === 'Conditional') { markdown += `⚠️ **May require replacement**\n\n`; } - // Property changes - if (rc.Details && rc.Details.length > 0) { - markdown += '**Property Changes:**\n\n'; - for (const detail of rc.Details) { - const target = detail.Target; - if (!target) - continue; - const propName = target.Name || target.Attribute || 'Unknown'; - // Try to parse as JSON and diff - let diffLines = []; - try { - const before = target.BeforeValue - ? JSON.parse(target.BeforeValue) - : undefined; - const after = target.AfterValue - ? JSON.parse(target.AfterValue) - : undefined; - diffLines = diffJson(before, after, propName); - } - catch (_c) { - // Not JSON, use simple string diff - if (target.BeforeValue && target.AfterValue) { - diffLines = [ - `- **${propName}:** \`${target.BeforeValue}\` → \`${target.AfterValue}\`` - ]; - } - else if (target.AfterValue) { - diffLines = [ - `- **${propName}:** (added) → \`${target.AfterValue}\`` - ]; - } - else if (target.BeforeValue) { - diffLines = [ - `- **${propName}:** \`${target.BeforeValue}\` → (removed)` - ]; - } - } - // Output the diff lines - if (diffLines.length > 0) { - if (diffLines.length === 1 && diffLines[0].startsWith(' - ')) { - // Single nested line from diffJson - promote to top level - markdown += `- ${diffLines[0].substring(4)}\n`; - } - else if (diffLines.length === 1) { - // Single line already formatted - markdown += `${diffLines[0]}\n`; - } - else { - // Multiple lines - show property name and indent children - markdown += `- **${propName}:**\n`; - diffLines.forEach(line => { - markdown += `${line}\n`; - }); - } + // Show diff view using BeforeContext/AfterContext when available + if (rc.BeforeContext || rc.AfterContext) { + try { + const before = rc.BeforeContext + ? JSON.parse(rc.BeforeContext) + : undefined; + const after = rc.AfterContext + ? JSON.parse(rc.AfterContext) + : undefined; + markdown += generateJsonDiff(before, after, rc.Details); + } + catch (_c) { + // If parsing fails, fall back to showing raw JSON + if (rc.AfterContext) { + markdown += '\n**Properties:**\n```json\n'; + markdown += rc.AfterContext; + markdown += '\n```\n'; } - if (target.RequiresRecreation && - target.RequiresRecreation !== 'Never') { - markdown += ` - ⚠️ Requires recreation: ${target.RequiresRecreation}\n`; + else if (rc.BeforeContext) { + markdown += '\n**Properties:**\n```json\n'; + markdown += rc.BeforeContext; + markdown += '\n```\n'; } } - markdown += '\n'; - } - // AfterContext for Add actions - if (rc.Action === 'Add' && rc.AfterContext) { - try { - const afterProps = JSON.parse(rc.AfterContext); - markdown += '\n**Properties:**\n```json\n'; - markdown += JSON.stringify(afterProps, null, 2); - markdown += '\n```\n'; - } - catch (_d) { - // Skip if can't parse - } - } - // BeforeContext for Remove actions - if (rc.Action === 'Remove' && rc.BeforeContext) { - try { - const beforeProps = JSON.parse(rc.BeforeContext); - markdown += '\n**Properties:**\n```json\n'; - markdown += JSON.stringify(beforeProps, null, 2); - markdown += '\n```\n'; - } - catch (_e) { - // Skip if can't parse - } } markdown += '\n\n\n'; } diff --git a/src/changeset-formatter.ts b/src/changeset-formatter.ts index 655e5dd..074df01 100644 --- a/src/changeset-formatter.ts +++ b/src/changeset-formatter.ts @@ -17,85 +17,70 @@ interface ChangeSetSummary { } /** - * Recursively diff JSON values and return formatted changes + * Generate a git-style diff view of JSON objects with recreation warnings */ -function diffJson(before: unknown, after: unknown, path = ''): string[] { - const lines: string[] = [] - - // Handle arrays - if (Array.isArray(before) || Array.isArray(after)) { - const beforeArr = Array.isArray(before) ? before : [] - const afterArr = Array.isArray(after) ? after : [] +function generateJsonDiff( + before: unknown, + after: unknown, + details?: Array<{ Target?: { Name?: string; RequiresRecreation?: string } }> +): string { + const beforeJson = before ? JSON.stringify(before, null, 2) : '{}' + const afterJson = after ? JSON.stringify(after, null, 2) : '{}' + + if (beforeJson === afterJson) { + return '```json\n' + beforeJson + '\n```\n' + } - // Special case: array of {Key, Value} objects (Tags) - if (beforeArr[0]?.Key || afterArr[0]?.Key) { - const beforeMap = new Map( - beforeArr.map((t: { Key: string; Value: string }) => [t.Key, t.Value]) - ) - const afterMap = new Map( - afterArr.map((t: { Key: string; Value: string }) => [t.Key, t.Value]) - ) - const allKeys = new Set([...beforeMap.keys(), ...afterMap.keys()]) - - for (const key of Array.from(allKeys).sort()) { - const b = beforeMap.get(key) - const a = afterMap.get(key) - const prefix = path ? `${path}.${key}` : key - if (b && a && b !== a) - lines.push(` - **${prefix}:** \`${b}\` → \`${a}\``) - else if (a && !b) lines.push(` - **${prefix}:** (added) → \`${a}\``) - else if (b && !a) lines.push(` - **${prefix}:** \`${b}\` → (removed)`) + // Build map of properties that require recreation + const recreationMap = new Map() + if (details) { + for (const detail of details) { + const target = detail.Target + if ( + target?.Name && + target.RequiresRecreation && + target.RequiresRecreation !== 'Never' + ) { + recreationMap.set(target.Name, target.RequiresRecreation) } - return lines } - - // Generic array - show as JSON if different - if (JSON.stringify(beforeArr) !== JSON.stringify(afterArr)) { - if (beforeArr.length && afterArr.length) { - lines.push( - ` - **${path || 'value'}:** \`${JSON.stringify(beforeArr)}\` → \`${JSON.stringify(afterArr)}\`` - ) - } else if (afterArr.length) { - lines.push( - ` - **${path || 'value'}:** (added) → \`${JSON.stringify(afterArr)}\`` - ) - } else if (beforeArr.length) { - lines.push( - ` - **${path || 'value'}:** \`${JSON.stringify(beforeArr)}\` → (removed)` - ) - } - } - return lines } - // Handle objects - if ( - (before && typeof before === 'object') || - (after && typeof after === 'object') - ) { - const beforeObj = before as Record - const afterObj = after as Record - const allKeys = new Set([ - ...Object.keys(beforeObj || {}), - ...Object.keys(afterObj || {}) - ]) - for (const key of allKeys) { - const newPath = path ? `${path}.${key}` : key - lines.push(...diffJson(beforeObj?.[key], afterObj?.[key], newPath)) + const beforeLines = beforeJson.split('\n') + const afterLines = afterJson.split('\n') + + const diff: string[] = [] + let i = 0 + let j = 0 + + while (i < beforeLines.length || j < afterLines.length) { + const beforeLine = beforeLines[i] + const afterLine = afterLines[j] + + if (beforeLine === afterLine) { + diff.push(' ' + beforeLine) + i++ + j++ + } else if (i < beforeLines.length && !afterLines.includes(beforeLines[i])) { + diff.push('-' + beforeLine) + i++ + } else if (j < afterLines.length) { + let line = '+' + afterLine + // Check if this line contains a property that requires recreation + for (const [propName, recreationType] of recreationMap) { + if (afterLine.includes(`"${propName}"`)) { + line += ` ⚠️ Requires recreation: ${recreationType}` + break + } + } + diff.push(line) + j++ + } else { + i++ } - return lines } - // Primitives - if (before !== after) { - const prefix = path || 'value' - if (before && after) - lines.push(` - **${prefix}:** \`${before}\` → \`${after}\``) - else if (after) lines.push(` - **${prefix}:** (added) → \`${after}\``) - else if (before) lines.push(` - **${prefix}:** \`${before}\` → (removed)`) - } - - return lines + return '```diff\n' + diff.join('\n') + '\n```\n' } /** @@ -473,90 +458,27 @@ export function generateChangeSetMarkdown(changesSummary: string): string { markdown += `⚠️ **May require replacement**\n\n` } - // Property changes - if (rc.Details && rc.Details.length > 0) { - markdown += '**Property Changes:**\n\n' - for (const detail of rc.Details) { - const target = detail.Target - if (!target) continue - - const propName = target.Name || target.Attribute || 'Unknown' - - // Try to parse as JSON and diff - let diffLines: string[] = [] - try { - const before = target.BeforeValue - ? JSON.parse(target.BeforeValue) - : undefined - const after = target.AfterValue - ? JSON.parse(target.AfterValue) - : undefined - diffLines = diffJson(before, after, propName) - } catch { - // Not JSON, use simple string diff - if (target.BeforeValue && target.AfterValue) { - diffLines = [ - `- **${propName}:** \`${target.BeforeValue}\` → \`${target.AfterValue}\`` - ] - } else if (target.AfterValue) { - diffLines = [ - `- **${propName}:** (added) → \`${target.AfterValue}\`` - ] - } else if (target.BeforeValue) { - diffLines = [ - `- **${propName}:** \`${target.BeforeValue}\` → (removed)` - ] - } - } - - // Output the diff lines - if (diffLines.length > 0) { - if (diffLines.length === 1 && diffLines[0].startsWith(' - ')) { - // Single nested line from diffJson - promote to top level - markdown += `- ${diffLines[0].substring(4)}\n` - } else if (diffLines.length === 1) { - // Single line already formatted - markdown += `${diffLines[0]}\n` - } else { - // Multiple lines - show property name and indent children - markdown += `- **${propName}:**\n` - diffLines.forEach(line => { - markdown += `${line}\n` - }) - } - } - - if ( - target.RequiresRecreation && - target.RequiresRecreation !== 'Never' - ) { - markdown += ` - ⚠️ Requires recreation: ${target.RequiresRecreation}\n` - } - } - markdown += '\n' - } - - // AfterContext for Add actions - if (rc.Action === 'Add' && rc.AfterContext) { - try { - const afterProps = JSON.parse(rc.AfterContext) - markdown += '\n**Properties:**\n```json\n' - markdown += JSON.stringify(afterProps, null, 2) - markdown += '\n```\n' - } catch { - // Skip if can't parse - } - } - - // BeforeContext for Remove actions - if (rc.Action === 'Remove' && rc.BeforeContext) { + // Show diff view using BeforeContext/AfterContext when available + if (rc.BeforeContext || rc.AfterContext) { try { - const beforeProps = JSON.parse(rc.BeforeContext) - markdown += '\n**Properties:**\n```json\n' - markdown += JSON.stringify(beforeProps, null, 2) - markdown += '\n```\n' + const before = rc.BeforeContext + ? JSON.parse(rc.BeforeContext) + : undefined + const after = rc.AfterContext + ? JSON.parse(rc.AfterContext) + : undefined + markdown += generateJsonDiff(before, after, rc.Details) } catch { - // Skip if can't parse + // If parsing fails, fall back to showing raw JSON + if (rc.AfterContext) { + markdown += '\n**Properties:**\n```json\n' + markdown += rc.AfterContext + markdown += '\n```\n' + } else if (rc.BeforeContext) { + markdown += '\n**Properties:**\n```json\n' + markdown += rc.BeforeContext + markdown += '\n```\n' + } } }