From 00a9825a07f63929b3f4c62380311a2c2255915a Mon Sep 17 00:00:00 2001 From: Orien Madgwick <497874+orien@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:37:25 +0700 Subject: [PATCH] Improve user feedback when CloudFormation reports no changes When CloudFormation returns 'no changes' errors during apply, provide a clearer explanation that while the template may have differences (e.g., whitespace, comments, formatting), no actual resource changes are needed and the stack is already in the desired state. This addresses the confusing scenario where users see a template diff but CloudFormation reports: 'The submitted information didn't contain changes. Submit different information to create a change set.' Changes: - Add user_friendly_changeset_error method to detect common 'no changes' error messages and provide helpful context - Update both create and update stack flows to use the improved message - Add comprehensive tests covering various CloudFormation error messages --- CHANGELOG.md | 2 ++ lib/stack_master/commands/apply.rb | 19 ++++++++++-- spec/stack_master/commands/apply_spec.rb | 37 ++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) 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