From 64f91ddd4f454854db8894d955684c176e9bd2fe Mon Sep 17 00:00:00 2001 From: Drew Scukanec Date: Thu, 26 Sep 2024 12:09:48 -0600 Subject: [PATCH 1/3] Add a new input flag to avoid commenting on stacks that have not changed. --- README.md | 17 +++++++++++++++++ src/action.ts | 3 ++- src/inputs.ts | 7 +++++++ src/stage-processor.ts | 17 ++++++++++++----- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0667837..77a33a8 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,20 @@ jobs: failOnDestructiveChanges: false githubToken: ${{ secrets.GITHUB_TOKEN }} ``` + +### Hide comments for unchanged stages + +If you want to prevent unnecessary comments by hiding comments for stacks that +have not changed, you can enable this feature. This defaults to off, showing +comments for all stacks. + +```yml +jobs: + Synth: + steps: + - name: Diff + uses: corymhall/cdk-diff-action@v1 + with: + ignoreUnchangedStacks: true + githubToken: ${{ secrets.GITHUB_TOKEN }} +``` \ No newline at end of file diff --git a/src/action.ts b/src/action.ts index afe6d0d..17430fb 100644 --- a/src/action.ts +++ b/src/action.ts @@ -13,6 +13,7 @@ export async function run() { noDiffForStages: getInput('noDiffForStages').split(','), noFailOnDestructiveChanges: getInput('noFailOnDestructiveChanges').split(','), cdkOutDir: getInput('cdkOutDir') ?? 'cdk.out', + ignoreUnchangedStacks: getBooleanInput('ignoreUnchangedStacks') }; const octokit = github.getOctokit(inputs.githubToken); const context = github.context; @@ -26,7 +27,7 @@ export async function run() { }); } const comments = new Comments(octokit, context); - const processor = new StageProcessor(stages, inputs.allowedDestroyTypes); + const processor = new StageProcessor(stages, inputs.allowedDestroyTypes, inputs.ignoreUnchangedStacks); try { await processor.processStages(inputs.noFailOnDestructiveChanges); } catch (e: any) { diff --git a/src/inputs.ts b/src/inputs.ts index 543bf06..67b4e3e 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -43,4 +43,11 @@ export interface Inputs { * @default cdk.out */ cdkOutDir: string; + + /** + * Whether the workflow will comment for unchanged stacks + * + * @default - comment for unchanged stacks + */ + ignoreUnchangedStacks: boolean; } diff --git a/src/stage-processor.ts b/src/stage-processor.ts index 2562999..c0695c6 100644 --- a/src/stage-processor.ts +++ b/src/stage-processor.ts @@ -40,6 +40,7 @@ export class StageProcessor { constructor( private readonly stages: StageInfo[], private readonly allowedDestroyTypes: string[], + private readonly ignoreUnchangedStacks: boolean, ) { this.stages.forEach(stage => { this.stageComments[stage.name] = { @@ -72,10 +73,12 @@ export class StageProcessor { for (const stage of this.stages) { for (const stack of stage.stacks) { try { - const { comment, changes } = await this.diffStack(stack); - this.stageComments[stage.name].stackComments[stack.name].push(...comment); + const { comment, destructiveChanges, anyChanges } = await this.diffStack(stack); + if (anyChanges || !this.ignoreUnchangedStacks) { + this.stageComments[stage.name].stackComments[stack.name].push(...comment); + } if (!ignoreDestructiveChanges.includes(stage.name)) { - this.stageComments[stage.name].destructiveChanges += changes; + this.stageComments[stage.name].destructiveChanges += destructiveChanges; } } catch (e: any) { console.error('Error processing stages: ', e); @@ -93,6 +96,9 @@ export class StageProcessor { stackName, })); const stackComment = this.getCommentForStack(stageName, stackName, comment); + if (stackComment.length == 0) { + continue; + } if (stackComment.join('\n').length > MAX_COMMENT_LENGTH) { throw new Error(`Comment for stack ${stackName} is too long, please report this as a bug https://github.com/corymhall/cdk-diff-action/issues/new`); } @@ -145,13 +151,14 @@ export class StageProcessor { return false; } - private async diffStack(stack: StackInfo): Promise<{comment: string[]; changes: number}> { + private async diffStack(stack: StackInfo): Promise<{comment: string[]; destructiveChanges: number, anyChanges: boolean}> { try { const stackDiff = new StackDiff(stack, this.allowedDestroyTypes); const { diff, changes } = await stackDiff.diffStack(); return { comment: this.formatStackComment(stack.name, diff, changes), - changes: changes.destructiveChanges.length, + destructiveChanges: changes.destructiveChanges.length, + anyChanges: changes.destructiveChanges.length > 0 || changes.removedResources > 0 || changes.updatedResources > 0 || changes.createdResources > 0, }; } catch (e: any) { From 7d95b1301d545d6e7c234ea15b7ae51df22611a3 Mon Sep 17 00:00:00 2001 From: Drew Scukanec Date: Thu, 26 Sep 2024 12:26:41 -0600 Subject: [PATCH 2/3] Add a test to protect the feature --- test/stage-processor.test.ts | 39 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/test/stage-processor.test.ts b/test/stage-processor.test.ts index dd66fa7..a9adb60 100644 --- a/test/stage-processor.test.ts +++ b/test/stage-processor.test.ts @@ -61,15 +61,15 @@ describe('StageProcessor', () => { test('stage with no diffs', async () => { cfnMock.on(GetTemplateCommand) - .resolves({ - TemplateBody: JSON.stringify(stackInfo.content), - }); + .resolves({ + TemplateBody: JSON.stringify(stackInfo.content), + }); const processor = new StageProcessor([ { name: 'Stage1', stacks: [stackInfo], }, - ], []); + ], [], false); await processor.processStages(); const p = (processor as any).stageComments; expect(p).toEqual({ @@ -78,6 +78,27 @@ describe('StageProcessor', () => { expect(p.Stage1.stackComments['my-stack']).toEqual(['No Changes for stack: my-stack :white_check_mark:']); }); + test('stage with no diffs and ignored', async () => { + cfnMock.on(GetTemplateCommand) + .resolves({ + TemplateBody: JSON.stringify(stackInfo.content), + }); + const processor = new StageProcessor([ + { + name: 'Stage1', + stacks: [stackInfo], + }, + ], [], true); + await processor.processStages(); + const p = (processor as any).stageComments; + expect(p).toEqual({ + Stage1: expect.any(Object), + }); + expect(p.Stage1.stackComments['my-stack']).toEqual([]); + expect(createCommentMock).toHaveBeenCalledTimes(0); + expect(updateCommentMock).toHaveBeenCalledTimes(0); + }); + test('stage with diff', async () => { cfnMock.on(GetTemplateCommand) .resolves({ @@ -101,7 +122,7 @@ describe('StageProcessor', () => { }, }], }, - ], []); + ], [], false); await processor.processStages(); const p = (processor as any).stageComments; expect(p).toEqual({ @@ -134,7 +155,7 @@ describe('StageProcessor', () => { }, }], }, - ], []); + ], [], false); await processor.processStages(['Stage1']); const p = (processor as any).stageComments; expect(p).toEqual({ @@ -167,7 +188,7 @@ describe('StageProcessor', () => { }, }], }, - ], []); + ], [], false); await processor.processStages(['Stage1']); await processor.commentStages(new Comments({} as any, {} as any)); expect(createCommentMock).toHaveBeenCalledTimes(1); @@ -198,7 +219,7 @@ describe('StageProcessor', () => { }, }], }, - ], []); + ], [], false); findPreviousMock.mockResolvedValue(1); await processor.processStages(['Stage1']); await processor.commentStages(new Comments({} as any, {} as any)); @@ -231,7 +252,7 @@ describe('StageProcessor', () => { name: 'Stage1', stacks: createStacks(10), }, - ], []); + ], [], false); findPreviousMock.mockResolvedValue(1); await processor.processStages(['Stage1']); await processor.commentStages(new Comments({} as any, {} as any)); From e447c54d79b44869eccdb9094da4a641351d6fa0 Mon Sep 17 00:00:00 2001 From: Drew Scukanec Date: Thu, 26 Sep 2024 12:28:26 -0600 Subject: [PATCH 3/3] Reduce diff --- test/stage-processor.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/stage-processor.test.ts b/test/stage-processor.test.ts index a9adb60..ef387fa 100644 --- a/test/stage-processor.test.ts +++ b/test/stage-processor.test.ts @@ -61,9 +61,9 @@ describe('StageProcessor', () => { test('stage with no diffs', async () => { cfnMock.on(GetTemplateCommand) - .resolves({ - TemplateBody: JSON.stringify(stackInfo.content), - }); + .resolves({ + TemplateBody: JSON.stringify(stackInfo.content), + }); const processor = new StageProcessor([ { name: 'Stage1',