From 83bee296ac49e221479806f2a97c8b33d0759cf7 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 29 Sep 2026 01:41:28 +0200 Subject: [PATCH 1/2] fix: explicitly disable AlarmConfiguration when no alarms are configured CodeDeploy treats an omitted alarmConfiguration on UpdateDeploymentGroup as "no change", so alarms removed from deploymentSettings (e.g. switching to AllAtOnce) stayed attached and could still stop the deployment. Closes #103 --- .../11.output.v2-websocket-authorizer.json | 4 ++++ .../13.output.multiple-function-hooks.json | 8 +++++++ ...ut.elbV2-with-provisioned-concurrency.json | 8 +++++++ fixtures/20.output.canary-alarms-mixed.json | 4 ++++ lib/CfTemplateGenerators/CodeDeploy.js | 22 +++++++++---------- lib/CfTemplateGenerators/CodeDeploy.test.js | 6 ++++- serverless-plugin-canary-deployments.test.js | 6 ++--- 7 files changed, 43 insertions(+), 15 deletions(-) diff --git a/fixtures/11.output.v2-websocket-authorizer.json b/fixtures/11.output.v2-websocket-authorizer.json index 9a8a91b..4beec45 100644 --- a/fixtures/11.output.v2-websocket-authorizer.json +++ b/fixtures/11.output.v2-websocket-authorizer.json @@ -986,6 +986,10 @@ "CodeDeployServiceRole", "Arn" ] + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, diff --git a/fixtures/13.output.multiple-function-hooks.json b/fixtures/13.output.multiple-function-hooks.json index aa0ac11..7f8d27d 100644 --- a/fixtures/13.output.multiple-function-hooks.json +++ b/fixtures/13.output.multiple-function-hooks.json @@ -346,6 +346,10 @@ "DeploymentStyle": { "DeploymentType": "BLUE_GREEN", "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, @@ -409,6 +413,10 @@ "DeploymentStyle": { "DeploymentType": "BLUE_GREEN", "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, diff --git a/fixtures/17.output.elbV2-with-provisioned-concurrency.json b/fixtures/17.output.elbV2-with-provisioned-concurrency.json index ce89c63..e15e047 100644 --- a/fixtures/17.output.elbV2-with-provisioned-concurrency.json +++ b/fixtures/17.output.elbV2-with-provisioned-concurrency.json @@ -425,6 +425,10 @@ "DeploymentStyle": { "DeploymentType": "BLUE_GREEN", "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, @@ -460,6 +464,10 @@ "DeploymentStyle": { "DeploymentType": "BLUE_GREEN", "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, diff --git a/fixtures/20.output.canary-alarms-mixed.json b/fixtures/20.output.canary-alarms-mixed.json index 154012f..88e4034 100644 --- a/fixtures/20.output.canary-alarms-mixed.json +++ b/fixtures/20.output.canary-alarms-mixed.json @@ -441,6 +441,10 @@ "CodeDeployServiceRole", "Arn" ] + }, + "AlarmConfiguration": { + "Alarms": [], + "Enabled": false } } }, diff --git a/lib/CfTemplateGenerators/CodeDeploy.js b/lib/CfTemplateGenerators/CodeDeploy.js index c4c3a16..faae8f5 100644 --- a/lib/CfTemplateGenerators/CodeDeploy.js +++ b/lib/CfTemplateGenerators/CodeDeploy.js @@ -40,17 +40,17 @@ function buildFnDeploymentGroup ({ codeDeployAppName, codeDeployGroupName, codeD const lookupRole = { 'Fn::GetAtt': ['CodeDeployServiceRole', 'Arn'] } const roleArn = codeDeployRoleArn || lookupRole Object.assign(deploymentGroup.Properties, { ServiceRoleArn: roleArn }) - if (deploymentSettings.alarms) { - const alarmNames = deploymentSettings.alarms.map(a => { - const name = _.propOr({ Ref: a }, 'name', a) - return { Name: name } - }) - const alarmConfig = { - Alarms: alarmNames, - Enabled: true - } - Object.assign(deploymentGroup.Properties, { AlarmConfiguration: alarmConfig }) - } + const alarmNames = (deploymentSettings.alarms || []).map(a => { + const name = _.propOr({ Ref: a }, 'name', a) + return { Name: name } + }) + // Always set AlarmConfiguration: CodeDeploy treats an omitted alarmConfiguration + // on UpdateDeploymentGroup as "no change", so previously attached alarms would + // otherwise keep gating deployments after being removed from the settings. + const alarmConfig = alarmNames.length + ? { Alarms: alarmNames, Enabled: true } + : { Alarms: [], Enabled: false } + Object.assign(deploymentGroup.Properties, { AlarmConfiguration: alarmConfig }) if (deploymentSettings.triggerConfigurations) { Object.assign(deploymentGroup.Properties, { TriggerConfigurations: deploymentSettings.triggerConfigurations }) } diff --git a/lib/CfTemplateGenerators/CodeDeploy.test.js b/lib/CfTemplateGenerators/CodeDeploy.test.js index 2a4bd6d..76bf6b6 100644 --- a/lib/CfTemplateGenerators/CodeDeploy.test.js +++ b/lib/CfTemplateGenerators/CodeDeploy.test.js @@ -49,6 +49,10 @@ describe('CodeDeploy', () => { DeploymentStyle: { DeploymentType: 'BLUE_GREEN', DeploymentOption: 'WITH_TRAFFIC_CONTROL' + }, + AlarmConfiguration: { + Alarms: [], + Enabled: false } } } @@ -73,7 +77,7 @@ describe('CodeDeploy', () => { }) context('when no alarms were provided', () => { - it('should not include the AlarmConfiguration property', () => { + it('should explicitly disable the AlarmConfiguration so previously attached alarms are detached', () => { const deploymentSettings = { type: 'Linear10PercentEvery1Minute' } const expected = _.pipe( _.set('Properties.ApplicationName', { Ref: codeDeployAppName }), diff --git a/serverless-plugin-canary-deployments.test.js b/serverless-plugin-canary-deployments.test.js index fc16812..e8d16de 100644 --- a/serverless-plugin-canary-deployments.test.js +++ b/serverless-plugin-canary-deployments.test.js @@ -152,7 +152,7 @@ describe('ServerlessCanaryDeployments', () => { const deploymentGroup = resources.HelloLambdaFunctionDeploymentGroup expect(deploymentGroup).to.not.equal(undefined) - expect(deploymentGroup.Properties.AlarmConfiguration).to.equal(undefined) + expect(deploymentGroup.Properties.AlarmConfiguration).to.deep.equal({ Alarms: [], Enabled: false }) }) it('attaches composite alarm to all deployment groups when multiple functions have canaryAlarms', () => { @@ -236,9 +236,9 @@ describe('ServerlessCanaryDeployments', () => { const helloDeploymentGroup = resources.HelloLambdaFunctionDeploymentGroup expect(helloDeploymentGroup.Properties.AlarmConfiguration).to.not.equal(undefined) - // world does not have canaryAlarms - should NOT have AlarmConfiguration + // world does not have canaryAlarms - should have alarms explicitly disabled const worldDeploymentGroup = resources.WorldLambdaFunctionDeploymentGroup - expect(worldDeploymentGroup.Properties.AlarmConfiguration).to.equal(undefined) + expect(worldDeploymentGroup.Properties.AlarmConfiguration).to.deep.equal({ Alarms: [], Enabled: false }) }) it('preserves existing alarms when canaryAlarms is also configured', () => { From a5de5c53a55cafc0f142926eb7735d18f566d93c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 29 Sep 2026 01:43:27 +0200 Subject: [PATCH 2/2] chore: drop comment --- lib/CfTemplateGenerators/CodeDeploy.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/CfTemplateGenerators/CodeDeploy.js b/lib/CfTemplateGenerators/CodeDeploy.js index faae8f5..ee6315e 100644 --- a/lib/CfTemplateGenerators/CodeDeploy.js +++ b/lib/CfTemplateGenerators/CodeDeploy.js @@ -44,9 +44,6 @@ function buildFnDeploymentGroup ({ codeDeployAppName, codeDeployGroupName, codeD const name = _.propOr({ Ref: a }, 'name', a) return { Name: name } }) - // Always set AlarmConfiguration: CodeDeploy treats an omitted alarmConfiguration - // on UpdateDeploymentGroup as "no change", so previously attached alarms would - // otherwise keep gating deployments after being removed from the settings. const alarmConfig = alarmNames.length ? { Alarms: alarmNames, Enabled: true } : { Alarms: [], Enabled: false }