From a556a7729bd7e812b0998793afebfa35ea5b9d59 Mon Sep 17 00:00:00 2001 From: Yaron Yarimi Date: Thu, 9 Apr 2026 22:40:37 +0300 Subject: [PATCH] Fix deploy hang with --verbose and disableRollback: true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both --verbose and disableRollback: true are set, the CLI hangs indefinitely after a CloudFormation resource failure. The monitor loop waits for ROLLBACK_COMPLETE, DELETE_FAILED, or DELETE_COMPLETE, but with rollback disabled the stack stays in UPDATE_FAILED or CREATE_FAILED — none of which matched the termination condition. Add CREATE_FAILED and UPDATE_FAILED to the status check so the CLI exits on failure regardless of whether rollback is enabled. Resolves oss-serverless/serverless#150 --- lib/plugins/aws/lib/monitor-stack.js | 7 +- .../lib/plugins/aws/lib/monitor-stack.test.js | 110 ++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/lib/plugins/aws/lib/monitor-stack.js b/lib/plugins/aws/lib/monitor-stack.js index 336e7fd946..389a184820 100644 --- a/lib/plugins/aws/lib/monitor-stack.js +++ b/lib/plugins/aws/lib/monitor-stack.js @@ -111,7 +111,12 @@ module.exports = { (!this.options.verbose || (stackStatus && (stackStatus.endsWith('ROLLBACK_COMPLETE') || - ['DELETE_FAILED', 'DELETE_COMPLETE'].includes(stackStatus)))) + [ + 'CREATE_FAILED', + 'UPDATE_FAILED', + 'DELETE_FAILED', + 'DELETE_COMPLETE', + ].includes(stackStatus)))) ) { const decoratedErrorMessage = `${stackLatestError.ResourceStatus}: ${ stackLatestError.LogicalResourceId diff --git a/test/unit/lib/plugins/aws/lib/monitor-stack.test.js b/test/unit/lib/plugins/aws/lib/monitor-stack.test.js index 1643bd93b3..43f9ecf228 100644 --- a/test/unit/lib/plugins/aws/lib/monitor-stack.test.js +++ b/test/unit/lib/plugins/aws/lib/monitor-stack.test.js @@ -459,6 +459,116 @@ describe('monitorStack', () => { }); }); + + it('should exit on failure with --verbose when stack status is UPDATE_FAILED (disableRollback)', () => { + awsPlugin.options.verbose = true; + const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); + const cfDataMock = { + StackId: 'new-service-dev', + }; + const updateStartEvent = { + StackEvents: [ + { + EventId: '1a2b3c4d', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'UPDATE_IN_PROGRESS', + }, + ], + }; + const resourceFailedEvent = { + StackEvents: [ + { + EventId: '1e2f3g4h', + StackName: 'new-service-dev', + LogicalResourceId: 'mochaApiGw', + ResourceType: 'AWS::ApiGateway::Deployment', + Timestamp: new Date(), + ResourceStatus: 'CREATE_FAILED', + ResourceStatusReason: 'Invalid stage identifier specified', + }, + ], + }; + const stackUpdateFailedEvent = { + StackEvents: [ + { + EventId: '1i2j3k4l', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'UPDATE_FAILED', + }, + ], + }; + describeStackEventsStub.onCall(0).resolves(updateStartEvent); + describeStackEventsStub.onCall(1).resolves(resourceFailedEvent); + describeStackEventsStub.onCall(2).resolves(stackUpdateFailedEvent); + + return awsPlugin.monitorStack('update', cfDataMock, { frequency: 10 }).catch((e) => { + if (e.name !== 'ServerlessError') throw e; + expect(e.message).to.include('mochaApiGw'); + expect(describeStackEventsStub.callCount).to.be.equal(3); + awsPlugin.provider.request.restore(); + }); + }); + + it('should exit on failure with --verbose when stack status is CREATE_FAILED (disableRollback)', () => { + awsPlugin.options.verbose = true; + const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); + const cfDataMock = { + StackId: 'new-service-dev', + }; + const createStartEvent = { + StackEvents: [ + { + EventId: '1a2b3c4d', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'CREATE_IN_PROGRESS', + }, + ], + }; + const resourceFailedEvent = { + StackEvents: [ + { + EventId: '1e2f3g4h', + StackName: 'new-service-dev', + LogicalResourceId: 'mochaLambda', + ResourceType: 'AWS::Lambda::Function', + Timestamp: new Date(), + ResourceStatus: 'CREATE_FAILED', + ResourceStatusReason: 'Resource creation cancelled', + }, + ], + }; + const stackCreateFailedEvent = { + StackEvents: [ + { + EventId: '1i2j3k4l', + StackName: 'new-service-dev', + LogicalResourceId: 'new-service-dev', + ResourceType: 'AWS::CloudFormation::Stack', + Timestamp: new Date(), + ResourceStatus: 'CREATE_FAILED', + }, + ], + }; + describeStackEventsStub.onCall(0).resolves(createStartEvent); + describeStackEventsStub.onCall(1).resolves(resourceFailedEvent); + describeStackEventsStub.onCall(2).resolves(stackCreateFailedEvent); + + return awsPlugin.monitorStack('create', cfDataMock, { frequency: 10 }).catch((e) => { + if (e.name !== 'ServerlessError') throw e; + expect(e.message).to.include('mochaLambda'); + expect(describeStackEventsStub.callCount).to.be.equal(3); + awsPlugin.provider.request.restore(); + }); + }); it('should keep monitoring when 1st ResourceType is not "AWS::CloudFormation::Stack"', async () => { const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request'); const cfDataMock = {