From e67c05ffa246d3611504e3ec10c5baea2fc9bb3d Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 11:32:56 +0100 Subject: [PATCH 01/65] Always use tags for indicating stages --- lib/epi_deploy/git_wrapper.rb | 13 ------------- lib/epi_deploy/release.rb | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index cd82af8..482bc20 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -54,19 +54,6 @@ def get_commit(git_reference) end end - def update_stage_tag_or_branch(stage, commit) - if EpiDeploy.use_tags_for_deploy - update_tag_commit(stage, commit) - else - update_branch_commit(stage, commit) - end - end - - def update_branch_commit(stage, commit) - Kernel.system "git branch -f #{stage} #{commit}" - self.push stage, force: true, tags: true - end - def update_tag_commit(stage, commit) Kernel.system "git push origin :refs/tags/#{stage}" git.add_tag(stage, commit, annotate: true, f: true, message: stage) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 86833c0..07808ed 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -41,7 +41,7 @@ def deploy!(environments) matches = environment.match(/\A(?[\w\-]+)(?:\.(?\w+))?\z/) # Force the tag/branch to the commit we want to deploy - git_wrapper.update_stage_tag_or_branch(matches[:stage], commit) + git_wrapper.update_tag_commit(matches[:stage], commit) completed = run_cap_deploy_to(environment) if !completed From d6a5d9e98c229368c3dc9a810cbc6756874e210a Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 11:33:37 +0100 Subject: [PATCH 02/65] Add deprecation warning to use_tags_for_deploy option --- lib/epi_deploy/config.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/epi_deploy/config.rb b/lib/epi_deploy/config.rb index f67e04d..04c5b80 100644 --- a/lib/epi_deploy/config.rb +++ b/lib/epi_deploy/config.rb @@ -6,6 +6,8 @@ def self.use_tags_for_deploy end def self.use_tags_for_deploy=(use_tags_for_deploy) + warn 'This option is now deprecated and will no effect on deployment' + warn 'Please remove this option from config/epi_deploy.rb as this may be removed in future releases' @@use_tags_for_deploy = use_tags_for_deploy end end \ No newline at end of file From 6aeed3e2582ed93ac55fc30cef561cc5c832dd47 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 11:49:42 +0100 Subject: [PATCH 03/65] Add test for GitWrapper.update_tag_commit --- spec/lib/epi_deploy/git_wrapper_spec.rb | 30 +++++-------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 019af7a..951b8d9 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -10,32 +10,12 @@ allow(subject).to receive(:git).and_return(mocked_git) end - describe '#update_stage_tag_or_branch' do + describe '#update_tag_commit' do + it 'adds a new tag for the stage to the commit' do + allow(Kernel).to receive(:system).and_return(true) + expect(mocked_git).to receive(:add_tag).with('production', 'main', any_args) - context 'when use_tags_for_deploy is set to true' do - before :each do - EpiDeploy.use_tags_for_deploy = true - end - - specify 'it calls update tag commit' do - allow(Kernel).to receive(:system).and_return(true) - expect(subject).to receive(:update_tag_commit) - - subject.update_stage_tag_or_branch('demo', '123') - end - end - - context 'when use_tags_for_deploy is set to false' do - before :each do - EpiDeploy.use_tags_for_deploy = false - end - - specify 'it calls update branch commit' do - allow(Kernel).to receive(:system).and_return(true) - expect(subject).to receive(:update_branch_commit) - - subject.update_stage_tag_or_branch('demo', '123') - end + subject.update_tag_commit 'production', 'main' end end From 23e94671f30e4e2a9f1ac524e97d922075182b66 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 12:03:38 +0100 Subject: [PATCH 04/65] Clean up comment --- lib/epi_deploy/release.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 07808ed..d08fad9 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -39,8 +39,6 @@ def deploy!(environments) git_wrapper.pull matches = environment.match(/\A(?[\w\-]+)(?:\.(?\w+))?\z/) - - # Force the tag/branch to the commit we want to deploy git_wrapper.update_tag_commit(matches[:stage], commit) completed = run_cap_deploy_to(environment) From 71e849258594c88b7b3b97a1c8eb9594c64b040c Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 12:19:06 +0100 Subject: [PATCH 05/65] Simplify deploy feature specs setup --- spec/features/deploy_spec.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/features/deploy_spec.rb b/spec/features/deploy_spec.rb index 6cc5ece..f39e9fd 100644 --- a/spec/features/deploy_spec.rb +++ b/spec/features/deploy_spec.rb @@ -3,10 +3,12 @@ describe "Deploy", type: :aruba do - it "errors if environment doesn't exist" do + before do setup_aruba_and_git run_command_and_stop 'bundle install --quiet' + end + it "errors if environment doesn't exist" do run_ed 'deploy invalidenvironment' expect(last_command_started).to have_exit_status(1) @@ -14,9 +16,6 @@ end it "errors if no latest release" do - setup_aruba_and_git - run_command_and_stop 'bundle install --quiet' - run_ed 'deploy production' expect(last_command_started).to have_exit_status(1) @@ -24,10 +23,8 @@ end it "deploys latest release" do - setup_aruba_and_git run_command_and_stop 'git tag -a example_tag -m "For testing"' # Create a pretend release run_command_and_stop 'git push' - run_command_and_stop 'bundle install --quiet' run_ed 'deploy production -r example_tag' From 0eb0432f771f7c231a33077a53c2323652e1dcc5 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 12:19:28 +0100 Subject: [PATCH 06/65] Add test for release to call update_tag_commit when deploying --- spec/lib/epi_deploy/release_spec.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index 8f6e879..61561b7 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -15,8 +15,8 @@ def commit(msg); end def tag(name); end def push(opts = {}); end def pull; end - def update_stage_tag_or_branch(branch, commit); end def current_branch; 'main'; end + def update_tag_commit(stage, commit); end end describe EpiDeploy::Release do @@ -103,6 +103,13 @@ def current_branch; 'main'; end end.to_not raise_error end end + + it 'updates the tag commit for the wrapper' do + allow(Kernel).to receive(:system).with('bundle exec cap production deploy target=test').and_return(true) + expect(git_wrapper).to receive(:update_tag_commit).with('production', nil) + + subject.deploy! ['production'] + end end end From de14ff38e280fc5f07240775481c6719c09902f5 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 14:42:25 +0100 Subject: [PATCH 07/65] Require 'byebug' for tests --- spec/spec_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a20a0fc..f83e281 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,7 @@ $: << File.expand_path('../../lib', __FILE__) +require 'byebug' + def run_ed(commands) run_command_and_stop "#{File.join(File.dirname(__FILE__), '../bin/epi_deploy')} #{commands}", fail_on_error: false end From 3aa34839b2a5ab0526be9212a630b740e3587ab3 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 15:23:02 +0100 Subject: [PATCH 08/65] Add method to delete branches both locally and remotely --- lib/epi_deploy/git_wrapper.rb | 17 +++++++ spec/lib/epi_deploy/git_wrapper_spec.rb | 65 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 482bc20..aaf3470 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -60,6 +60,17 @@ def update_tag_commit(stage, commit) Kernel.system "git push origin --tags" end + def delete_branches(*stages, delete_remote: true) + stages.each do |stage| + git.push('origin', "refs/heads/#{stage}", delete: true) + if local_branches.has_key? stage + branch_object = local_branches[stage] + branch_object.delete + local_branches.delete(stage) + end + end + end + def tag_list @tag_list ||= `git for-each-ref --sort=taggerdate --format '%(tag)' refs/tags`.gsub("'", '').split.reverse end @@ -74,5 +85,11 @@ def git @git ||= ::Git.open(Dir.pwd) end + def local_branches + @branches ||= git.branches.local.map do |branch| + [branch.name, branch] + end.compact.to_h + end + end end diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 951b8d9..1977fea 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -52,4 +52,69 @@ end end + describe '#delete_branches' do + let(:stages) { ['production', 'demo'] } + let(:local_branches) { [] } + + before do + allow(subject).to receive(:local_branches).and_return(local_branches) + end + + context 'if all the deployment stages exist as local branches' do + let(:production_branch) { double('production branch') } + let(:demo_branch) { double('demo branch') } + let(:local_branches) { + { + 'production' => production_branch, + 'demo' => demo_branch, + } + } + + specify 'it deletes each stage branch from the remote' do + allow(production_branch).to receive(:delete) + allow(demo_branch).to receive(:delete) + + expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) + expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) + + subject.delete_branches(*stages) + end + + specify 'it deletes each branch locally' do + expect(local_branches).to receive(:[]).with('production').and_call_original + expect(local_branches).to receive(:[]).with('demo').and_call_original + expect(production_branch).to receive(:delete) + expect(demo_branch).to receive(:delete) + + subject.delete_branches(*stages) + end + end + + context 'if not all the deployment stages exist as local branches' do + let(:production_branch) { double('production branch') } + let(:local_branches) { + { + 'production' => production_branch, + } + } + + specify 'it deletes each stage branch from the remote' do + allow(production_branch).to receive(:delete) + + expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) + expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) + + subject.delete_branches(*stages) + end + + specify 'it deletes only the branches that exist locally' do + expect(local_branches).to receive(:[]).with('production').and_call_original + expect(local_branches).to_not receive(:[]).with('demo') + expect(production_branch).to receive(:delete) + + subject.delete_branches(*stages) + end + end + end + end From 0c6a24649b893562da1185f5d98dafb5448ef149 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 15:24:24 +0100 Subject: [PATCH 09/65] Add method on git wrapper to delete branches both locally and remotely --- lib/epi_deploy/git_wrapper.rb | 4 ++-- spec/lib/epi_deploy/git_wrapper_spec.rb | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index aaf3470..9d3b947 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -60,8 +60,8 @@ def update_tag_commit(stage, commit) Kernel.system "git push origin --tags" end - def delete_branches(*stages, delete_remote: true) - stages.each do |stage| + def delete_branches(*branch, delete_remote: true) + branch.each do |stage| git.push('origin', "refs/heads/#{stage}", delete: true) if local_branches.has_key? stage branch_object = local_branches[stage] diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 1977fea..806b2bc 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -53,14 +53,14 @@ end describe '#delete_branches' do - let(:stages) { ['production', 'demo'] } + let(:branches) { ['production', 'demo'] } let(:local_branches) { [] } before do allow(subject).to receive(:local_branches).and_return(local_branches) end - context 'if all the deployment stages exist as local branches' do + context 'if all the branches exist as local branches' do let(:production_branch) { double('production branch') } let(:demo_branch) { double('demo branch') } let(:local_branches) { @@ -70,14 +70,14 @@ } } - specify 'it deletes each stage branch from the remote' do + specify 'it deletes each branch from the remote' do allow(production_branch).to receive(:delete) allow(demo_branch).to receive(:delete) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) - subject.delete_branches(*stages) + subject.delete_branches(*branches) end specify 'it deletes each branch locally' do @@ -86,11 +86,11 @@ expect(production_branch).to receive(:delete) expect(demo_branch).to receive(:delete) - subject.delete_branches(*stages) + subject.delete_branches(*branches) end end - context 'if not all the deployment stages exist as local branches' do + context 'if not all the branches exist as local branches' do let(:production_branch) { double('production branch') } let(:local_branches) { { @@ -98,13 +98,13 @@ } } - specify 'it deletes each stage branch from the remote' do + specify 'it deletes each branch from the remote' do allow(production_branch).to receive(:delete) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) - subject.delete_branches(*stages) + subject.delete_branches(*branches) end specify 'it deletes only the branches that exist locally' do @@ -112,7 +112,7 @@ expect(local_branches).to_not receive(:[]).with('demo') expect(production_branch).to receive(:delete) - subject.delete_branches(*stages) + subject.delete_branches(*branches) end end end From 8207b142561dd223e97f6944397bf102373f0db5 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 16:19:16 +0100 Subject: [PATCH 10/65] Change GitWrapper#delete_branches to accept an array of branch names --- lib/epi_deploy/git_wrapper.rb | 12 ++++++------ spec/lib/epi_deploy/git_wrapper_spec.rb | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 9d3b947..f29c6c7 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -60,13 +60,13 @@ def update_tag_commit(stage, commit) Kernel.system "git push origin --tags" end - def delete_branches(*branch, delete_remote: true) - branch.each do |stage| - git.push('origin', "refs/heads/#{stage}", delete: true) - if local_branches.has_key? stage - branch_object = local_branches[stage] + def delete_branches(branches) + branches.each do |branch| + git.push('origin', "refs/heads/#{branch}", delete: true) + if local_branches.has_key? branch + branch_object = local_branches[branch] branch_object.delete - local_branches.delete(stage) + local_branches.delete(branch) end end end diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 806b2bc..75f2942 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -77,7 +77,7 @@ expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) - subject.delete_branches(*branches) + subject.delete_branches(branches) end specify 'it deletes each branch locally' do @@ -86,7 +86,7 @@ expect(production_branch).to receive(:delete) expect(demo_branch).to receive(:delete) - subject.delete_branches(*branches) + subject.delete_branches(branches) end end @@ -104,7 +104,7 @@ expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) - subject.delete_branches(*branches) + subject.delete_branches(branches) end specify 'it deletes only the branches that exist locally' do @@ -112,7 +112,7 @@ expect(local_branches).to_not receive(:[]).with('demo') expect(production_branch).to receive(:delete) - subject.delete_branches(*branches) + subject.delete_branches(branches) end end end From 7779af71d7b29bb829737d22eeb8b464d6612fbe Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 16:42:01 +0100 Subject: [PATCH 11/65] Add #environments method to stages extractor to get a list of all deployment environments and refactor environment matching to stages extractor --- lib/epi_deploy/release.rb | 2 +- lib/epi_deploy/stages_extractor.rb | 24 +++++++++++++++----- spec/lib/epi_deploy/stages_extractor_spec.rb | 20 ++++++++++++++-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index d08fad9..0cc63f8 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -38,7 +38,7 @@ def deploy!(environments) begin git_wrapper.pull - matches = environment.match(/\A(?[\w\-]+)(?:\.(?\w+))?\z/) + matches = StagesExtractor.match_with(environment) git_wrapper.update_tag_commit(matches[:stage], commit) completed = run_cap_deploy_to(environment) diff --git a/lib/epi_deploy/stages_extractor.rb b/lib/epi_deploy/stages_extractor.rb index 108fbf9..09b32d2 100644 --- a/lib/epi_deploy/stages_extractor.rb +++ b/lib/epi_deploy/stages_extractor.rb @@ -1,13 +1,18 @@ require 'set' + module EpiDeploy class StagesExtractor - attr_accessor :multi_customer_stages, :all_stages + STAGE_REGEX = /\A(?[\w\-]+)(?:\.(?\w+))?\z/ + DEPLOY_FILE_REGEX = /\A(?[\w\-]+)(?:\.(?[\w\-]+))?\.rb\z/ + + attr_accessor :multi_customer_stages, :all_stages, :environments def initialize self.multi_customer_stages = Set.new self.all_stages = Set.new - + self.environments = Set.new + stage_config_files.each do |stage_config_file_name| - matches = stage_config_file_name.match /(?[\w\-]+)(?:\.(?[\w\-]+))?\.rb/ + matches = stage_config_file_name.match(DEPLOY_FILE_REGEX) if matches[:customer] multi_customer_stages << matches[:stage] @@ -15,6 +20,8 @@ def initialize else all_stages << matches[:stage] end + + environments << matches[:stage] end end @@ -25,11 +32,16 @@ def multi_customer_stage?(stage) def valid_stage?(stage) all_stages.include?(stage) || multi_customer_stage?(stage) end - + + def self.match_with(environment) + environment.match(STAGE_REGEX) + end + private def stage_config_files - @stage_config_files ||= Dir.chdir(File.join(Dir.pwd, 'config', 'deploy')) do - Dir.glob("*.rb") + @stage_config_files ||= begin + glob_pattern = File.join(Dir.pwd, 'config', 'deploy', '*.rb') + Dir[glob_pattern].map { |path| File.basename(path) } end end end diff --git a/spec/lib/epi_deploy/stages_extractor_spec.rb b/spec/lib/epi_deploy/stages_extractor_spec.rb index 96201a4..f5e785f 100644 --- a/spec/lib/epi_deploy/stages_extractor_spec.rb +++ b/spec/lib/epi_deploy/stages_extractor_spec.rb @@ -36,7 +36,23 @@ specify "a stage without a config file is not valid" do expect(subject.valid_stage?('qa')).to be false end - end - + + describe '#environments' do + specify 'it returns a list of environments' do + expect(subject.environments).to match_array ['production', 'demo'] + end + end + + describe '#all_stages' do + specify 'it returns of all deployment stages' do + expect(subject.all_stages).to match_array ['production.epigenesys', 'production.genesys', 'demo'] + end + end + + describe '#multi_customer_stages' do + specify 'it returns only stages that have multiple customers for the same environment' do + expect(subject.all_stages).to match_array ['production.epigenesys', 'production.genesys', 'demo'] + end + end end \ No newline at end of file From 9d52173cc08b73e30a3195a4ba68d6e8dddb0f8a Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 16:52:34 +0100 Subject: [PATCH 12/65] Delete legacy environment branches locally and on remote when deploying --- lib/epi_deploy/release.rb | 3 +++ spec/lib/epi_deploy/release_spec.rb | 34 ++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 0cc63f8..1d24d3d 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -38,6 +38,9 @@ def deploy!(environments) begin git_wrapper.pull + # Remove legacy environment branches in the local repo and remotely + git_wrapper.delete_branches(stages_extractor.environments) + matches = StagesExtractor.match_with(environment) git_wrapper.update_tag_commit(matches[:stage], commit) diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index 61561b7..e19707b 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -17,6 +17,7 @@ def push(opts = {}); end def pull; end def current_branch; 'main'; end def update_tag_commit(stage, commit); end + def delete_branches(branches); end end describe EpiDeploy::Release do @@ -90,26 +91,39 @@ def update_tag_commit(stage, commit); end end describe "#deploy!" do - it "runs the capistrano deploy command for each of the environments given" do + before do + # Suppress output from epiDeploy + allow_any_instance_of(IO).to receive(:puts) + end + + around do |example| Dir.chdir(File.join(File.dirname(__FILE__), '../..', 'fixtures')) do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + example.run + end + end - expect do - # Suppress output from epiDeploy - allow_any_instance_of(IO).to receive(:puts) + it "runs the capistrano deploy command for each of the environments given" do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) - subject.deploy! %w(demo production) - end.to_not raise_error - end + expect do + subject.deploy! %w(demo production) + end.to_not raise_error end it 'updates the tag commit for the wrapper' do - allow(Kernel).to receive(:system).with('bundle exec cap production deploy target=test').and_return(true) + allow(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) expect(git_wrapper).to receive(:update_tag_commit).with('production', nil) subject.deploy! ['production'] end + + it 'deletes branches for all deployment environments' do + allow(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) + + subject.deploy! ['production'] + end end end From b703faddf2aabb1aac0952b1d78eed486de16de7 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 16:58:48 +0100 Subject: [PATCH 13/65] Improve use_tags_for_deploy option deprecation warnng --- lib/epi_deploy/config.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/epi_deploy/config.rb b/lib/epi_deploy/config.rb index 04c5b80..60b0586 100644 --- a/lib/epi_deploy/config.rb +++ b/lib/epi_deploy/config.rb @@ -6,8 +6,7 @@ def self.use_tags_for_deploy end def self.use_tags_for_deploy=(use_tags_for_deploy) - warn 'This option is now deprecated and will no effect on deployment' - warn 'Please remove this option from config/epi_deploy.rb as this may be removed in future releases' + warn '[Deprecation Warning] The use_tags_for_deploy option is now obsolete. Remove this from your configuration.' @@use_tags_for_deploy = use_tags_for_deploy end end \ No newline at end of file From 2a3b59358601d61043c7e9b604abeed25d6b3a8e Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 30 Sep 2024 17:01:52 +0100 Subject: [PATCH 14/65] Add Ruby 3.1, 3.2 and 3.3 to test workflow --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 5431d61..9195486 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.6', '2.7', '3.0'] + ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3'] steps: - uses: actions/checkout@v2 From 1d80515603a72f74e1033ebb0fb31904f1c78aba Mon Sep 17 00:00:00 2001 From: William Lee Date: Tue, 1 Oct 2024 10:52:35 +0100 Subject: [PATCH 15/65] Rename method for creating/updating tags and update implementation to use git library methods --- lib/epi_deploy/git_wrapper.rb | 8 ++++---- lib/epi_deploy/release.rb | 2 +- spec/lib/epi_deploy/git_wrapper_spec.rb | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index f29c6c7..ee32f92 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -54,10 +54,10 @@ def get_commit(git_reference) end end - def update_tag_commit(stage, commit) - Kernel.system "git push origin :refs/tags/#{stage}" - git.add_tag(stage, commit, annotate: true, f: true, message: stage) - Kernel.system "git push origin --tags" + def create_or_update_tag(name, commit) + git.push('origin', "refs/tags/#{name}", delete: true) + git.add_tag(name, commit, annotate: true, f: true, message: name) + git.push('origin', name) end def delete_branches(branches) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 1d24d3d..718026d 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -42,7 +42,7 @@ def deploy!(environments) git_wrapper.delete_branches(stages_extractor.environments) matches = StagesExtractor.match_with(environment) - git_wrapper.update_tag_commit(matches[:stage], commit) + git_wrapper.create_or_update_tag(matches[:stage], commit) completed = run_cap_deploy_to(environment) if !completed diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 75f2942..59a5910 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -10,12 +10,13 @@ allow(subject).to receive(:git).and_return(mocked_git) end - describe '#update_tag_commit' do + describe '#create_or_update_tag' do it 'adds a new tag for the stage to the commit' do - allow(Kernel).to receive(:system).and_return(true) + expect(mocked_git).to receive(:push).with('origin', 'refs/tags/production', delete: true) expect(mocked_git).to receive(:add_tag).with('production', 'main', any_args) + expect(mocked_git).to receive(:push).with('origin', 'production') - subject.update_tag_commit 'production', 'main' + subject.create_or_update_tag 'production', 'main' end end From 8fe2721404b67be4aa83e2ab033018a05019222d Mon Sep 17 00:00:00 2001 From: William Lee Date: Tue, 1 Oct 2024 16:41:31 +0100 Subject: [PATCH 16/65] Add StagesExtractor#stages_for_environment to get deployable stages for each environment --- lib/epi_deploy/stages_extractor.rb | 32 +++++++++++++++----- spec/fixtures/config/deploy/production.rb | 0 spec/lib/epi_deploy/stages_extractor_spec.rb | 9 +++++- 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 spec/fixtures/config/deploy/production.rb diff --git a/lib/epi_deploy/stages_extractor.rb b/lib/epi_deploy/stages_extractor.rb index 09b32d2..13a6a7a 100644 --- a/lib/epi_deploy/stages_extractor.rb +++ b/lib/epi_deploy/stages_extractor.rb @@ -5,24 +5,32 @@ class StagesExtractor STAGE_REGEX = /\A(?[\w\-]+)(?:\.(?\w+))?\z/ DEPLOY_FILE_REGEX = /\A(?[\w\-]+)(?:\.(?[\w\-]+))?\.rb\z/ - attr_accessor :multi_customer_stages, :all_stages, :environments + attr_accessor :multi_customer_stages, :all_stages def initialize self.multi_customer_stages = Set.new - self.all_stages = Set.new - self.environments = Set.new + @environment_to_stages = {} stage_config_files.each do |stage_config_file_name| matches = stage_config_file_name.match(DEPLOY_FILE_REGEX) - + if matches[:customer] multi_customer_stages << matches[:stage] - all_stages << "#{matches[:stage]}.#{matches[:customer]}" - else - all_stages << matches[:stage] end - environments << matches[:stage] + @environment_to_stages[matches[:stage]] ||= Set.new + @environment_to_stages[matches[:stage]] << matches[1..2].compact.join('.') end + + # Remove stage with same name as environment if it's a multi-customer stage + # e.g. removes production from the set of production stages + # if production has production.epigenesys and production.genesys + multi_customer_stages.each do |environment| + if @environment_to_stages.has_key? environment + @environment_to_stages[environment].delete(environment) + end + end + + self.all_stages = Set.new(@environment_to_stages.values.map(&:to_a).flatten) end def multi_customer_stage?(stage) @@ -33,6 +41,14 @@ def valid_stage?(stage) all_stages.include?(stage) || multi_customer_stage?(stage) end + def stages_for_environment(environment) + @environment_to_stages[environment] || Set.new + end + + def environments + @environment_to_stages.keys + end + def self.match_with(environment) environment.match(STAGE_REGEX) end diff --git a/spec/fixtures/config/deploy/production.rb b/spec/fixtures/config/deploy/production.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/lib/epi_deploy/stages_extractor_spec.rb b/spec/lib/epi_deploy/stages_extractor_spec.rb index f5e785f..3eb399a 100644 --- a/spec/lib/epi_deploy/stages_extractor_spec.rb +++ b/spec/lib/epi_deploy/stages_extractor_spec.rb @@ -52,7 +52,14 @@ describe '#multi_customer_stages' do specify 'it returns only stages that have multiple customers for the same environment' do - expect(subject.all_stages).to match_array ['production.epigenesys', 'production.genesys', 'demo'] + expect(subject.multi_customer_stages).to match_array ['production'] + end + end + + describe '#stages_for_environment' do + specify 'it returns the deployable stages for an environment' do + expect(subject.stages_for_environment('production')).to match_array ['production.epigenesys', 'production.genesys'] + expect(subject.stages_for_environment('demo')).to match_array ['demo'] end end end \ No newline at end of file From 97085886027e2cbba294a746e952d6bb791794f5 Mon Sep 17 00:00:00 2001 From: William Lee Date: Tue, 1 Oct 2024 16:55:58 +0100 Subject: [PATCH 17/65] Update stages extractor so that either a valid stage or environment can be passed to find the stages --- lib/epi_deploy/stages_extractor.rb | 12 ++++++++++-- spec/lib/epi_deploy/stages_extractor_spec.rb | 15 +++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/epi_deploy/stages_extractor.rb b/lib/epi_deploy/stages_extractor.rb index 13a6a7a..0fd4c7d 100644 --- a/lib/epi_deploy/stages_extractor.rb +++ b/lib/epi_deploy/stages_extractor.rb @@ -41,8 +41,16 @@ def valid_stage?(stage) all_stages.include?(stage) || multi_customer_stage?(stage) end - def stages_for_environment(environment) - @environment_to_stages[environment] || Set.new + def stages_for_stage_or_environment(stage_or_environment) + if @environment_to_stages.has_key? stage_or_environment + # Environment + @environment_to_stages[stage_or_environment] + elsif self.all_stages.include? stage_or_environment + # Stage + [stage_or_environment] + else + [] + end end def environments diff --git a/spec/lib/epi_deploy/stages_extractor_spec.rb b/spec/lib/epi_deploy/stages_extractor_spec.rb index 3eb399a..c3a59cd 100644 --- a/spec/lib/epi_deploy/stages_extractor_spec.rb +++ b/spec/lib/epi_deploy/stages_extractor_spec.rb @@ -56,10 +56,17 @@ end end - describe '#stages_for_environment' do - specify 'it returns the deployable stages for an environment' do - expect(subject.stages_for_environment('production')).to match_array ['production.epigenesys', 'production.genesys'] - expect(subject.stages_for_environment('demo')).to match_array ['demo'] + describe '#stages_for_stage_or_environment' do + specify 'it returns the deployable stages for a multi-customer environment' do + expect(subject.stages_for_stage_or_environment('production')).to match_array ['production.epigenesys', 'production.genesys'] + end + + specify 'it returns the deployment stage itself for a single customer environment' do + expect(subject.stages_for_stage_or_environment('demo')).to match_array ['demo'] + end + + specify 'it returns the deployment stage for a fully qualified stage in a multi-customer environment' do + expect(subject.stages_for_stage_or_environment('production.epigenesys')).to match_array ['production.epigenesys'] end end end \ No newline at end of file From 946d757843f5bb7669aa8319afb8b59c903123c3 Mon Sep 17 00:00:00 2001 From: William Lee Date: Tue, 1 Oct 2024 16:59:53 +0100 Subject: [PATCH 18/65] Update Release.deploy! so that each deployment to a stage is done within the library rather than using the deploy_all task for multi-user environments --- lib/epi_deploy/release.rb | 31 ++++++++++++++++---------- spec/lib/epi_deploy/release_spec.rb | 34 +++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 718026d..4a13a64 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -33,20 +33,24 @@ def version app_version.version end - def deploy!(environments) - environments.each do |environment| - begin - git_wrapper.pull + def deploy!(stages_or_environments) + begin + git_wrapper.pull - # Remove legacy environment branches in the local repo and remotely - git_wrapper.delete_branches(stages_extractor.environments) + # Remove legacy environment branches in the local repo and remotely + print_notice 'Removing any legacy deployment branches' + git_wrapper.delete_branches(stages_extractor.environments) - matches = StagesExtractor.match_with(environment) - git_wrapper.create_or_update_tag(matches[:stage], commit) + stages_or_environments.each do |stage_or_environment| + stages_extractor.stages_for_stage_or_environment(stage_or_environment).each do |stage| + tag_name = tag_name_for_stage(stage) + git_wrapper.create_or_update_tag(tag_name, commit) + print_success "Created deployment tag #{tag_name} on commit #{commit}" - completed = run_cap_deploy_to(environment) - if !completed - print_failure_and_abort "Deployment failed - please review output before deploying again" + completed = run_cap_deploy_to(stage) + if !completed + print_failure_and_abort "Deployment failed - please review output before deploying again" + end end rescue ::Git::GitExecuteError => e print_failure_and_abort "A git error occurred: #{e.message}" @@ -99,5 +103,10 @@ def stages_extractor @stages_extractor ||= StagesExtractor.new end + def tag_name_for_stage(stage) + timestamp = Time.now.strftime('%Y_%m_%d-%H_%M_%S') + "#{stage}-#{timestamp}" + end + end end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index e19707b..ed216a1 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -1,3 +1,5 @@ +require 'time' + require 'spec_helper' require 'epi_deploy/stages_extractor' require 'epi_deploy/release' @@ -16,7 +18,7 @@ def tag(name); end def push(opts = {}); end def pull; end def current_branch; 'main'; end - def update_tag_commit(stage, commit); end + def create_or_update_tag(stage, commit); end def delete_branches(branches); end end @@ -24,7 +26,7 @@ def delete_branches(branches); end let(:git_wrapper) { MockGit.new } before do - allow(subject).to receive_messages(reference: 'test', git_wrapper: git_wrapper, app_version: double(bump!: 42, version_file_path: '')) + allow(subject).to receive_messages(reference: 'test', git_wrapper: git_wrapper, commit: 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b', app_version: double(bump!: 42, version_file_path: '')) end describe "#create!" do @@ -92,7 +94,6 @@ def delete_branches(branches); end describe "#deploy!" do before do - # Suppress output from epiDeploy allow_any_instance_of(IO).to receive(:puts) end @@ -104,22 +105,37 @@ def delete_branches(branches); end it "runs the capistrano deploy command for each of the environments given" do expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) expect do subject.deploy! %w(demo production) end.to_not raise_error end - it 'updates the tag commit for the wrapper' do - allow(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) - expect(git_wrapper).to receive(:update_tag_commit).with('production', nil) + it 'adds a tag with the deployment stage and timestamp' do + def deployment_stage_with_timestamp(stage) + satisfy do |tag_name| + tag_stage, timestamp = tag_name.split('-', 2) + tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) + end + end - subject.deploy! ['production'] + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) + + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] end it 'deletes branches for all deployment environments' do - allow(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) subject.deploy! ['production'] From d05f7c5b552fc11a5d148471e35fd33c8041dd68 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 09:46:19 +0100 Subject: [PATCH 19/65] Stub output helper methods instead of IO.puts to avoid silencing test output --- lib/epi_deploy/release.rb | 2 +- spec/lib/epi_deploy/release_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 4a13a64..4cc3c69 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -88,7 +88,7 @@ def date_and_time_for_tag(time_class = (Time.respond_to?(:zone) ? Time.zone : Ti end def run_cap_deploy_to(environment) - $stdout.puts "Deploying to #{environment}... " + print_notice "Deploying to #{environment}... " task_to_run = if stages_extractor.multi_customer_stage?(environment) "deploy_all" diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index ed216a1..cf3d186 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -94,7 +94,7 @@ def delete_branches(branches); end describe "#deploy!" do before do - allow_any_instance_of(IO).to receive(:puts) + allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil, print_failure_and_abort: nil) end around do |example| From 3235f889fc73b09c970800a5ceabc1b5597e0298 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 09:51:05 +0100 Subject: [PATCH 20/65] Move creation of deployment tags so that a tag for each stage is only created when successfully deployed --- lib/epi_deploy/release.rb | 9 ++++--- spec/lib/epi_deploy/release_spec.rb | 42 ++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 4cc3c69..2c28d2a 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -44,11 +44,12 @@ def deploy!(stages_or_environments) stages_or_environments.each do |stage_or_environment| stages_extractor.stages_for_stage_or_environment(stage_or_environment).each do |stage| tag_name = tag_name_for_stage(stage) - git_wrapper.create_or_update_tag(tag_name, commit) - print_success "Created deployment tag #{tag_name} on commit #{commit}" - + completed = run_cap_deploy_to(stage) - if !completed + if completed + git_wrapper.create_or_update_tag(tag_name, commit) + print_success "Created deployment tag #{tag_name} on commit #{commit}" + else print_failure_and_abort "Deployment failed - please review output before deploying again" end end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index cf3d186..cae0596 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -22,6 +22,13 @@ def create_or_update_tag(stage, commit); end def delete_branches(branches); end end +def deployment_stage_with_timestamp(stage) + satisfy do |tag_name| + tag_stage, timestamp = tag_name.split('-', 2) + tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) + end +end + describe EpiDeploy::Release do let(:git_wrapper) { MockGit.new } @@ -113,23 +120,32 @@ def delete_branches(branches); end end.to_not raise_error end - it 'adds a tag with the deployment stage and timestamp' do - def deployment_stage_with_timestamp(stage) - satisfy do |tag_name| - tag_stage, timestamp = tag_name.split('-', 2) - tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) - end + context 'if deployment to all stages is successful' do + it 'adds a tag for all deployment stages with the name of the stage and timestamp' do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) + + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] end + end - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + context 'if deployment to some stages is unsuccessful' do + it 'only adds the tag to the deployment stages have succeeded' do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) + expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) - subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + end end it 'deletes branches for all deployment environments' do From d83601bf91740a518eb3044574552eea48c27500 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 10:45:44 +0100 Subject: [PATCH 21/65] Add use_timestamped_deploy_tags option for configuring use of new tag style --- lib/epi_deploy/config.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/epi_deploy/config.rb b/lib/epi_deploy/config.rb index 60b0586..d31f997 100644 --- a/lib/epi_deploy/config.rb +++ b/lib/epi_deploy/config.rb @@ -1,12 +1,14 @@ module EpiDeploy - @@use_tags_for_deploy = false + @use_tags_for_deploy = false + @use_timestamped_deploy_tags = false - def self.use_tags_for_deploy - @@use_tags_for_deploy - end + class << self + attr_reader :use_tags_for_deploy + attr_accessor :use_timestamped_deploy_tags - def self.use_tags_for_deploy=(use_tags_for_deploy) - warn '[Deprecation Warning] The use_tags_for_deploy option is now obsolete. Remove this from your configuration.' - @@use_tags_for_deploy = use_tags_for_deploy - end + def use_tags_for_deploy=(use_tags_for_deploy) + warn '[Deprecation Warning] The use_tags_for_deploy option is now obsolete. Remove this from your configuration.' + @use_tags_for_deploy = use_tags_for_deploy + end + end end \ No newline at end of file From 21576b36303f98a4cdaa9bcc67ef9dbf54e98d25 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 10:50:06 +0100 Subject: [PATCH 22/65] Fix position of rescue clause --- lib/epi_deploy/release.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 2c28d2a..ca4f508 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -53,9 +53,9 @@ def deploy!(stages_or_environments) print_failure_and_abort "Deployment failed - please review output before deploying again" end end - rescue ::Git::GitExecuteError => e - print_failure_and_abort "A git error occurred: #{e.message}" end + rescue ::Git::GitExecuteError => e + print_failure_and_abort "A git error occurred: #{e.message}" end end From e9ba734c3a9c19b24f16ff7cb5a3a0494ba3ea5d Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 11:57:36 +0100 Subject: [PATCH 23/65] Refactor release deployment logic to a deployer class --- lib/epi_deploy/command.rb | 3 +- lib/epi_deploy/deployer.rb | 67 ++++++++++++++++++ lib/epi_deploy/release.rb | 47 ------------- spec/lib/epi_deploy/command_spec.rb | 42 ++++++----- spec/lib/epi_deploy/deployer_spec.rb | 100 +++++++++++++++++++++++++++ spec/lib/epi_deploy/release_spec.rb | 67 ------------------ 6 files changed, 193 insertions(+), 133 deletions(-) create mode 100644 lib/epi_deploy/deployer.rb create mode 100644 spec/lib/epi_deploy/deployer_spec.rb diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index cc439f9..9bc62f6 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -32,7 +32,8 @@ def deploy(environments = self.args) if release.nil? print_failure_and_abort "You did not enter a valid Git reference. Please try again." else - if release.deploy!(environments) + deployer = Deployer.new(release) + if deployer.deploy!(environments) print_success "Deployment complete." else print_failure_and_abort "An error occurred." diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb new file mode 100644 index 0000000..03cde3d --- /dev/null +++ b/lib/epi_deploy/deployer.rb @@ -0,0 +1,67 @@ +require 'git' + +require_relative 'helpers' +require_relative 'stages_extractor' + +module EpiDeploy + class Deployer + include Helpers + + def initialize(release, git_wrapper: nil) + @release = release + end + + def deploy!(stages_or_environments) + begin + git_wrapper.pull + + # Remove legacy environment branches in the local repo and remotely + print_notice 'Removing any legacy deployment branches' + git_wrapper.delete_branches(stages_extractor.environments) + + stages_or_environments.each do |stage_or_environment| + stages_extractor.stages_for_stage_or_environment(stage_or_environment).each do |stage| + tag_name = tag_name_for_stage(stage) + + completed = run_cap_deploy_to(stage) + if completed + git_wrapper.create_or_update_tag(tag_name, @release.commit) + print_success "Created deployment tag #{tag_name} on commit #{@release.commit}" + else + print_failure_and_abort "Deployment failed - please review output before deploying again" + end + end + end + rescue ::Git::GitExecuteError => e + print_failure_and_abort "A git error occurred: #{e.message}" + end + end + + private + + def git_wrapper + @git_wrapper ||= GitWrapper.new + end + + def stages_extractor + @stages_extractor ||= StagesExtractor.new + end + + def tag_name_for_stage(stage) + timestamp = Time.now.strftime('%Y_%m_%d-%H_%M_%S') + "#{stage}-#{timestamp}" + end + + def run_cap_deploy_to(environment) + print_notice "Deploying to #{environment}... " + + task_to_run = if stages_extractor.multi_customer_stage?(environment) + "deploy_all" + else + "deploy" + end + + Kernel.system "bundle exec cap #{environment} #{task_to_run} target=#{@release.reference}" + end + end +end \ No newline at end of file diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index ca4f508..99e40d3 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -33,32 +33,6 @@ def version app_version.version end - def deploy!(stages_or_environments) - begin - git_wrapper.pull - - # Remove legacy environment branches in the local repo and remotely - print_notice 'Removing any legacy deployment branches' - git_wrapper.delete_branches(stages_extractor.environments) - - stages_or_environments.each do |stage_or_environment| - stages_extractor.stages_for_stage_or_environment(stage_or_environment).each do |stage| - tag_name = tag_name_for_stage(stage) - - completed = run_cap_deploy_to(stage) - if completed - git_wrapper.create_or_update_tag(tag_name, commit) - print_success "Created deployment tag #{tag_name} on commit #{commit}" - else - print_failure_and_abort "Deployment failed - please review output before deploying again" - end - end - end - rescue ::Git::GitExecuteError => e - print_failure_and_abort "A git error occurred: #{e.message}" - end - end - def tag_list(options = nil) git_wrapper.tag_list(options) end @@ -88,26 +62,5 @@ def date_and_time_for_tag(time_class = (Time.respond_to?(:zone) ? Time.zone : Ti time.strftime "%Y#{MONTHS[time.month - 1]}%d-%H%M" end - def run_cap_deploy_to(environment) - print_notice "Deploying to #{environment}... " - - task_to_run = if stages_extractor.multi_customer_stage?(environment) - "deploy_all" - else - "deploy" - end - - Kernel.system "bundle exec cap #{environment} #{task_to_run} target=#{reference}" - end - - def stages_extractor - @stages_extractor ||= StagesExtractor.new - end - - def tag_name_for_stage(stage) - timestamp = Time.now.strftime('%Y_%m_%d-%H_%M_%S') - "#{stage}-#{timestamp}" - end - end end diff --git a/spec/lib/epi_deploy/command_spec.rb b/spec/lib/epi_deploy/command_spec.rb index fc06d1f..638da75 100644 --- a/spec/lib/epi_deploy/command_spec.rb +++ b/spec/lib/epi_deploy/command_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'epi_deploy/stages_extractor' require 'epi_deploy/command' +require 'epi_deploy/deployer' require 'slop' class MockOptions @@ -29,9 +30,9 @@ def tag_list describe "Command" do before do - $stdout = StringIO.new - $stderr = StringIO.new + allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil, print_failure_and_abort: nil) end + let(:options) { MockOptions.new } let(:args) { [] } @@ -73,9 +74,13 @@ def tag_list end describe "deploy" do - before { allow(Kernel).to receive(:abort) } subject { EpiDeploy::Command.new options, args, MockRelease } - + + before do + allow(Kernel).to receive(:abort) + allow_any_instance_of(EpiDeploy::Deployer).to receive(:deploy!).and_return(true) + end + around do |example| Dir.chdir(File.join(File.dirname(__FILE__), '../..', 'fixtures')) do example.call @@ -83,16 +88,21 @@ def tag_list end describe "required arguments" do + let(:release) { MockRelease.new } + let(:deployer) { double(:deployer) } + it "errors if no deploy environment is provided" do expect{ subject.deploy }.to raise_error(Slop::InvalidArgumentError, "No environments provided") end - it "accepts valid target environments" do - subject.args = %w(production) + it "it deploys the specified targets when they are all valid" do allow(subject).to receive_messages(determine_release_reference: :latest) - release = MockRelease.new allow(MockRelease).to receive_messages(find: release) - expect(release).to receive(:deploy!).with(%w(production)) + allow(EpiDeploy::Deployer).to receive(:new).and_return(deployer) + + expect(deployer).to receive(:deploy!).with(%w(production)) + + subject.args = %w(production) subject.deploy end @@ -104,42 +114,38 @@ def tag_list describe "optional --ref flag" do subject { EpiDeploy::Command.new options, ['production'], MockRelease } + let(:options) do options = Hash.new - allow(options).to receive_messages(ref?: true) options end - + before do - allow(subject).to receive_messages(valid_reference?: true) + allow(options).to receive_messages(ref?: true) end - + specify "if not supplied then the latest release is used" do allow(options).to receive_messages(ref?: false) - subject.options = options expect(subject.release_class).to receive(:find).with(:latest).and_return(MockRelease.new) subject.deploy end specify "if flag supplied with no argument then list of releases displayed with choice" do options[:ref] = nil - subject.options = options expect(subject).to receive(:prompt_for_a_release) subject.deploy end it "can be supplied with a git reference" do options[:ref] = 'an_exisiting_ref' - subject.options = options expect(subject.release_class).to receive(:find).with('an_exisiting_ref').and_return(MockRelease.new) subject.deploy end it "errors if the reference not exist" do options[:ref] = 'invalid_ref' - subject.options = options - allow(subject).to receive_messages(valid_reference?: false) - expect(subject).to receive_messages(print_failure_and_abort: "You did not enter a valid Git reference. Please try again.") + allow(MockRelease).to receive_messages(find: nil) + expect(subject).to receive(:print_failure_and_abort).with("You did not enter a valid Git reference. Please try again.") subject.deploy end end diff --git a/spec/lib/epi_deploy/deployer_spec.rb b/spec/lib/epi_deploy/deployer_spec.rb new file mode 100644 index 0000000..712ee0c --- /dev/null +++ b/spec/lib/epi_deploy/deployer_spec.rb @@ -0,0 +1,100 @@ +require 'time' + +require 'epi_deploy/deployer' + +require 'spec_helper' + +class MockGit + def initialize(on_primary_branch: true, pending_changes: false) + @on_primary_branch = on_primary_branch + @pending_changes = pending_changes + end + def add(files); end + def on_primary_branch?; @on_primary_branch; end + def pending_changes?; @pending_changes; end + def short_commit_hash; 'abc1234'; end + def commit(msg); end + def tag(name); end + def push(opts = {}); end + def pull; end + def current_branch; 'main'; end + def create_or_update_tag(stage, commit); end + def delete_branches(branches); end +end + +def deployment_stage_with_timestamp(stage) + satisfy do |tag_name| + tag_stage, timestamp = tag_name.split('-', 2) + tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) + end +end + +RSpec.describe EpiDeploy::Deployer do + let(:release) { double('release') } + let(:git_wrapper) { MockGit.new } + subject { described_class.new(release) } + + before do + allow(release).to receive_messages(reference: 'test', commit: 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b') + allow(subject).to receive_messages(git_wrapper: git_wrapper) + end + + describe "#deploy!" do + before do + allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil, print_failure_and_abort: nil) + end + + around do |example| + Dir.chdir(File.join(File.dirname(__FILE__), '../..', 'fixtures')) do + example.run + end + end + + it "runs the capistrano deploy command for each of the environments given" do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect do + subject.deploy! %w(demo production) + end.to_not raise_error + end + + context 'if deployment to all stages is successful' do + it 'adds a tag for all deployment stages with the name of the stage and timestamp' do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + end + end + + context 'if deployment to some stages is unsuccessful' do + it 'only adds the tag to the deployment stages have succeeded' do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) + expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + end + end + + it 'deletes branches for all deployment environments' do + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) + + subject.deploy! ['production'] + end + end +end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index cae0596..0acf7e9 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -22,13 +22,6 @@ def create_or_update_tag(stage, commit); end def delete_branches(branches); end end -def deployment_stage_with_timestamp(stage) - satisfy do |tag_name| - tag_stage, timestamp = tag_name.split('-', 2) - tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) - end -end - describe EpiDeploy::Release do let(:git_wrapper) { MockGit.new } @@ -98,64 +91,4 @@ def deployment_stage_with_timestamp(stage) expect { subject.create! }.to_not raise_error end end - - describe "#deploy!" do - before do - allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil, print_failure_and_abort: nil) - end - - around do |example| - Dir.chdir(File.join(File.dirname(__FILE__), '../..', 'fixtures')) do - example.run - end - end - - it "runs the capistrano deploy command for each of the environments given" do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - - expect do - subject.deploy! %w(demo production) - end.to_not raise_error - end - - context 'if deployment to all stages is successful' do - it 'adds a tag for all deployment stages with the name of the stage and timestamp' do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) - - subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] - end - end - - context 'if deployment to some stages is unsuccessful' do - it 'only adds the tag to the deployment stages have succeeded' do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) - - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), subject.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), subject.commit) - expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), subject.commit) - - subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] - end - end - - it 'deletes branches for all deployment environments' do - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - - expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) - - subject.deploy! ['production'] - end - end - end From 3d9455e89095a0cded7b08b1668b38e61ac22c59 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 15:26:58 +0100 Subject: [PATCH 24/65] Add back method to create or update branch --- lib/epi_deploy/git_wrapper.rb | 37 ++++++++++++-------- spec/lib/epi_deploy/git_wrapper_spec.rb | 46 ++++++++++++++----------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index ee32f92..b8ce122 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -23,8 +23,9 @@ def commit(message) git.commit_all message end - def push(branch, options = {force: false, tags: true}) - git.push 'origin', branch, options + def push(branch, **options) + options = { force: false, tags: true }.merge(options) + git.push 'origin', branch, **options end def add(files = nil) @@ -60,15 +61,15 @@ def create_or_update_tag(name, commit) git.push('origin', name) end + def create_or_update_branch(name, commit) + force_create_branch(name, commit) + self.push name, force: true, tags: false + end + def delete_branches(branches) - branches.each do |branch| - git.push('origin', "refs/heads/#{branch}", delete: true) - if local_branches.has_key? branch - branch_object = local_branches[branch] - branch_object.delete - local_branches.delete(branch) - end - end + remote_refs = branches.map { |branch| "refs/heads/#{branch}" } + run_custom_command("git push origin #{remote_refs.join(' ')} --delete") + local_branches(branches).each(&:delete) end def tag_list @@ -85,11 +86,19 @@ def git @git ||= ::Git.open(Dir.pwd) end - def local_branches - @branches ||= git.branches.local.map do |branch| - [branch.name, branch] - end.compact.to_h + def force_create_branch(name, commit) + run_custom_command("git branch -f #{name} #{commit}") end + def local_branches(branch_names = []) + branches = git.branches.local.find { |branch| branch_names.include? branch.name } + branches || [] + end + + def run_custom_command(command) + unless Kernel.system(command) + raise ::Git::GitExecuteError.new("Failed to run command '#{command}'") + end + end end end diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 59a5910..975b14b 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -5,6 +5,7 @@ describe EpiDeploy::GitWrapper do let(:mocked_git) { double(:git, current_branch: current_branch, add_tag: true, push: true) } + let(:commit) { 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b' } let(:current_branch) { 'main' } before(:each) do allow(subject).to receive(:git).and_return(mocked_git) @@ -13,10 +14,19 @@ describe '#create_or_update_tag' do it 'adds a new tag for the stage to the commit' do expect(mocked_git).to receive(:push).with('origin', 'refs/tags/production', delete: true) - expect(mocked_git).to receive(:add_tag).with('production', 'main', any_args) + expect(mocked_git).to receive(:add_tag).with('production', commit, any_args) expect(mocked_git).to receive(:push).with('origin', 'production') - subject.create_or_update_tag 'production', 'main' + subject.create_or_update_tag 'production', commit + end + end + + describe '#create_or_update_branch' do + it 'creates or moves the branch to the commit' do + expect(Kernel).to receive(:system).with("git branch -f production #{commit}").and_return(true) + expect(mocked_git).to receive(:push).with('origin', 'production', force: true, tags: false) + + subject.create_or_update_branch('production', commit) end end @@ -62,28 +72,26 @@ end context 'if all the branches exist as local branches' do - let(:production_branch) { double('production branch') } - let(:demo_branch) { double('demo branch') } + let(:production_branch) { double('production branch', delete: true) } + let(:demo_branch) { double('demo branch', delete: true) } let(:local_branches) { - { - 'production' => production_branch, - 'demo' => demo_branch, - } + [ + production_branch, + demo_branch, + ] } specify 'it deletes each branch from the remote' do allow(production_branch).to receive(:delete) allow(demo_branch).to receive(:delete) - expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) - expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) + expect(subject).to receive(:run_custom_command).with("git push origin refs/heads/production refs/heads/demo --delete") subject.delete_branches(branches) end specify 'it deletes each branch locally' do - expect(local_branches).to receive(:[]).with('production').and_call_original - expect(local_branches).to receive(:[]).with('demo').and_call_original + allow(subject).to receive(:run_custom_command) expect(production_branch).to receive(:delete) expect(demo_branch).to receive(:delete) @@ -93,25 +101,21 @@ context 'if not all the branches exist as local branches' do let(:production_branch) { double('production branch') } - let(:local_branches) { - { - 'production' => production_branch, - } - } + let(:demo_branch) { double('demo branch') } + let(:local_branches) { [production_branch ] } specify 'it deletes each branch from the remote' do allow(production_branch).to receive(:delete) - expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', delete: true) - expect(mocked_git).to receive(:push).with('origin', 'refs/heads/demo', delete: true) + expect(subject).to receive(:run_custom_command).with("git push origin refs/heads/production refs/heads/demo --delete") subject.delete_branches(branches) end specify 'it deletes only the branches that exist locally' do - expect(local_branches).to receive(:[]).with('production').and_call_original - expect(local_branches).to_not receive(:[]).with('demo') + allow(subject).to receive(:run_custom_command) expect(production_branch).to receive(:delete) + expect(demo_branch).to_not receive(:delete) subject.delete_branches(branches) end From dfd4268b972bbac188218c46dabf9529500d2460 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 15:34:07 +0100 Subject: [PATCH 25/65] Replace GitWrapper#tag with create_or_update_tag --- lib/epi_deploy/git_wrapper.rb | 16 +++++++++------- lib/epi_deploy/release.rb | 4 ++-- spec/lib/epi_deploy/release_spec.rb | 6 +++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index b8ce122..8668861 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -36,10 +36,6 @@ def short_commit_hash git.log.first.sha[0..6] end - def tag(tag_name) - git.add_tag(tag_name, annotate: true, message: tag_name) - end - def get_commit(git_reference) if git_reference == :latest print_failure_and_abort("There is no latest release. Create one, or specify a reference with --ref") if tag_list.empty? @@ -55,10 +51,16 @@ def get_commit(git_reference) end end - def create_or_update_tag(name, commit) - git.push('origin', "refs/tags/#{name}", delete: true) + def create_or_update_tag(name, commit = nil, push: true) + if push + git.push('origin', "refs/tags/#{name}", delete: true) + end + git.add_tag(name, commit, annotate: true, f: true, message: name) - git.push('origin', name) + + if push + git.push('origin', name) + end end def create_or_update_branch(name, commit) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 99e40d3..881fa80 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -22,8 +22,8 @@ def create! git_wrapper.commit "Bumped to version #{new_version} [skip ci]" self.tag = "#{date_and_time_for_tag}-#{git_wrapper.short_commit_hash}-v#{new_version}" - git_wrapper.tag self.tag - git_wrapper.push git_wrapper.current_branch + git_wrapper.create_or_update_tag(self.tag, push: false) + git_wrapper.push(git_wrapper.current_branch, tags: true) rescue ::Git::GitExecuteError => e print_failure_and_abort "A git error occurred: #{e.message}" end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index 0acf7e9..3fa4db7 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -15,7 +15,7 @@ def pending_changes?; @pending_changes; end def short_commit_hash; 'abc1234'; end def commit(msg); end def tag(name); end - def push(opts = {}); end + def push(ref, **opts); end def pull; end def current_branch; 'main'; end def create_or_update_tag(stage, commit); end @@ -79,14 +79,14 @@ def delete_branches(branches); end allow(subject).to receive_messages bump_version: 42 now = Time.new 2014, 12, 1, 16, 15 allow(Time).to receive_messages now: now - expect(git_wrapper).to receive(:tag).with('2014dec01-1615-abc1234-v42') + expect(git_wrapper).to receive(:create_or_update_tag).with('2014dec01-1615-abc1234-v42', push: false) expect { subject.create! }.to_not raise_error end it "pushes the new version to primary branch to reduce the chance of version number collisions" do allow(subject).to receive_messages bump_version: 42 - expect(git_wrapper).to receive(:push) + expect(git_wrapper).to receive(:push).with('main', tags: true) expect { subject.create! }.to_not raise_error end From b008c3287ce3b472f9a43e83fe11bbe2117e5ff7 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 15:55:00 +0100 Subject: [PATCH 26/65] Add old branch-style deployment behaviour back, using the use_timestamped_deploy_tags option to decide how to deploy --- lib/epi_deploy/deployer.rb | 35 +++++++- lib/epi_deploy/stages_extractor.rb | 4 +- spec/lib/epi_deploy/deployer_spec.rb | 115 +++++++++++++++++++-------- 3 files changed, 117 insertions(+), 37 deletions(-) diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index 03cde3d..18e6046 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -15,6 +15,19 @@ def deploy!(stages_or_environments) begin git_wrapper.pull + if EpiDeploy.use_timestamped_deploy_tags + deploy_with_timestamped_tags(stages_or_environments) + else + deploy_with_environment_branches(stages_or_environments) + end + rescue ::Git::GitExecuteError => e + print_failure_and_abort "A git error occurred: #{e.message}" + end + end + + private + + def deploy_with_timestamped_tags(stages_or_environments) # Remove legacy environment branches in the local repo and remotely print_notice 'Removing any legacy deployment branches' git_wrapper.delete_branches(stages_extractor.environments) @@ -32,12 +45,26 @@ def deploy!(stages_or_environments) end end end - rescue ::Git::GitExecuteError => e - print_failure_and_abort "A git error occurred: #{e.message}" end - end - private + def deploy_with_environment_branches(stages_or_environments) + stages_or_environments.each do |stage_or_environment| + begin + git_wrapper.pull + + matches = StagesExtractor.match_with(stage_or_environment) + # Force the tag/branch to the commit we want to deploy + git_wrapper.create_or_update_branch(matches[:stage], @release.commit) + + completed = run_cap_deploy_to(stage_or_environment) + if !completed + print_failure_and_abort "Deployment failed - please review output before deploying again" + end + rescue ::Git::GitExecuteError => e + print_failure_and_abort "A git error occurred: #{e.message}" + end + end + end def git_wrapper @git_wrapper ||= GitWrapper.new diff --git a/lib/epi_deploy/stages_extractor.rb b/lib/epi_deploy/stages_extractor.rb index 0fd4c7d..9afef4e 100644 --- a/lib/epi_deploy/stages_extractor.rb +++ b/lib/epi_deploy/stages_extractor.rb @@ -57,8 +57,8 @@ def environments @environment_to_stages.keys end - def self.match_with(environment) - environment.match(STAGE_REGEX) + def self.match_with(stage_or_environment) + stage_or_environment.match(STAGE_REGEX) end private diff --git a/spec/lib/epi_deploy/deployer_spec.rb b/spec/lib/epi_deploy/deployer_spec.rb index 712ee0c..13fd663 100644 --- a/spec/lib/epi_deploy/deployer_spec.rb +++ b/spec/lib/epi_deploy/deployer_spec.rb @@ -18,7 +18,8 @@ def tag(name); end def push(opts = {}); end def pull; end def current_branch; 'main'; end - def create_or_update_tag(stage, commit); end + def create_or_update_tag(name, commit); end + def create_or_update_branch(name, commit); end def delete_branches(branches); end end @@ -31,6 +32,7 @@ def deployment_stage_with_timestamp(stage) RSpec.describe EpiDeploy::Deployer do let(:release) { double('release') } + let(:system_exit) { Exception.new('test exception') } let(:git_wrapper) { MockGit.new } subject { described_class.new(release) } @@ -41,7 +43,8 @@ def deployment_stage_with_timestamp(stage) describe "#deploy!" do before do - allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil, print_failure_and_abort: nil) + allow_any_instance_of(EpiDeploy::Helpers).to receive_messages(print_notice: nil, print_success: nil) + allow_any_instance_of(EpiDeploy::Helpers).to receive(:print_failure_and_abort) { raise system_exit } end around do |example| @@ -50,51 +53,101 @@ def deployment_stage_with_timestamp(stage) end end - it "runs the capistrano deploy command for each of the environments given" do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - - expect do - subject.deploy! %w(demo production) - end.to_not raise_error - end + context 'given that timestamped deployment tags are enabled' do + before do + allow(EpiDeploy).to receive(:use_timestamped_deploy_tags).and_return(true) + end - context 'if deployment to all stages is successful' do - it 'adds a tag for all deployment stages with the name of the stage and timestamp' do + it "runs the capistrano deploy command for each of the environments given" do expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + expect do + subject.deploy! %w(demo production) + end.to_not raise_error + end - subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + context 'if deployment to all stages is successful' do + it 'adds a tag for all deployment stages with the name of the stage and timestamp' do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + end end - end - context 'if deployment to some stages is unsuccessful' do - it 'only adds the tag to the deployment stages have succeeded' do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + context 'if deployment to some stages is unsuccessful' do + it 'only adds the tag to the deployment stages have succeeded' do + expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) + expect(Kernel).to_not receive(:system).with('bundle exec cap demo deploy target=test') + + expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) + expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) + + expect { subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] }.to raise_error system_exit + end + end + + it 'deletes branches for all deployment environments' do expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) + expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) - expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) - expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) + expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) - subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + subject.deploy! ['production'] end end - it 'deletes branches for all deployment environments' do - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + context 'given that timestamped deploy tags have not been enabled' do + before do + allow(EpiDeploy).to receive(:use_timestamped_deploy_tags).and_return(false) + end - expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) + it "runs the capistrano deploy task for single-customer environments" do + expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + + expect do + subject.deploy! %w(demo) + end.to_not raise_error + end + + it 'runs the capistrano deploy_all task for multi-customer environments' do + expect(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + + expect do + subject.deploy! %w(production) + end.to_not raise_error + end + + it 'creates a branch for each deployment environment' do + allow(Kernel).to receive(:system).and_return(true) - subject.deploy! ['production'] + expect(git_wrapper).to receive(:create_or_update_branch).with('demo', release.commit) + expect(git_wrapper).to receive(:create_or_update_branch).with('production', release.commit).at_least(:once) + + expect do + subject.deploy! ['production.epigenesys', 'production.genesys', 'demo'] + end.to_not raise_error + end + + it 'creates a branch for a deployment stage each if it does not succeed but not for subsequent environments' do + allow(Kernel).to receive(:system).and_return(false) + + expect(git_wrapper).to receive(:create_or_update_branch).with('demo', release.commit) + expect(git_wrapper).to_not receive(:create_or_update_branch).with('production', any_args) + + expect do + subject.deploy! ['demo', 'production'] + end.to raise_error system_exit + end end end end From 2e7762e5d98f6c4b4eba7467c05d74fca2894a28 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 16:05:56 +0100 Subject: [PATCH 27/65] Add git wrapper method for fetching a list of tags for a given object reference --- lib/epi_deploy/git_wrapper.rb | 4 ++++ spec/lib/epi_deploy/git_wrapper_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 8668861..f8edf52 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -82,6 +82,10 @@ def current_branch git.current_branch end + def tags_for_object(object) + `git tag --points-at #{object}`.split() + end + private def git diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 975b14b..ca0992e 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -122,4 +122,14 @@ end end + describe '#tags_for_object' do + before do + allow(Kernel).to receive(:`).with('git tags --points-at HEAD').and_return("test1\ntest2\n") + end + + it 'returns a list of tags that point at a given object' do + expect(subject.tags_for_object('HEAD')).to match_array ['test1', 'test2'] + end + end + end From 714670b9e0f5baaf3f7d5dba6e65b1e1550400d0 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 16:39:48 +0100 Subject: [PATCH 28/65] Update Release.create! so that it does not create a new release and tag if the most recent commit is already a release commit --- lib/epi_deploy/command.rb | 7 +++++-- lib/epi_deploy/git_wrapper.rb | 4 ++++ lib/epi_deploy/release.rb | 19 ++++++++++++------- spec/lib/epi_deploy/release_spec.rb | 23 +++++++++++++++-------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index 9bc62f6..3b976f9 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -19,8 +19,11 @@ def release(setup_class = EpiDeploy::Setup) setup_class.initial_setup_if_required release = self.release_class.new - release.create! - print_success "Release #{release.version} created with tag #{release.tag}" + if release.create! + print_success "Release #{release.version} created with tag #{release.tag}" + else + print_notice "Release #{release.version} has already been created on the most commit" + end environments = self.options.to_hash[:deploy] self.deploy(environments) unless environments.nil? end diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index f8edf52..67b898e 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -86,6 +86,10 @@ def tags_for_object(object) `git tag --points-at #{object}`.split() end + def most_recent_commit(object) + git.log(1).first + end + private def git diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 881fa80..a51a56d 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -17,13 +17,18 @@ def create! begin git_wrapper.pull - new_version = app_version.bump! - git_wrapper.add(app_version.version_file_path) - git_wrapper.commit "Bumped to version #{new_version} [skip ci]" - - self.tag = "#{date_and_time_for_tag}-#{git_wrapper.short_commit_hash}-v#{new_version}" - git_wrapper.create_or_update_tag(self.tag, push: false) - git_wrapper.push(git_wrapper.current_branch, tags: true) + if git_wrapper.most_recent_commit.message.start_with? 'Bumped to version' + false + else + new_version = app_version.bump! + git_wrapper.add(app_version.version_file_path) + git_wrapper.commit "Bumped to version #{new_version} [skip ci]" + + self.tag = "#{date_and_time_for_tag}-#{git_wrapper.short_commit_hash}-v#{new_version}" + git_wrapper.create_or_update_tag(self.tag, push: false) + git_wrapper.push(git_wrapper.current_branch, tags: true) + true + end rescue ::Git::GitExecuteError => e print_failure_and_abort "A git error occurred: #{e.message}" end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index 3fa4db7..b045860 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -35,14 +35,14 @@ def delete_branches(branches); end allow(subject).to receive_messages(git_wrapper: MockGit.new(on_primary_branch: false)) expect(subject).to receive(:print_failure_and_abort).with('You can only create a release on the main or master branch. Please switch to main or master and try again.') - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "errors when pending changes exist" do allow(subject).to receive_messages(git_wrapper: MockGit.new(pending_changes: true)) expect(subject).to receive(:print_failure_and_abort).with('You have pending changes, please commit or stash them and try again.') - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end end @@ -50,14 +50,14 @@ def delete_branches(branches); end allow(subject).to receive_messages(bump_version: nil) expect(git_wrapper).to receive(:pull) - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "stops with a warning message when a git pull fails (eg. merge errors)" do allow(subject).to receive_messages(bump_version: nil) expect(git_wrapper).to receive(:pull) - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "bumps the version number" do @@ -65,14 +65,14 @@ def delete_branches(branches); end allow(subject).to receive_messages(app_version: app_version) expect(app_version).to receive(:bump!) - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "commits the new version number" do allow(subject).to receive_messages bump_version: 42 expect(git_wrapper).to receive(:commit).with('Bumped to version 42 [skip ci]') - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "creates a tag in the format YYYYmonDD-HHMM-CommitRef-version for the new commit" do @@ -81,14 +81,21 @@ def delete_branches(branches); end allow(Time).to receive_messages now: now expect(git_wrapper).to receive(:create_or_update_tag).with('2014dec01-1615-abc1234-v42', push: false) - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true end it "pushes the new version to primary branch to reduce the chance of version number collisions" do allow(subject).to receive_messages bump_version: 42 expect(git_wrapper).to receive(:push).with('main', tags: true) - expect { subject.create! }.to_not raise_error + expect(subject.create!).to eq true + end + + it 'does not create a new release if the most recent commit is a release commit' do + allow(git_wrapper).to receive(:most_recent_commit).and_return(double('commit', message: 'Bumped to version 12 [skip ci]')) + expect(git_wrapper).to_not receive(:create_or_update_tag) + + expect(subject.create!).to eq false end end end From b324aad63ad8964759c5056bf563c1c079e2dd37 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 16:49:10 +0100 Subject: [PATCH 29/65] Fix specs for Release class --- spec/lib/epi_deploy/release_spec.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index b045860..fb91c48 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -27,22 +27,23 @@ def delete_branches(branches); end let(:git_wrapper) { MockGit.new } before do allow(subject).to receive_messages(reference: 'test', git_wrapper: git_wrapper, commit: 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b', app_version: double(bump!: 42, version_file_path: '')) + allow(git_wrapper).to receive(:most_recent_commit).and_return(double('commit', message: 'Some non-release commit')) end describe "#create!" do describe "preconditions" do it "can only be done on the primary branch" do - allow(subject).to receive_messages(git_wrapper: MockGit.new(on_primary_branch: false)) + allow(git_wrapper).to receive_messages(on_primary_branch?: false) expect(subject).to receive(:print_failure_and_abort).with('You can only create a release on the main or master branch. Please switch to main or master and try again.') - expect(subject.create!).to eq true + subject.create! end it "errors when pending changes exist" do - allow(subject).to receive_messages(git_wrapper: MockGit.new(pending_changes: true)) + allow(git_wrapper).to receive_messages(pending_changes?: true) expect(subject).to receive(:print_failure_and_abort).with('You have pending changes, please commit or stash them and try again.') - expect(subject.create!).to eq true + subject.create! end end From c5703bce223801d913279780e9a61c3c1274d3e9 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 16:51:49 +0100 Subject: [PATCH 30/65] Remove incorrect parameter for method --- lib/epi_deploy/git_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 67b898e..d786478 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -86,7 +86,7 @@ def tags_for_object(object) `git tag --points-at #{object}`.split() end - def most_recent_commit(object) + def most_recent_commit git.log(1).first end From 2bca50f2431e15ae6c5afdc4004803cf22144234 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 2 Oct 2024 17:01:05 +0100 Subject: [PATCH 31/65] Fix test for fetching tags that point to an object --- spec/lib/epi_deploy/git_wrapper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index ca0992e..6bd52f6 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -124,7 +124,7 @@ describe '#tags_for_object' do before do - allow(Kernel).to receive(:`).with('git tags --points-at HEAD').and_return("test1\ntest2\n") + allow(subject).to receive(:`).with('git tag --points-at HEAD').and_return("test1\ntest2\n") end it 'returns a list of tags that point at a given object' do From 56f3194f07445f22daf3145ad95dc17a5ef62d85 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 09:30:20 +0100 Subject: [PATCH 32/65] Only symlink the customer settings if the file exists --- lib/capistrano/tasks/multi_customers.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/capistrano/tasks/multi_customers.rb b/lib/capistrano/tasks/multi_customers.rb index 800c79a..ad3c325 100644 --- a/lib/capistrano/tasks/multi_customers.rb +++ b/lib/capistrano/tasks/multi_customers.rb @@ -1,7 +1,10 @@ namespace :epi_deploy do task :symlink_customer_configs do on release_roles :all do - execute :ln, '-s', release_path.join("config/customers/customer_settings/#{fetch(:current_customer)}.yml"), release_path.join("config/customer_settings.yml") + customer_settings_path = release_path.join("config/customers/customer_settings/#{fetch(:current_customer)}.yml") + if test("[ -f #{customer_settings_path}]") + execute :ln, '-s', customer_settings_path, release_path.join("config/customer_settings.yml") + end end end end From fdb8cbc6e237f8807fa4872f1d469f5d213a1f99 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 09:39:36 +0100 Subject: [PATCH 33/65] Extract file contents in executables to Cli class --- bin/ed | 44 ++--------------------------------- bin/epi_deploy | 44 ++--------------------------------- lib/epi_deploy/cli.rb | 54 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 84 deletions(-) create mode 100644 lib/epi_deploy/cli.rb diff --git a/bin/ed b/bin/ed index f00d6ba..0d89b9a 100755 --- a/bin/ed +++ b/bin/ed @@ -1,45 +1,5 @@ #!/usr/bin/env ruby -require 'slop' -require_relative '../lib/epi_deploy/command' +require_relative '../lib/epi_deploy/cli' -config_path = File.join(Dir.pwd, 'config/epi_deploy.rb') -if File.exist?(config_path) - require config_path -end - -begin - opts = Slop.parse strict: true do - - banner 'Usage: bundle exec epi_deploy ' - - command 'release' do - description 'Create a new release with optional deploy' - - on :d=, :deploy=, 'Deploy to specified environment(s)', argument: :optional, as: Array, delimiter: ':' - - run do |options, args| - command = EpiDeploy::Command.new(options, args) - command.release - end - end - - command 'deploy' do - description 'Deploys an existing release' - - on :r=, :ref=, 'Git reference to deploy', argument: :optional - - run do |options, args| - command = EpiDeploy::Command.new(options, args) - command.deploy - end - end - - run do |options, args| - Kernel.abort "\x1B[31mValid commands are 'release' and 'deploy'.\x1B[0m" - end - - end -rescue Slop::InvalidOptionError, Slop::InvalidArgumentError, RuntimeError => e - Kernel.abort "\x1B[31m#{e.message}\x1B[0m" -end +EpiDeploy::Cli.new.run! diff --git a/bin/epi_deploy b/bin/epi_deploy index f00d6ba..0d89b9a 100755 --- a/bin/epi_deploy +++ b/bin/epi_deploy @@ -1,45 +1,5 @@ #!/usr/bin/env ruby -require 'slop' -require_relative '../lib/epi_deploy/command' +require_relative '../lib/epi_deploy/cli' -config_path = File.join(Dir.pwd, 'config/epi_deploy.rb') -if File.exist?(config_path) - require config_path -end - -begin - opts = Slop.parse strict: true do - - banner 'Usage: bundle exec epi_deploy ' - - command 'release' do - description 'Create a new release with optional deploy' - - on :d=, :deploy=, 'Deploy to specified environment(s)', argument: :optional, as: Array, delimiter: ':' - - run do |options, args| - command = EpiDeploy::Command.new(options, args) - command.release - end - end - - command 'deploy' do - description 'Deploys an existing release' - - on :r=, :ref=, 'Git reference to deploy', argument: :optional - - run do |options, args| - command = EpiDeploy::Command.new(options, args) - command.deploy - end - end - - run do |options, args| - Kernel.abort "\x1B[31mValid commands are 'release' and 'deploy'.\x1B[0m" - end - - end -rescue Slop::InvalidOptionError, Slop::InvalidArgumentError, RuntimeError => e - Kernel.abort "\x1B[31m#{e.message}\x1B[0m" -end +EpiDeploy::Cli.new.run! diff --git a/lib/epi_deploy/cli.rb b/lib/epi_deploy/cli.rb new file mode 100644 index 0000000..76b0855 --- /dev/null +++ b/lib/epi_deploy/cli.rb @@ -0,0 +1,54 @@ +require 'slop' + +require_relative 'command' + +module EpiDeploy + class Cli + def run! + load_config + + opts = Slop.parse strict: true do + + banner 'Usage: bundle exec epi_deploy ' + + command 'release' do + description 'Create a new release with optional deploy' + + on :d=, :deploy=, 'Deploy to specified environment(s)', argument: :optional, as: Array, delimiter: ':' + + run do |options, args| + command = EpiDeploy::Command.new(options, args) + command.release + end + end + + command 'deploy' do + description 'Deploys an existing release' + + on :r=, :ref=, 'Git reference to deploy', argument: :optional + + run do |options, args| + command = EpiDeploy::Command.new(options, args) + command.deploy + end + end + + run do |options, args| + Kernel.abort "\x1B[31mValid commands are 'release' and 'deploy'.\x1B[0m" + end + + end + rescue Slop::InvalidOptionError, Slop::InvalidArgumentError, RuntimeError => e + Kernel.abort "\x1B[31m#{e.message}\x1B[0m" + end + + private + + def load_config + config_path = File.join(Dir.pwd, 'config/epi_deploy.rb') + if File.exist?(config_path) + require config_path + end + end + end +end \ No newline at end of file From 7b863f604d7f880966a1a8dfc9b55fd47a20a380 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 09:41:33 +0100 Subject: [PATCH 34/65] Clean up unused variable --- lib/epi_deploy/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/cli.rb b/lib/epi_deploy/cli.rb index 76b0855..46d6b32 100644 --- a/lib/epi_deploy/cli.rb +++ b/lib/epi_deploy/cli.rb @@ -7,7 +7,7 @@ class Cli def run! load_config - opts = Slop.parse strict: true do + Slop.parse strict: true do banner 'Usage: bundle exec epi_deploy ' From 54056945e5a1f8c872f7fcbaf99a966a0e182e9b Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 09:47:58 +0100 Subject: [PATCH 35/65] Update authors in gemspec --- epi_deploy.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/epi_deploy.gemspec b/epi_deploy.gemspec index 670d61e..e0f75b5 100644 --- a/epi_deploy.gemspec +++ b/epi_deploy.gemspec @@ -6,8 +6,8 @@ require 'epi_deploy/version' Gem::Specification.new do |gem| gem.name = "epi_deploy" gem.version = EpiDeploy::VERSION - gem.authors = ["Anthony Nettleship", "Shuo Chen", "Chris Hunt", "James Gregory"] - gem.email = ["anthony.nettleship@epigenesys.org.uk", "shuo.chen@epigenesys.org.uk", "chris.hunt@epigenesys.org.uk", "james.gregory@epigenesys.org.uk"] + gem.authors = ["Anthony Nettleship", "Shuo Chen", "Chris Hunt", "James Gregory", "William Lee"] + gem.email = ["anthony.nettleship@epigenesys.org.uk", "shuo.chen@epigenesys.org.uk", "chris.hunt@epigenesys.org.uk", "james.gregory@epigenesys.org.uk", "william.lee@epigenesys.org.uk"] gem.description = "A gem to facilitate deployment across multiple git branches and evironments" gem.summary = "eD" gem.homepage = "https://www.epigenesys.org.uk" From e445ed8056311a3961f86912851af96a9f7b8b69 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 09:55:34 +0100 Subject: [PATCH 36/65] Remove unused test import --- spec/lib/epi_deploy/command_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/lib/epi_deploy/command_spec.rb b/spec/lib/epi_deploy/command_spec.rb index 638da75..2019790 100644 --- a/spec/lib/epi_deploy/command_spec.rb +++ b/spec/lib/epi_deploy/command_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'epi_deploy/stages_extractor' require 'epi_deploy/command' require 'epi_deploy/deployer' require 'slop' From e6a8f1725bfdb1cd1c983063d421b51e2d7e1c93 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 10:00:47 +0100 Subject: [PATCH 37/65] Avoid duplicate updates to the same deployment branch when deploying with branches --- lib/epi_deploy/deployer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index 18e6046..9853175 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -48,13 +48,17 @@ def deploy_with_timestamped_tags(stages_or_environments) end def deploy_with_environment_branches(stages_or_environments) + updated_branches = Set.new + stages_or_environments.each do |stage_or_environment| begin git_wrapper.pull matches = StagesExtractor.match_with(stage_or_environment) # Force the tag/branch to the commit we want to deploy - git_wrapper.create_or_update_branch(matches[:stage], @release.commit) + unless updated_branches.include? matches[:stage] + git_wrapper.create_or_update_branch(matches[:stage], @release.commit) + end completed = run_cap_deploy_to(stage_or_environment) if !completed From a5d35cb12593eb155dd64b92217589247560f184 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 10:01:35 +0100 Subject: [PATCH 38/65] Fix deployment branches deduplication --- lib/epi_deploy/deployer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index 9853175..c02ea4d 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -58,6 +58,7 @@ def deploy_with_environment_branches(stages_or_environments) # Force the tag/branch to the commit we want to deploy unless updated_branches.include? matches[:stage] git_wrapper.create_or_update_branch(matches[:stage], @release.commit) + updated_branches << matches[:stage] end completed = run_cap_deploy_to(stage_or_environment) From fea5e505127067c4e74da25851e7a16232349d2f Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 10:59:48 +0100 Subject: [PATCH 39/65] Fix typo in release message --- lib/epi_deploy/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index 3b976f9..dbe29b5 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -22,7 +22,7 @@ def release(setup_class = EpiDeploy::Setup) if release.create! print_success "Release #{release.version} created with tag #{release.tag}" else - print_notice "Release #{release.version} has already been created on the most commit" + print_notice "Release #{release.version} has already been created on the most recent commit" end environments = self.options.to_hash[:deploy] self.deploy(environments) unless environments.nil? From a16341f0caae1c96c9da4c1f4f5798a01134d2a6 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 11:04:55 +0100 Subject: [PATCH 40/65] Remove unused method --- lib/epi_deploy/git_wrapper.rb | 4 ---- spec/lib/epi_deploy/git_wrapper_spec.rb | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index d786478..511ba97 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -82,10 +82,6 @@ def current_branch git.current_branch end - def tags_for_object(object) - `git tag --points-at #{object}`.split() - end - def most_recent_commit git.log(1).first end diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 6bd52f6..153d53d 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -121,15 +121,4 @@ end end end - - describe '#tags_for_object' do - before do - allow(subject).to receive(:`).with('git tag --points-at HEAD').and_return("test1\ntest2\n") - end - - it 'returns a list of tags that point at a given object' do - expect(subject.tags_for_object('HEAD')).to match_array ['test1', 'test2'] - end - end - end From 6a7d003620f10d8202b558eaff105192e6cee062 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 11:15:32 +0100 Subject: [PATCH 41/65] Use fully qualified ref names to avoid ambiguity between branches and tags with the same name --- lib/epi_deploy/git_wrapper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 511ba97..e2c754f 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -59,7 +59,7 @@ def create_or_update_tag(name, commit = nil, push: true) git.add_tag(name, commit, annotate: true, f: true, message: name) if push - git.push('origin', name) + git.push('origin', "refs/tags/#{name}") end end @@ -93,7 +93,7 @@ def git end def force_create_branch(name, commit) - run_custom_command("git branch -f #{name} #{commit}") + run_custom_command("git branch -f refs/heads/#{name} #{commit}") end def local_branches(branch_names = []) From 76bc8aeae85e98c916cd267c0f2079c2e06d54b5 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 11:20:18 +0100 Subject: [PATCH 42/65] Fix tests --- lib/epi_deploy/git_wrapper.rb | 2 +- spec/lib/epi_deploy/git_wrapper_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index e2c754f..892c6a5 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -65,7 +65,7 @@ def create_or_update_tag(name, commit = nil, push: true) def create_or_update_branch(name, commit) force_create_branch(name, commit) - self.push name, force: true, tags: false + self.push "refs/heads/#{name}", force: true, tags: false end def delete_branches(branches) diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 153d53d..582b503 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -15,7 +15,7 @@ it 'adds a new tag for the stage to the commit' do expect(mocked_git).to receive(:push).with('origin', 'refs/tags/production', delete: true) expect(mocked_git).to receive(:add_tag).with('production', commit, any_args) - expect(mocked_git).to receive(:push).with('origin', 'production') + expect(mocked_git).to receive(:push).with('origin', 'refs/tags/production') subject.create_or_update_tag 'production', commit end @@ -23,9 +23,9 @@ describe '#create_or_update_branch' do it 'creates or moves the branch to the commit' do - expect(Kernel).to receive(:system).with("git branch -f production #{commit}").and_return(true) - expect(mocked_git).to receive(:push).with('origin', 'production', force: true, tags: false) - + expect(Kernel).to receive(:system).with("git branch -f refs/heads/production #{commit}").and_return(true) + expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', force: true, tags: false +) subject.create_or_update_branch('production', commit) end end From d6b6587d690508bb1d4cf09d653d682010334dae Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 11:21:01 +0100 Subject: [PATCH 43/65] Remove unused parameter --- lib/epi_deploy/deployer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index c02ea4d..7c7e9e4 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -7,7 +7,7 @@ module EpiDeploy class Deployer include Helpers - def initialize(release, git_wrapper: nil) + def initialize(release) @release = release end From 54ed3d00643c31fc965414d98fb22afea9dc72f1 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 11:25:49 +0100 Subject: [PATCH 44/65] Use filter instead of find for finding local branches --- lib/epi_deploy/git_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 892c6a5..58eee80 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -97,7 +97,7 @@ def force_create_branch(name, commit) end def local_branches(branch_names = []) - branches = git.branches.local.find { |branch| branch_names.include? branch.name } + branches = git.branches.local.filter { |branch| branch_names.include? branch.name } branches || [] end From f9baca33554675807348cb89d20edeae4fbdda0b Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 12:25:19 +0100 Subject: [PATCH 45/65] Update README with deployment tags information --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 23d706a..46e87f7 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,23 @@ This gem provides a convenient interface for creating releases and deploying usi ## Installation +At the top of your application's `Gemfile` add this line if it does not already exist: + +```sh +git_source(:github) { |repo| "https://github.com/#{repo}.git" } +``` + Add this line to your application's Gemfile: - gem 'epi_deploy', github: 'epigenesys/epi_deploy' +```rb +gem 'epi_deploy', github: 'epigenesys/epi_deploy' +``` And then execute: - $ bundle install - +```sh +$ bundle install +``` ## Usage @@ -101,10 +110,35 @@ You can also deploy to all customers for a given environment by running e.g. `ca Using branches for stages, i.e. demo and production branches, can clutter up your branches screen. This can be particularly awkward when running CI and keeping track of multiple active branches. To resolve this you can optionally configure epi_deploy to use tags for this instead of branches. -1. Update to epi_deploy 2.2.0 or greater. -2. Add config/epi_deploy.rb to your application. -3. Add `EpiDeploy.use_tags_for_deploy = true` to the newly created config file. -4. Delete your stage branches on gitlab (likely `production`, `demo`, `qa`). -5. Push your changes and deploy to a demo site to test it is working correctly. +Tags will be automatically created for each successful deployment with the format `environment.stage-timestamp`, for example `production.epigenesys-2024_10_03-12_20_09`, and pushed to the remote. + +1. Change the line in your Gemfile to this, to ensure that you have version 2.3 or greater. + + ```rb + gem 'epi_deploy', '>= 2.3', github: 'epigenesys/epi_deploy' + ``` + +1. Update epi_deploy in your application's gems + + ```sh + bundle update epi_deploy + ``` + +1. Create a file called `config/epi_deploy.rb` if it does not already exist, with this configuration option: + + ```rb + EpiDeploy.use_timestamped_deploy_tags = true + ``` + +1. Push your changes and deploy to a demo site to test it is working correctly. + +If you've previously used the `use_tags_for_deploy` configuration option, then this has now been removed since v2.3. If you upgrade to v2.3, then you should remove the old deployment tags manually from your local repo and remotely by doing, e.g. + +```sh +git tag --delete production demo +git push origin --delete production demo +``` + +and remove the old `EpiDeploy.use_tags_for_deploy` from your application's `config/epi_deploy.rb` file. -Note: In the future we intend to change this configuration option to default to true. \ No newline at end of file +You can then use the deployment branches (the default behaviour) or the new tags for deployment. From c174c8cf182bf639a5cf5d2409423efe0a0ff24a Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 14:30:43 +0100 Subject: [PATCH 46/65] Add epi_deploy:set_branch task for manually set the Capistrano :branch variable on deployment --- lib/capistrano/epi_deploy.rb | 2 +- lib/capistrano/tasks/branches.rb | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 lib/capistrano/tasks/branches.rb diff --git a/lib/capistrano/epi_deploy.rb b/lib/capistrano/epi_deploy.rb index 8ad9ba3..3be9d81 100644 --- a/lib/capistrano/epi_deploy.rb +++ b/lib/capistrano/epi_deploy.rb @@ -1 +1 @@ -load File.join(File.dirname(__FILE__), 'tasks', 'multi_customers.rb') \ No newline at end of file +Dir.glob(File.join(File.dirname(__FILE__), 'tasks', '*.rb')).each { |f| require f } \ No newline at end of file diff --git a/lib/capistrano/tasks/branches.rb b/lib/capistrano/tasks/branches.rb new file mode 100644 index 0000000..d1664cb --- /dev/null +++ b/lib/capistrano/tasks/branches.rb @@ -0,0 +1,21 @@ +if File.exist?('config/initializers/version.rb') + require_relative 'config/initializers/version' +end + +namespace :epi_deploy do + task :set_branch do + branch = if ENV['BRANCH'] + ENV['BRANCH'] + elsif Object.const_defined?('LATEST_RELEASE_TAG') + LATEST_RELEASE_TAG + end + + if branch.nil? + raise 'Cannot determine commit to deploy as BRANCH environment variable is not set and LATEST_RELEASE_TAG constant in version.rb could not be found' + end + + set :branch, branch + end +end + +before 'deploy:starting', 'epi_deploy:set_branch' From 7cdc07e9402f777bc34742834f683c4598056c29 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 14:43:02 +0100 Subject: [PATCH 47/65] Update deployer to pass the branch reference as an environment variable to the capistrano task --- lib/epi_deploy/deployer.rb | 2 +- spec/lib/epi_deploy/deployer_spec.rb | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index 7c7e9e4..c138a30 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -93,7 +93,7 @@ def run_cap_deploy_to(environment) "deploy" end - Kernel.system "bundle exec cap #{environment} #{task_to_run} target=#{@release.reference}" + Kernel.system "BRANCH=#{@release.commit} bundle exec cap #{environment} #{task_to_run}" end end end \ No newline at end of file diff --git a/spec/lib/epi_deploy/deployer_spec.rb b/spec/lib/epi_deploy/deployer_spec.rb index 13fd663..45cbfc1 100644 --- a/spec/lib/epi_deploy/deployer_spec.rb +++ b/spec/lib/epi_deploy/deployer_spec.rb @@ -59,9 +59,9 @@ def deployment_stage_with_timestamp(stage) end it "runs the capistrano deploy command for each of the environments given" do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap demo deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.epigenesys deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.genesys deploy").and_return(true) expect do subject.deploy! %w(demo production) @@ -70,9 +70,9 @@ def deployment_stage_with_timestamp(stage) context 'if deployment to all stages is successful' do it 'adds a tag for all deployment stages with the name of the stage and timestamp' do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap demo deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.epigenesys deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.genesys deploy").and_return(true) expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('demo'), release.commit) expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) @@ -84,9 +84,9 @@ def deployment_stage_with_timestamp(stage) context 'if deployment to some stages is unsuccessful' do it 'only adds the tag to the deployment stages have succeeded' do - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(false) - expect(Kernel).to_not receive(:system).with('bundle exec cap demo deploy target=test') + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.epigenesys deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.genesys deploy").and_return(false) + expect(Kernel).to_not receive(:system).with("BRANCH=#{release.commit} bundle exec cap demo deploy") expect(git_wrapper).to receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.epigenesys'), release.commit) expect(git_wrapper).to_not receive(:create_or_update_tag).with(deployment_stage_with_timestamp('production.genesys'), release.commit) @@ -97,8 +97,8 @@ def deployment_stage_with_timestamp(stage) end it 'deletes branches for all deployment environments' do - expect(Kernel).to receive(:system).with('bundle exec cap production.epigenesys deploy target=test').and_return(true) - expect(Kernel).to receive(:system).with('bundle exec cap production.genesys deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.epigenesys deploy").and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production.genesys deploy").and_return(true) expect(git_wrapper).to receive(:delete_branches).with(a_collection_containing_exactly('production', 'demo')) @@ -112,7 +112,7 @@ def deployment_stage_with_timestamp(stage) end it "runs the capistrano deploy task for single-customer environments" do - expect(Kernel).to receive(:system).with('bundle exec cap demo deploy target=test').and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap demo deploy").and_return(true) expect do subject.deploy! %w(demo) @@ -120,7 +120,7 @@ def deployment_stage_with_timestamp(stage) end it 'runs the capistrano deploy_all task for multi-customer environments' do - expect(Kernel).to receive(:system).with('bundle exec cap production deploy_all target=test').and_return(true) + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap production deploy_all").and_return(true) expect do subject.deploy! %w(production) From ab67eebcc9871613a5cc177cedb5712b0c7a2d2d Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 14:46:22 +0100 Subject: [PATCH 48/65] Use require instead of require_relative in epi_deploy:set_branch task --- lib/capistrano/tasks/branches.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capistrano/tasks/branches.rb b/lib/capistrano/tasks/branches.rb index d1664cb..0659c79 100644 --- a/lib/capistrano/tasks/branches.rb +++ b/lib/capistrano/tasks/branches.rb @@ -1,5 +1,5 @@ if File.exist?('config/initializers/version.rb') - require_relative 'config/initializers/version' + require 'config/initializers/version' end namespace :epi_deploy do From 2224f1b5d67039a8a4707ed6b5d10e367986c05f Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 14:47:49 +0100 Subject: [PATCH 49/65] Use absolute path for requiring version.rb --- lib/capistrano/tasks/branches.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capistrano/tasks/branches.rb b/lib/capistrano/tasks/branches.rb index 0659c79..b3d43ce 100644 --- a/lib/capistrano/tasks/branches.rb +++ b/lib/capistrano/tasks/branches.rb @@ -1,5 +1,5 @@ if File.exist?('config/initializers/version.rb') - require 'config/initializers/version' + require File.join(Dir.pwd, 'config/initializers/version') end namespace :epi_deploy do From 969c10643aecb45bb8aa3874380c7918b0b1ab9a Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 15:22:45 +0100 Subject: [PATCH 50/65] Dereference tags to commit if a tag name is used as the reference --- lib/epi_deploy/git_wrapper.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 58eee80..e3bfe77 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -42,13 +42,7 @@ def get_commit(git_reference) git_reference = tag_list.first end - git_object_type = git.lib.object_type(git_reference) - - case git_object_type - when 'tag' then git.tag(git_reference) - when 'commit' then git.object(git_reference) - else nil - end + git.object(commit_hash_for(git_reference)) end def create_or_update_tag(name, commit = nil, push: true) @@ -86,6 +80,10 @@ def most_recent_commit git.log(1).first end + def commit_hash_for(ref) + `git rev-list -n1 #{ref}`.strip + end + private def git From e79b3e44c8a5451af47b6288e47a6116aa44b8a0 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 15:46:28 +0100 Subject: [PATCH 51/65] Remove setup class, and add latest_release_tag to AppVersion --- lib/epi_deploy/app_version.rb | 54 +++++++++++++++++++++-------------- lib/epi_deploy/command.rb | 4 +-- lib/epi_deploy/release.rb | 7 +++-- lib/epi_deploy/setup.rb | 13 --------- 4 files changed, 39 insertions(+), 39 deletions(-) delete mode 100644 lib/epi_deploy/setup.rb diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index b273bfd..b41ee4b 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -1,38 +1,50 @@ module EpiDeploy class AppVersion - attr_reader :version_file_path + attr_accessor :version_file_path, :version, :latest_release_tag, def initialize(current_dir = Dir.pwd) - @version_file_path = File.join(current_dir, 'config/initializers/version.rb') + self.version_file_path = File.join(current_dir, 'config/initializers/version.rb') end - - def initial_version_file_if_required - self.version = 0 unless version_file_exists? - end - - def bump! - self.version = version + 1 - end - - def version - @version ||= extract_version_number + + def load + if version_file_exists? + File.open(version_file_path) do |f| + contents = f.read + self.version = extract_version_number(contents) + self.latest_release_tag = extract_latest_release_tag(contents) + end + else + self.version = 0 + self.latest_release_tag = '' + end end - - def version=(new_version) + + def save! File.open version_file_path, 'w' do |f| - f.write "APP_VERSION = '#{new_version}'" + f.write "APP_VERSION = '#{new_version}'\n" + f.write "LATEST_RELEASE_TAG = '#{latest_release_tag}'\n" end - @version = extract_version_number end - + + def bump! + self.version += 1 + end + private def version_file_exists? File.exist? version_file_path end - def extract_version_number - return 0 unless version_file_exists? - File.read(version_file_path).match(/APP_VERSION = '(?\d+).*'/)[:version].to_i + def extract_version_number(contents) + contents.match(/APP_VERSION = '(?\d+).*'/)[:version].to_i + end + + def extract_latest_release_tag(contents) + if (match = contents.match(/LATEST_RELEASE_TAG = '(?[A-za-z0-9_-])+'/)) + match[:tag] + else + '' + end end end diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index dbe29b5..3f0b337 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -15,9 +15,7 @@ def initialize(options, args, release_class = EpiDeploy::Release) self.release_class = release_class end - def release(setup_class = EpiDeploy::Setup) - setup_class.initial_setup_if_required - + def release release = self.release_class.new if release.create! print_success "Release #{release.version} created with tag #{release.tag}" diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index a51a56d..6b3795b 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -20,13 +20,16 @@ def create! if git_wrapper.most_recent_commit.message.start_with? 'Bumped to version' false else - new_version = app_version.bump! + new_version = app_version.bump git_wrapper.add(app_version.version_file_path) git_wrapper.commit "Bumped to version #{new_version} [skip ci]" - + self.tag = "#{date_and_time_for_tag}-#{git_wrapper.short_commit_hash}-v#{new_version}" + app_version.latest_release_tag = self.tag git_wrapper.create_or_update_tag(self.tag, push: false) git_wrapper.push(git_wrapper.current_branch, tags: true) + + app_version.save! true end rescue ::Git::GitExecuteError => e diff --git a/lib/epi_deploy/setup.rb b/lib/epi_deploy/setup.rb deleted file mode 100644 index 95ee1b2..0000000 --- a/lib/epi_deploy/setup.rb +++ /dev/null @@ -1,13 +0,0 @@ -module EpiDeploy - class Setup - - def self.initial_setup_if_required - app_version.initial_version_file_if_required - end - - private - def self.app_version(klass = EpiDeploy::AppVersion) - @app_version ||= klass.new - end - end -end \ No newline at end of file From 6f2a0141c762cefdb4e6464ecd16b5c17659dad8 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 3 Oct 2024 16:36:11 +0100 Subject: [PATCH 52/65] Make the default tag the latest release tag, rather than any tag --- lib/epi_deploy/command.rb | 2 +- lib/epi_deploy/git_wrapper.rb | 17 ++++++----------- lib/epi_deploy/release.rb | 17 ++++++++++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index 3f0b337..0a11380 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -49,7 +49,7 @@ def prompt_for_a_release print_notice "Select a recent release (or just press enter for latest):" tag_list = {} - self.release_class.new.tag_list.each_with_index do |release, i| + self.release_class.new.release_tags_list.each_with_index do |release, i| number = i + 1 tag_list[number.to_s] = release print_notice "#{number}: #{release}" diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index e3bfe77..3f8895e 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -36,15 +36,6 @@ def short_commit_hash git.log.first.sha[0..6] end - def get_commit(git_reference) - if git_reference == :latest - print_failure_and_abort("There is no latest release. Create one, or specify a reference with --ref") if tag_list.empty? - git_reference = tag_list.first - end - - git.object(commit_hash_for(git_reference)) - end - def create_or_update_tag(name, commit = nil, push: true) if push git.push('origin', "refs/tags/#{name}", delete: true) @@ -80,8 +71,8 @@ def most_recent_commit git.log(1).first end - def commit_hash_for(ref) - `git rev-list -n1 #{ref}`.strip + def git_object_for(ref) + git.object(commit_hash_for(ref)) end private @@ -104,5 +95,9 @@ def run_custom_command(command) raise ::Git::GitExecuteError.new("Failed to run command '#{command}'") end end + + def commit_hash_for(ref) + `git rev-list -n1 #{ref}`.strip + end end end diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 6b3795b..86fad00 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -41,8 +41,10 @@ def version app_version.version end - def tag_list(options = nil) - git_wrapper.tag_list(options) + def release_tags_list(options = nil) + git_wrapper.tag_list(options).filter do |tag| + tag.match?(/\A\d{4}[a-z]{3}\d{2}-\d{4}-[0-9a-f]+-v\d+\z/) + end end def git_wrapper(klass = EpiDeploy::GitWrapper) @@ -51,7 +53,7 @@ def git_wrapper(klass = EpiDeploy::GitWrapper) def self.find(reference) release = self.new - commit = release.git_wrapper.get_commit(reference) + commit = self.get_commit(reference) print_failure_and_abort("Cannot find commit for reference '#{reference}'") if commit.nil? release.commit = commit release.reference = reference @@ -70,5 +72,14 @@ def date_and_time_for_tag(time_class = (Time.respond_to?(:zone) ? Time.zone : Ti time.strftime "%Y#{MONTHS[time.month - 1]}%d-%H%M" end + def get_commit(git_reference) + if git_reference == :latest + print_failure_and_abort("There is no latest release. Create one, or specify a reference with --ref") if self.release_tags_list.empty? + git_reference = release_tags_list.first + end + + git_wrapper.git_object_for(git_reference) + end + end end From de6b42312b4f53965607ca59026ecfc78d550aef Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 11:58:18 +0100 Subject: [PATCH 53/65] Add AppVersion.open method --- lib/epi_deploy/app_version.rb | 6 ++++++ lib/epi_deploy/release.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index b41ee4b..a89a6ab 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -30,6 +30,12 @@ def bump! self.version += 1 end + def self.open(current_dir = Dir.pwd) + app_version = self.new(current_dir) + app_version.load + app_version + end + private def version_file_exists? File.exist? version_file_path diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 86fad00..99536a0 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -63,7 +63,7 @@ def self.find(reference) private def app_version(app_version_class = EpiDeploy::AppVersion) - @app_version ||= app_version_class.new + @app_version ||= app_version_class.open end # Use Time.zone if we have it (i.e. Rails), otherwise use Time From 00afe710131894cdeb2a4096850970c2e221e9c8 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 11:58:35 +0100 Subject: [PATCH 54/65] Fix AppVersion#initialize method --- lib/epi_deploy/app_version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index a89a6ab..542d276 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -1,6 +1,6 @@ module EpiDeploy class AppVersion - attr_accessor :version_file_path, :version, :latest_release_tag, + attr_accessor :version_file_path, :version, :latest_release_tag def initialize(current_dir = Dir.pwd) self.version_file_path = File.join(current_dir, 'config/initializers/version.rb') From 0dcb970734fb2906f8d5dd2c89523fd8c1d7cf1e Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 11:59:10 +0100 Subject: [PATCH 55/65] Rename bump! method to bump --- lib/epi_deploy/app_version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index 542d276..3f8ccd1 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -26,7 +26,7 @@ def save! end end - def bump! + def bump self.version += 1 end From 4fa3205f2c1bc8829da2d87c2f84eb87b55f2b13 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 11:59:29 +0100 Subject: [PATCH 56/65] Fix saving of app version file --- lib/epi_deploy/app_version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index 3f8ccd1..1477d99 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -21,7 +21,7 @@ def load def save! File.open version_file_path, 'w' do |f| - f.write "APP_VERSION = '#{new_version}'\n" + f.write "APP_VERSION = '#{version}'\n" f.write "LATEST_RELEASE_TAG = '#{latest_release_tag}'\n" end end From c6a0b08d1994ff639a52ed551df55696682dc0d9 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 12:05:53 +0100 Subject: [PATCH 57/65] Fix release creation to create app version save file before creating release --- lib/epi_deploy/release.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/epi_deploy/release.rb b/lib/epi_deploy/release.rb index 99536a0..cd76d4b 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -21,15 +21,15 @@ def create! false else new_version = app_version.bump - git_wrapper.add(app_version.version_file_path) - git_wrapper.commit "Bumped to version #{new_version} [skip ci]" - self.tag = "#{date_and_time_for_tag}-#{git_wrapper.short_commit_hash}-v#{new_version}" app_version.latest_release_tag = self.tag + app_version.save! + git_wrapper.add(app_version.version_file_path) + git_wrapper.commit "Bumped to version #{new_version} [skip ci]" + git_wrapper.create_or_update_tag(self.tag, push: false) git_wrapper.push(git_wrapper.current_branch, tags: true) - app_version.save! true end rescue ::Git::GitExecuteError => e @@ -41,8 +41,8 @@ def version app_version.version end - def release_tags_list(options = nil) - git_wrapper.tag_list(options).filter do |tag| + def release_tags_list + git_wrapper.tag_list.filter do |tag| tag.match?(/\A\d{4}[a-z]{3}\d{2}-\d{4}-[0-9a-f]+-v\d+\z/) end end @@ -53,7 +53,7 @@ def git_wrapper(klass = EpiDeploy::GitWrapper) def self.find(reference) release = self.new - commit = self.get_commit(reference) + commit = release.send(:get_commit, reference) print_failure_and_abort("Cannot find commit for reference '#{reference}'") if commit.nil? release.commit = commit release.reference = reference From 1f5a902dc13849d73ac3c3fdd2c9133e81917fd7 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 12:06:32 +0100 Subject: [PATCH 58/65] Fix tests --- spec/lib/epi_deploy/command_spec.rb | 16 ++++------------ spec/lib/epi_deploy/release_spec.rb | 7 +++---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/spec/lib/epi_deploy/command_spec.rb b/spec/lib/epi_deploy/command_spec.rb index 2019790..02f35d5 100644 --- a/spec/lib/epi_deploy/command_spec.rb +++ b/spec/lib/epi_deploy/command_spec.rb @@ -37,18 +37,10 @@ def tag_list describe "release" do subject { EpiDeploy::Command.new options, args, MockRelease } - let(:setup_class) { double(initial_setup_if_required: true) } - - describe "preconditions" do - it "sets up the initial bra nches if they don't exist" do - expect(setup_class).to receive :initial_setup_if_required - subject.release(setup_class) - end - end - + specify "the user is notified of success" do expect(subject).to receive_messages(print_success: "Release v5 created with tag nice-taggy") - subject.release(setup_class) + subject.release end describe "optional --deploy flag" do @@ -62,12 +54,12 @@ def tag_list it "deploys to the specified environments if options specified" do subject.options = MockOptions.new deploy: %w(production) expect(subject).to receive(:deploy).with(%w(production)) - subject.release(setup_class) + subject.release end it "does not deploy if option not specified" do expect(subject).to_not receive(:deploy) - subject.release(setup_class) + subject.release end end end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index fb91c48..d97f3c0 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -25,8 +25,9 @@ def delete_branches(branches); end describe EpiDeploy::Release do let(:git_wrapper) { MockGit.new } + let(:app_version) { double(bump: 42, version_file_path: '', :latest_release_tag= => nil, save!: true) } before do - allow(subject).to receive_messages(reference: 'test', git_wrapper: git_wrapper, commit: 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b', app_version: double(bump!: 42, version_file_path: '')) + allow(subject).to receive_messages(reference: 'test', git_wrapper: git_wrapper, commit: 'caa2c06f96cb0e52cdc6059014bc69bd94573d7a592b8c380bca5348e1f6806e0e9ad9bd12d7a78b', app_version: app_version) allow(git_wrapper).to receive(:most_recent_commit).and_return(double('commit', message: 'Some non-release commit')) end @@ -62,9 +63,7 @@ def delete_branches(branches); end end it "bumps the version number" do - app_version = double version_file_path: '' - allow(subject).to receive_messages(app_version: app_version) - expect(app_version).to receive(:bump!) + expect(app_version).to receive(:bump) expect(subject.create!).to eq true end From df6112cffa8e5f96bd3ee150c5ea3f705d670ae7 Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 7 Oct 2024 12:34:46 +0100 Subject: [PATCH 59/65] Update README to add instructions to include Capistrano tasks in Capfile for deployment tags --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46e87f7..48f06b5 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,13 @@ Tags will be automatically created for each successful deployment with the forma EpiDeploy.use_timestamped_deploy_tags = true ``` -1. Push your changes and deploy to a demo site to test it is working correctly. +1. If it hasn't be added already, add this line to `Capfile` + + ```rb + require 'capistrano/epi_deploy' + ``` + +1. Commit and push your changes, then deploy to a demo site to test it is working correctly. If you've previously used the `use_tags_for_deploy` configuration option, then this has now been removed since v2.3. If you upgrade to v2.3, then you should remove the old deployment tags manually from your local repo and remotely by doing, e.g. From 5944114a435e0834a9cf8cdca005dd7ef9224e72 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 16 Oct 2024 16:28:59 +0100 Subject: [PATCH 60/65] Add tests for the release setting the latest release tag in the version.rb when the release is created --- spec/lib/epi_deploy/release_spec.rb | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index d97f3c0..49488ee 100644 --- a/spec/lib/epi_deploy/release_spec.rb +++ b/spec/lib/epi_deploy/release_spec.rb @@ -75,13 +75,24 @@ def delete_branches(branches); end expect(subject.create!).to eq true end - it "creates a tag in the format YYYYmonDD-HHMM-CommitRef-version for the new commit" do - allow(subject).to receive_messages bump_version: 42 - now = Time.new 2014, 12, 1, 16, 15 - allow(Time).to receive_messages now: now - expect(git_wrapper).to receive(:create_or_update_tag).with('2014dec01-1615-abc1234-v42', push: false) + context 'given that the date and time is 2024-12-1 16:15:00' do + before do + now = Time.new 2014, 12, 1, 16, 15 + allow(Time).to receive_messages now: now + end - expect(subject.create!).to eq true + it 'sets the latest release tag in the version the newly-created tag' do + expect(app_version).to receive(:latest_release_tag=).with('2014dec01-1615-abc1234-v42') + + expect(subject.create!).to eq true + end + + it "creates a tag in the format YYYYmonDD-HHMM-CommitRef-version for the new commit" do + allow(subject).to receive_messages bump_version: 42 + expect(git_wrapper).to receive(:create_or_update_tag).with('2014dec01-1615-abc1234-v42', push: false) + + expect(subject.create!).to eq true + end end it "pushes the new version to primary branch to reduce the chance of version number collisions" do From 16d82d441a1be208ce2ca49f9dc17bd45c652371 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 16 Oct 2024 17:09:00 +0100 Subject: [PATCH 61/65] Add tests for app version --- lib/epi_deploy/app_version.rb | 2 +- spec/lib/epi_deploy/app_version_spec.rb | 134 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 spec/lib/epi_deploy/app_version_spec.rb diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index 1477d99..2447209 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -46,7 +46,7 @@ def extract_version_number(contents) end def extract_latest_release_tag(contents) - if (match = contents.match(/LATEST_RELEASE_TAG = '(?[A-za-z0-9_-])+'/)) + if (match = contents.match(/LATEST_RELEASE_TAG = '(?[A-Za-z0-9_-]+)'/)) match[:tag] else '' diff --git a/spec/lib/epi_deploy/app_version_spec.rb b/spec/lib/epi_deploy/app_version_spec.rb new file mode 100644 index 0000000..fa7a3c6 --- /dev/null +++ b/spec/lib/epi_deploy/app_version_spec.rb @@ -0,0 +1,134 @@ +require 'fileutils' + +require 'spec_helper' + +require 'epi_deploy/app_version' + +RSpec.describe EpiDeploy::AppVersion do + let(:current_dir) { File.expand_path(File.join('../../../../tmp/test_directory'), __FILE__) } + let(:initializers_dir) { File.join(current_dir, 'config/initializers') } + let(:version_file_path) { File.join(initializers_dir, 'version.rb') } + + subject { described_class.new(current_dir) } + + before do + FileUtils.mkdir_p(initializers_dir) + end + + around do |example| + begin + example.run + ensure + if File.exist? version_file_path + File.unlink(version_file_path) + end + end + end + + describe '#load' do + context 'given a version file does not exist' do + it 'defaults the app version to 0' do + subject.load + + expect(subject.version).to eq 0 + end + + it 'defaults to the latest release tag to an empty string' do + subject.load + + expect(subject.latest_release_tag).to eq '' + end + end + + context 'given a version file does exist' do + context 'and only a version is set in the file' do + before do + File.open(version_file_path, 'w') do |f| + f.write("APP_VERSION = '3'") + end + end + + it 'loads the version number' do + subject.load + + expect(subject.version).to eq 3 + end + + it 'defaults to the latest release tag to an empty string' do + subject.load + + expect(subject.latest_release_tag).to eq '' + end + end + + context 'and both a version number and latest release tag is set in the file' do + before do + File.open(version_file_path, 'w') do |f| + f.write("APP_VERSION = '3'\nLATEST_RELEASE_TAG = 'test_tag'") + end + end + + it 'loads the correct version number' do + subject.load + + expect(subject.version).to eq 3 + end + + it 'loads the latest release tag' do + subject.load + + expect(subject.latest_release_tag).to eq 'test_tag' + end + end + end + end + + describe '#save!' do + subject do + app_version = described_class.new(current_dir) + app_version.version = 13 + app_version.latest_release_tag = 'another_test_tag' + app_version + end + + context 'given a version file does not exist' do + it 'creates a new file with the correct contents' do + subject.save! + + expect(File).to exist version_file_path + File.open(version_file_path) do |f| + contents = f.read + expect(contents).to eq "APP_VERSION = '13'\nLATEST_RELEASE_TAG = 'another_test_tag'\n" + end + end + end + + context 'given a version file does exist' do + before do + File.open(version_file_path, 'w') do |f| + f.write("APP_VERSION = '3'\nLATEST_RELEASE_TAG = 'test_tag'") + end + end + + it 'creates a new file with the correct contents' do + subject.save! + + File.open(version_file_path) do |f| + contents = f.read + expect(contents).to eq "APP_VERSION = '13'\nLATEST_RELEASE_TAG = 'another_test_tag'\n" + end + end + end + end + + describe '.open' do + let(:app_version_double) { double('app version') } + + it 'creates a new app version class instance and loads the configuration from file' do + expect(described_class).to receive(:new).and_return(app_version_double) + expect(app_version_double).to receive(:load) + + described_class.open + end + end +end From d5bfc54602c92c569c05104cd040094d4f6d2679 Mon Sep 17 00:00:00 2001 From: William Lee Date: Wed, 16 Oct 2024 17:09:55 +0100 Subject: [PATCH 62/65] Make regexes for version.rb more resilient to whitespace differences --- lib/epi_deploy/app_version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index 2447209..6df313e 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -42,11 +42,11 @@ def version_file_exists? end def extract_version_number(contents) - contents.match(/APP_VERSION = '(?\d+).*'/)[:version].to_i + contents.match(/APP_VERSION\s*=\s*'(?\d+).*'/)[:version].to_i end def extract_latest_release_tag(contents) - if (match = contents.match(/LATEST_RELEASE_TAG = '(?[A-Za-z0-9_-]+)'/)) + if (match = contents.match(/LATEST_RELEASE_TAG\s*=\s*'(?[A-Za-z0-9_-]+)'/)) match[:tag] else '' From b19bdafbce814039074a00ce53e5f6d62c7ce28f Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 21 Oct 2024 09:32:06 +0100 Subject: [PATCH 63/65] Do not define set_branch cap task if using default deployment behaviour --- lib/capistrano/tasks/branches.rb | 34 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/capistrano/tasks/branches.rb b/lib/capistrano/tasks/branches.rb index b3d43ce..437b65a 100644 --- a/lib/capistrano/tasks/branches.rb +++ b/lib/capistrano/tasks/branches.rb @@ -1,21 +1,25 @@ -if File.exist?('config/initializers/version.rb') - require File.join(Dir.pwd, 'config/initializers/version') +require_relative '../../epi_deploy/config' + +Dir["config/initializers/version.rb", "config/epi_deploy.rb"].each do |file| + require File.join(Dir.pwd, file) end -namespace :epi_deploy do - task :set_branch do - branch = if ENV['BRANCH'] - ENV['BRANCH'] - elsif Object.const_defined?('LATEST_RELEASE_TAG') - LATEST_RELEASE_TAG - end +if EpiDeploy.use_timestamped_deploy_tags + namespace :epi_deploy do + task :set_branch do + branch = if ENV['BRANCH'] + ENV['BRANCH'] + elsif Object.const_defined?('LATEST_RELEASE_TAG') + LATEST_RELEASE_TAG + end - if branch.nil? - raise 'Cannot determine commit to deploy as BRANCH environment variable is not set and LATEST_RELEASE_TAG constant in version.rb could not be found' - end + if branch.nil? + raise 'Cannot determine commit to deploy as BRANCH environment variable is not set and LATEST_RELEASE_TAG constant in version.rb could not be found' + end - set :branch, branch + set :branch, branch + end end -end -before 'deploy:starting', 'epi_deploy:set_branch' + before 'deploy:starting', 'epi_deploy:set_branch' +end From 2a48c389d4e7b5c211bdcb1ace215df17f78944d Mon Sep 17 00:00:00 2001 From: William Lee Date: Mon, 21 Oct 2024 09:38:20 +0100 Subject: [PATCH 64/65] Fix creation of branches for default behaviour --- lib/epi_deploy/git_wrapper.rb | 2 +- spec/lib/epi_deploy/git_wrapper_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index 3f8895e..77f9509 100644 --- a/lib/epi_deploy/git_wrapper.rb +++ b/lib/epi_deploy/git_wrapper.rb @@ -82,7 +82,7 @@ def git end def force_create_branch(name, commit) - run_custom_command("git branch -f refs/heads/#{name} #{commit}") + run_custom_command("git branch -f #{name} #{commit}") end def local_branches(branch_names = []) diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 582b503..9c42049 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -23,7 +23,7 @@ describe '#create_or_update_branch' do it 'creates or moves the branch to the commit' do - expect(Kernel).to receive(:system).with("git branch -f refs/heads/production #{commit}").and_return(true) + expect(Kernel).to receive(:system).with("git branch -f production #{commit}").and_return(true) expect(mocked_git).to receive(:push).with('origin', 'refs/heads/production', force: true, tags: false ) subject.create_or_update_branch('production', commit) From 789e95493ff6a43da0bc59797d70358e6ec99864 Mon Sep 17 00:00:00 2001 From: William Lee Date: Thu, 31 Oct 2024 15:44:21 +0000 Subject: [PATCH 65/65] Add deploy- prefix to deployment tags to avoid conflict with tags that start with demo that are used for deployment purposes --- README.md | 2 +- lib/epi_deploy/deployer.rb | 2 +- spec/lib/epi_deploy/deployer_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48f06b5..fce79f8 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ You can also deploy to all customers for a given environment by running e.g. `ca Using branches for stages, i.e. demo and production branches, can clutter up your branches screen. This can be particularly awkward when running CI and keeping track of multiple active branches. To resolve this you can optionally configure epi_deploy to use tags for this instead of branches. -Tags will be automatically created for each successful deployment with the format `environment.stage-timestamp`, for example `production.epigenesys-2024_10_03-12_20_09`, and pushed to the remote. +Tags will be automatically created for each successful deployment with the format `deploy-.-`, for example `deploy-production.epigenesys-2024_10_03-12_20_09`, and pushed to the remote. 1. Change the line in your Gemfile to this, to ensure that you have version 2.3 or greater. diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb index c138a30..0e693e4 100644 --- a/lib/epi_deploy/deployer.rb +++ b/lib/epi_deploy/deployer.rb @@ -81,7 +81,7 @@ def stages_extractor def tag_name_for_stage(stage) timestamp = Time.now.strftime('%Y_%m_%d-%H_%M_%S') - "#{stage}-#{timestamp}" + "deploy-#{stage}-#{timestamp}" end def run_cap_deploy_to(environment) diff --git a/spec/lib/epi_deploy/deployer_spec.rb b/spec/lib/epi_deploy/deployer_spec.rb index 45cbfc1..fa145a7 100644 --- a/spec/lib/epi_deploy/deployer_spec.rb +++ b/spec/lib/epi_deploy/deployer_spec.rb @@ -25,8 +25,8 @@ def delete_branches(branches); end def deployment_stage_with_timestamp(stage) satisfy do |tag_name| - tag_stage, timestamp = tag_name.split('-', 2) - tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) + deploy_prefix, tag_stage, timestamp = tag_name.split('-', 3) + deploy_prefix == 'deploy' && tag_stage == stage && (Time.now - Time.strptime(timestamp, '%Y_%m_%d-%H_%M_%S') <= 5) end end