Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
fail-fast: false

steps:
- uses: actions/checkout@v2
Expand Down
18 changes: 8 additions & 10 deletions epi_deploy.gemspec
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'epi_deploy/version'
# frozen_string_literal: true

require_relative 'lib/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", "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.summary = "A gem to facilitate deployment across multiple git branches and environments"
gem.homepage = "https://www.epigenesys.org.uk"

gem.files = `git ls-files`.split($/)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]
gem.files = Dir['README.md', 'LICENSE.txt', 'lib/**/*.rb', 'bin/*']
gem.executables = gem.files.grep(/^bin/).map{ |f| File.basename(f) }

gem.required_ruby_version = '>= 2.7', '< 3.5'

gem.add_dependency('slop', '~> 3.6')
gem.add_dependency('git', '~> 1.5')
Expand Down
6 changes: 5 additions & 1 deletion lib/capistrano/tasks/multi_customers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
task :revision do
on roles :app do |host|
within current_path do
info "#{fetch(:user)}@#{host}: #{capture :cat, 'REVISION'}"
if test "[ -f REVISION ]"
info "#{fetch(:user)}@#{host}: #{capture :cat, 'REVISION'}"
else
info "#{fetch(:user)}@#{host}: REVISION file not found"
end
end
end
end
Expand Down
37 changes: 17 additions & 20 deletions lib/epi_deploy/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

module EpiDeploy
class Command

include EpiDeploy::Helpers

attr_accessor :options
attr_accessor :args
attr_accessor :release_class

def initialize(options, args, release_class = EpiDeploy::Release)
self.options = options
self.args = args
Expand All @@ -25,7 +25,7 @@ def release
environments = self.options.to_hash[:deploy]
self.deploy(environments) unless environments.nil?
end

def deploy(environments = self.args)
raise Slop::InvalidArgumentError.new("No environments provided") unless environments.any?
check_environments_are_valid(environments)
Expand All @@ -41,32 +41,28 @@ def deploy(environments = self.args)
end
end
end



private

def prompt_for_a_release
print_notice "Select a recent release (or just press enter for latest):"

tag_list = {}
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}"
end

selected_release = nil
while selected_release.nil? do
selected_release = STDIN.gets[/\d/] rescue nil
if selected_release.nil?
return :latest
else
unless tag_list.key?(selected_release)
print_failure_and_abort "Invalid selection '#{selected_release}'. Try again..."
selected_release = nil
end
selected_release = STDIN.gets[/\d/] rescue nil
if selected_release.nil?
return :latest
else
unless tag_list.key?(selected_release)
print_failure_and_abort "Invalid selection '#{selected_release}'. Try again..."
end
end

tag_list[selected_release]
end

Expand All @@ -79,14 +75,15 @@ def determine_release_reference(options)
:latest
end
end

def check_environments_are_valid(environments)
invalid_environments = environments.reject { |environment| stages_extractor.valid_stage?(environment) }
raise Slop::InvalidArgumentError.new("Environment '#{invalid_environments.first}' does not exist") unless invalid_environments.empty?
end

def stages_extractor
@stages_extractor ||= StagesExtractor.new
end

end
end
40 changes: 15 additions & 25 deletions lib/epi_deploy/deployer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,33 @@ def deploy_with_timestamped_tags(stages_or_environments)
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|
stages_extractor.each_stage(stages_or_environments) do |stage|
completed = run_cap_deploy_to(stage)
if completed
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
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

def deploy_with_environment_branches(stages_or_environments)
updated_branches = Set.new

stages_or_environments.each do |stage_or_environment|
begin
git_wrapper.pull
git_wrapper.pull

matches = StagesExtractor.match_with(stage_or_environment)
stages_extractor.each_stage(stages_or_environments) do |stage|
begin
matches = StagesExtractor.match_with(stage)
# 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)
completed = run_cap_deploy_to(stage)
if !completed
print_failure_and_abort "Deployment failed - please review output before deploying again"
end
Expand All @@ -84,16 +81,9 @@ def tag_name_for_stage(stage)
"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}"
def run_cap_deploy_to(stage)
print_notice "Deploying to #{stage}... "
Kernel.system "BRANCH=#{@release.commit} bundle exec cap #{stage} deploy"
end
end
end
4 changes: 4 additions & 0 deletions lib/epi_deploy/git_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def git_object_for(ref)
git.object(commit_hash_for(ref))
end

def ancestor?(reference, of:)
system("git merge-base #{reference} #{of} --is-ancestor")
end

private

def git
Expand Down
37 changes: 37 additions & 0 deletions lib/epi_deploy/overwrite_detector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module EpiDeploy
class OverwriteDetector
attr_reader :release

def initialize(release)
@release = release
end

def release_overwrites?(stage)
@overwrites ||= {}
@overwrites[stage] ||= calculate_overwrite(stage)
end

private

def calculate_overwrite(stage)
revision = extract_revision(stage)
return nil if revision.nil?

!release.has_ancestor?(revision)
end

# returns nil if the regex does not match for two reasons:
# 1. the command failed to execute
# 2. the REVISION file was not found on the remote
def extract_revision(stage)
output = `bundle exec cap #{stage} deploy:revision`
match = output.match(/^\s*.*?@.*?: (?<hash>[A-Za-z0-9]{40})/)

if match && match[:hash]
match[:hash]
else
nil
end
end
end
end
4 changes: 4 additions & 0 deletions lib/epi_deploy/release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def release_tags_list
end
end

def has_ancestor?(reference)
git_wrapper.ancestor?(reference, of: commit)
end

def git_wrapper(klass = EpiDeploy::GitWrapper)
@git_wrapper ||= klass.new
end
Expand Down
13 changes: 12 additions & 1 deletion lib/epi_deploy/stages_extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def valid_stage?(stage)
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]
@environment_to_stages[stage_or_environment].to_a
elsif self.all_stages.include? stage_or_environment
# Stage
[stage_or_environment]
Expand All @@ -53,6 +53,17 @@ def stages_for_stage_or_environment(stage_or_environment)
end
end

# stages are sorted lexicographically within each stage or environment passed
def each_stage(stages_or_environments)
stages = stages_or_environments.flat_map do |stage_or_environment|
stages_for_stage_or_environment(stage_or_environment).sort
end

stages.each do |stage|
yield stage
end
end

def environments
@environment_to_stages.keys
end
Expand Down
5 changes: 3 additions & 2 deletions spec/lib/epi_deploy/deployer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ def deployment_stage_with_timestamp(stage)
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)
it 'runs the capistrano deploy task for each stage for multi-customer 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 do
subject.deploy! %w(production)
Expand Down
25 changes: 25 additions & 0 deletions spec/lib/epi_deploy/git_wrapper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,29 @@
end
end
end

describe '#ancestor?' do
let(:reference) { '275cf7e295b879a62526b13b4fb78e7b04935fe1' }
let(:of) { 'ae4ff8053b6beb3a9e57b8c4d59038edf7d4c8f9' }

context 'if the command exits with a status code of 0' do
before do
allow(subject).to receive(:system).with("git merge-base #{reference} #{of} --is-ancestor").and_return(true)
end

specify 'it returns true' do
expect(subject).to be_ancestor reference, of: of
end
end

context 'if the command exits with a status code of 0' do
before do
allow(subject).to receive(:system).with("git merge-base #{reference} #{of} --is-ancestor").and_return(false)
end

specify 'it returns false' do
expect(subject).to_not be_ancestor reference, of: of
end
end
end
end
Loading