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 diff --git a/README.md b/README.md index 23d706a..fce79f8 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,41 @@ 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 `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. + + ```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. 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. + +```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. 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/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" 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..437b65a --- /dev/null +++ b/lib/capistrano/tasks/branches.rb @@ -0,0 +1,25 @@ +require_relative '../../epi_deploy/config' + +Dir["config/initializers/version.rb", "config/epi_deploy.rb"].each do |file| + require File.join(Dir.pwd, file) +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 + + set :branch, branch + end + end + + before 'deploy:starting', 'epi_deploy:set_branch' +end 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 diff --git a/lib/epi_deploy/app_version.rb b/lib/epi_deploy/app_version.rb index b273bfd..6df313e 100644 --- a/lib/epi_deploy/app_version.rb +++ b/lib/epi_deploy/app_version.rb @@ -1,38 +1,56 @@ 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 = '#{version}'\n" + f.write "LATEST_RELEASE_TAG = '#{latest_release_tag}'\n" end - @version = extract_version_number end - + + 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 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\s*=\s*'(?\d+).*'/)[:version].to_i + end + + def extract_latest_release_tag(contents) + if (match = contents.match(/LATEST_RELEASE_TAG\s*=\s*'(?[A-Za-z0-9_-]+)'/)) + match[:tag] + else + '' + end end end diff --git a/lib/epi_deploy/cli.rb b/lib/epi_deploy/cli.rb new file mode 100644 index 0000000..46d6b32 --- /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 + + 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 diff --git a/lib/epi_deploy/command.rb b/lib/epi_deploy/command.rb index cc439f9..0a11380 100644 --- a/lib/epi_deploy/command.rb +++ b/lib/epi_deploy/command.rb @@ -15,12 +15,13 @@ 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 - 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 recent commit" + end environments = self.options.to_hash[:deploy] self.deploy(environments) unless environments.nil? end @@ -32,7 +33,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." @@ -47,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/config.rb b/lib/epi_deploy/config.rb index f67e04d..d31f997 100644 --- a/lib/epi_deploy/config.rb +++ b/lib/epi_deploy/config.rb @@ -1,11 +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) - @@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 diff --git a/lib/epi_deploy/deployer.rb b/lib/epi_deploy/deployer.rb new file mode 100644 index 0000000..0e693e4 --- /dev/null +++ b/lib/epi_deploy/deployer.rb @@ -0,0 +1,99 @@ +require 'git' + +require_relative 'helpers' +require_relative 'stages_extractor' + +module EpiDeploy + class Deployer + include Helpers + + def initialize(release) + @release = release + end + + 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) + + 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 + 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 + 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) + 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 + 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') + "deploy-#{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 "BRANCH=#{@release.commit} bundle exec cap #{environment} #{task_to_run}" + end + end +end \ No newline at end of file diff --git a/lib/epi_deploy/git_wrapper.rb b/lib/epi_deploy/git_wrapper.rb index cd82af8..77f9509 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) @@ -35,42 +36,27 @@ 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? - git_reference = tag_list.first + def create_or_update_tag(name, commit = nil, push: true) + if push + git.push('origin', "refs/tags/#{name}", delete: true) 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 - end + git.add_tag(name, commit, annotate: true, f: true, message: name) - 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) + if push + git.push('origin', "refs/tags/#{name}") end end - def update_branch_commit(stage, commit) - Kernel.system "git branch -f #{stage} #{commit}" - self.push stage, force: true, tags: true + def create_or_update_branch(name, commit) + force_create_branch(name, commit) + self.push "refs/heads/#{name}", force: true, tags: false 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 delete_branches(branches) + 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 @@ -81,11 +67,37 @@ def current_branch git.current_branch end + def most_recent_commit + git.log(1).first + end + + def git_object_for(ref) + git.object(commit_hash_for(ref)) + end + private def git @git ||= ::Git.open(Dir.pwd) end + def force_create_branch(name, commit) + run_custom_command("git branch -f #{name} #{commit}") + end + + def local_branches(branch_names = []) + branches = git.branches.local.filter { |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 + + 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 86833c0..cd76d4b 100644 --- a/lib/epi_deploy/release.rb +++ b/lib/epi_deploy/release.rb @@ -17,13 +17,21 @@ 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]" + if git_wrapper.most_recent_commit.message.start_with? 'Bumped to version' + false + else + new_version = app_version.bump + 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) - 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 + true + end rescue ::Git::GitExecuteError => e print_failure_and_abort "A git error occurred: #{e.message}" end @@ -33,37 +41,19 @@ def version app_version.version end - def deploy!(environments) - environments.each do |environment| - begin - git_wrapper.pull - - 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) - - completed = run_cap_deploy_to(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 + 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 - def tag_list(options = nil) - git_wrapper.tag_list(options) - end - def git_wrapper(klass = EpiDeploy::GitWrapper) @git_wrapper ||= klass.new end def self.find(reference) release = self.new - commit = release.git_wrapper.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 @@ -73,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 @@ -82,20 +72,13 @@ 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) - $stdout.puts "Deploying to #{environment}... " - - task_to_run = if stages_extractor.multi_customer_stage?(environment) - "deploy_all" - else - "deploy" + 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 - - Kernel.system "bundle exec cap #{environment} #{task_to_run} target=#{reference}" - end - - def stages_extractor - @stages_extractor ||= StagesExtractor.new + + git_wrapper.git_object_for(git_reference) end end 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 diff --git a/lib/epi_deploy/stages_extractor.rb b/lib/epi_deploy/stages_extractor.rb index 108fbf9..9afef4e 100644 --- a/lib/epi_deploy/stages_extractor.rb +++ b/lib/epi_deploy/stages_extractor.rb @@ -1,21 +1,36 @@ require 'set' + module EpiDeploy class StagesExtractor + STAGE_REGEX = /\A(?[\w\-]+)(?:\.(?\w+))?\z/ + DEPLOY_FILE_REGEX = /\A(?[\w\-]+)(?:\.(?[\w\-]+))?\.rb\z/ + attr_accessor :multi_customer_stages, :all_stages def initialize self.multi_customer_stages = Set.new - self.all_stages = Set.new - + @environment_to_stages = {} + 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] - all_stages << "#{matches[:stage]}.#{matches[:customer]}" - else - all_stages << matches[:stage] end + + @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) @@ -25,11 +40,32 @@ def multi_customer_stage?(stage) def valid_stage?(stage) all_stages.include?(stage) || multi_customer_stage?(stage) end - + + 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 + @environment_to_stages.keys + end + + def self.match_with(stage_or_environment) + stage_or_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/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' 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/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 diff --git a/spec/lib/epi_deploy/command_spec.rb b/spec/lib/epi_deploy/command_spec.rb index fc06d1f..02f35d5 100644 --- a/spec/lib/epi_deploy/command_spec.rb +++ b/spec/lib/epi_deploy/command_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -require 'epi_deploy/stages_extractor' require 'epi_deploy/command' +require 'epi_deploy/deployer' require 'slop' class MockOptions @@ -29,26 +29,18 @@ 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) { [] } 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,20 +54,24 @@ 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 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 +79,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 +105,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..fa145a7 --- /dev/null +++ b/spec/lib/epi_deploy/deployer_spec.rb @@ -0,0 +1,153 @@ +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(name, commit); end + def create_or_update_branch(name, commit); end + def delete_branches(branches); end +end + +def deployment_stage_with_timestamp(stage) + satisfy do |tag_name| + 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 + +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) } + + 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) + allow_any_instance_of(EpiDeploy::Helpers).to receive(:print_failure_and_abort) { raise system_exit } + end + + around do |example| + Dir.chdir(File.join(File.dirname(__FILE__), '../..', 'fixtures')) do + example.run + end + end + + context 'given that timestamped deployment tags are enabled' do + before do + allow(EpiDeploy).to receive(:use_timestamped_deploy_tags).and_return(true) + end + + it "runs the capistrano deploy command for each of the environments given" do + 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) + 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("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) + 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("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) + 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("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')) + + subject.deploy! ['production'] + end + end + + 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 + + it "runs the capistrano deploy task for single-customer environments" do + expect(Kernel).to receive(:system).with("BRANCH=#{release.commit} bundle exec cap demo deploy").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("BRANCH=#{release.commit} bundle exec cap production deploy_all").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) + + 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 diff --git a/spec/lib/epi_deploy/git_wrapper_spec.rb b/spec/lib/epi_deploy/git_wrapper_spec.rb index 019af7a..9c42049 100644 --- a/spec/lib/epi_deploy/git_wrapper_spec.rb +++ b/spec/lib/epi_deploy/git_wrapper_spec.rb @@ -5,37 +5,28 @@ 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) end - describe '#update_stage_tag_or_branch' do + 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', commit, any_args) + expect(mocked_git).to receive(:push).with('origin', 'refs/tags/production') - 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 + subject.create_or_update_tag 'production', commit 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 + 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', 'refs/heads/production', force: true, tags: false +) + subject.create_or_update_branch('production', commit) end end @@ -72,4 +63,62 @@ end end + describe '#delete_branches' do + let(:branches) { ['production', 'demo'] } + let(:local_branches) { [] } + + before do + allow(subject).to receive(:local_branches).and_return(local_branches) + end + + context 'if all the branches exist as local branches' do + let(:production_branch) { double('production branch', delete: true) } + let(:demo_branch) { double('demo branch', delete: true) } + let(:local_branches) { + [ + 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(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 + allow(subject).to receive(:run_custom_command) + expect(production_branch).to receive(:delete) + expect(demo_branch).to receive(:delete) + + subject.delete_branches(branches) + end + end + + context 'if not all the branches exist as local branches' do + let(:production_branch) { double('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(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 + 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 + end + end end diff --git a/spec/lib/epi_deploy/release_spec.rb b/spec/lib/epi_deploy/release_spec.rb index 8f6e879..49488ee 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' @@ -13,33 +15,36 @@ 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 update_stage_tag_or_branch(branch, commit); end def current_branch; 'main'; end + def create_or_update_tag(stage, commit); end + def delete_branches(branches); end 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, 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 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_not raise_error + 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_not raise_error + subject.create! end end @@ -47,62 +52,61 @@ def current_branch; 'main'; 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 - 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_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 - 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') + 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 + + 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 - expect { subject.create! }.to_not raise_error + 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 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 + expect(subject.create!).to eq true end - end - describe "#deploy!" do - it "runs the capistrano deploy command for each of the environments given" do - 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) + 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 do - # Suppress output from epiDeploy - allow_any_instance_of(IO).to receive(:puts) - - subject.deploy! %w(demo production) - end.to_not raise_error - end + expect(subject.create!).to eq false 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..c3a59cd 100644 --- a/spec/lib/epi_deploy/stages_extractor_spec.rb +++ b/spec/lib/epi_deploy/stages_extractor_spec.rb @@ -36,7 +36,37 @@ 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.multi_customer_stages).to match_array ['production'] + end + end + + 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 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