diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ccbe15..1d4407e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ The format is based on [Keep a Changelog], and this project adheres to ### Changed +- Improve message when CloudFormation claims there are no changes to apply, even when a template diff is present. ([#398]) - Display Tags diff (stack tags) in `stack_master diff` and `stack_master apply` commands. ([#397]) - Resolve style issues identified by RuboCop. ([#396]) [Unreleased]: https://github.com/envato/stack_master/compare/v2.17.1...HEAD [#396]: https://github.com/envato/stack_master/pull/396 [#397]: https://github.com/envato/stack_master/pull/397 +[#398]: https://github.com/envato/stack_master/pull/398 ## [2.17.1] - 2025-12-19 diff --git a/lib/stack_master/commands/apply.rb b/lib/stack_master/commands/apply.rb index 46c3438f..ad096952 100644 --- a/lib/stack_master/commands/apply.rb +++ b/lib/stack_master/commands/apply.rb @@ -89,7 +89,7 @@ def create_stack_by_change_set @change_set = ChangeSet.create(stack_options.merge(change_set_type: 'CREATE')) if @change_set.failed? ChangeSet.delete(@change_set.id) - halt!(@change_set.status_reason) + halt!(user_friendly_changeset_error(@change_set.status_reason)) end @change_set.display(StackMaster.stdout) @@ -123,7 +123,7 @@ def update_stack @change_set = ChangeSet.create(stack_options) if @change_set.failed? ChangeSet.delete(@change_set.id) - halt!(@change_set.status_reason) + halt!(user_friendly_changeset_error(@change_set.status_reason)) end @change_set.display(StackMaster.stdout) @@ -230,6 +230,21 @@ def set_stack_policy StackMaster.stdout.puts 'done.' end + def user_friendly_changeset_error(status_reason) + # CloudFormation returns various messages when there are no changes to apply + if status_reason =~ /didn'?t contain changes|no changes|no updates are to be performed/i + <<~MESSAGE.chomp + #{status_reason} + + While there may be differences in the template file (e.g., whitespace, comments, or + formatting), CloudFormation has determined that no actual resource changes are needed. + The stack is already in the desired state. + MESSAGE + else + status_reason + end + end + extend Forwardable def_delegators :@stack_definition, :stack_name, :region end diff --git a/spec/stack_master/commands/apply_spec.rb b/spec/stack_master/commands/apply_spec.rb index 9df35da9..1e1441f3 100644 --- a/spec/stack_master/commands/apply_spec.rb +++ b/spec/stack_master/commands/apply_spec.rb @@ -127,11 +127,42 @@ def apply before do allow(StackMaster::ChangeSet).to receive(:delete) allow(change_set).to receive(:failed?).and_return(true) - allow(change_set).to receive(:status_reason).and_return('reason') end - it 'outputs the status reason' do - expect { apply }.to output(/reason/).to_stdout + context 'with a generic error' do + before do + allow(change_set).to receive(:status_reason).and_return('reason') + end + + it 'outputs the status reason' do + expect { apply }.to output(/reason/).to_stdout + end + end + + context 'with a "no changes" error from CloudFormation' do + before do + allow(change_set) + .to receive(:status_reason) + .and_return("The submitted information didn't contain changes. " \ + 'Submit different information to create a change set.') + end + + it 'outputs a user-friendly explanation' do + expect { apply }.to output(/The submitted information didn't contain changes/).to_stdout + expect { apply }.to output(/no actual resource changes are needed/).to_stdout + expect { apply }.to output(/stack is already in the desired state/).to_stdout + end + end + + context 'with alternative "no changes" error message' do + before do + allow(change_set).to receive(:status_reason).and_return('No updates are to be performed.') + end + + it 'outputs a user-friendly explanation' do + expect { apply }.to output(/No updates are to be performed/).to_stdout + expect { apply }.to output(/no actual resource changes are needed/).to_stdout + end end end