Skip to content

Instantly share code, notes, and snippets.

@seanski
Last active January 30, 2017 20:35
Show Gist options
  • Save seanski/3f734559b1165dfa782d to your computer and use it in GitHub Desktop.
Save seanski/3f734559b1165dfa782d to your computer and use it in GitHub Desktop.
Here is a little script that I wrote to manage capistrano deploys. It checks for asset changes and precompiles assets, if needed. It also looks for schema changes and will run migrations if there are any changes. It uses GitFlow for release creation.
#!/usr/bin/env ruby
require 'date'
require 'git'
class Releaser
attr_accessor :app_path, :repo, :skip_deploy, :version, :version_file
def initialize(app_path, version, version_file, skip_deploy=false)
raise ArgumentError.new('First argument must be version number in Major, Minor, Patch format, ex. 11.222.324') unless version.valid?
@app_path = app_path
@repo = Git.open(app_path)
@skip_deploy = skip_deploy
@version = version
@version_file = version_file
end
def run!
start_release
precompile_assets
bump_version
finish_release!
deploy_unless_skipped!
puts 'Finished!'
end
class << self
def run!(app_path, version, version_file, skip_deploy=false)
new(app_path, version, version_file, skip_deploy).run!
end
end
private
def start_release
command("git flow release start #{version}")
end
def repo_diff
@repo_diff ||= repo.diff('develop', 'master')
end
def needs_precompile?
has_asset_changes? || environment_changed?
end
def has_asset_changes?
repo_diff.any? { |d| d.path =~ /assets/ }
end
def environment_changed?
repo_diff.any? { |d| d.path =~ /production\.rb/ }
end
def precompile_assets
if needs_precompile?
puts 'Compiling assets...'
command('RAILS_ENV=production rake assets:precompile')
command('git add public/assets/*')
commit("Precompile assets for version #{version}")
else
puts 'No asset changes. Skipping precompile.'
end
end
def bump_version
puts "Bumping version to #{version}..."
File.write(version_file, version, mode: 'w')
commit("Bump version to #{version}")
end
def finish_release!
puts 'Finishing release...'
File.write('.git/RELEASE_TAG_MSG', "Release Date: #{Date.today.strftime('%Y-%m-%d')}", mode: 'w')
command("git flow release finish -F -f .git/RELEASE_TAG_MSG #{version}")
command('git push origin master develop')
end
def deploy_unless_skipped!
if not skip_deploy
puts 'Deploying application...'
deploy!
else
puts 'Skip deploy specified. Skipping deployment.'
end
end
def deploy!
if needs_migration?
puts 'Schema changes detected. Deploying app to production and running migrations...'
command('cap deploy:migrations')
else
puts 'No schema changes detected. Deploying app to production without running migrations...'
command('cap deploy')
end
end
def needs_migration?
repo_diff.any? { |d| d.path =~ /schema\.rb/ }
end
def command(command)
raise SystemCommandFailure.new unless system(command)
end
def commit(message)
repo.commit(message, all: true)
end
end
class SystemCommandFailure < Exception
def initialize
super('The system command failed to run. Please see console output for errors.')
end
end
class CommandLineExecutor
attr_reader :app_path, :current_version, :skip_deploy, :version_file
def initialize(app_path: CommandLineExecutor.app_path,
current_version: CommandLineExecutor.current_version,
skip_deploy: CommandLineExecutor.skip_deploy?,
version_file: CommandLineExecutor.version_file)
@app_path = app_path
if current_version.is_a?(Version)
@current_version = current_version
else
@current_version = Version.new(current_version)
end
@skip_deploy = skip_deploy
@version_file = version_file
end
def execute!
Releaser.run!(app_path, new_version, version_file, skip_deploy)
end
def new_version
self.class.new_version || get_version_from_user
end
class << self
def app_path
File.expand_path('../', __FILE__)
end
def current_version
Version.new(File.read(version_file))
end
def default_execute!
new.execute!
end
def increment
ARGV.grep(/(major|minor|patch)/).first
end
def new_version
if increment
current_version.send "next_#{increment}"
elsif version_string = ARGV.grep(/\d+\.\d+\.\d+/).first
Version.new(version_string)
end
end
def skip_deploy?
ARGV.include?('--skip-deploy')
end
def version_file
File.join(app_path, 'VERSION')
end
end
protected
def get_version_from_user
puts "Current version is #{current_version}"
while true
print "Enter new version number (#{current_version.next_patch}): "
input_version = STDIN.gets.chomp
return current_version.next_patch if input_version.empty?
version = Version.new(input_version)
if not version.valid?
puts 'Invalid version number. Format should be MAJOR.MINOR.PATCH, ex. 2.24.5'
loop
elsif version.increment_of?(current_version)
return version
else
continue = false
until continue == true
print "Version #{version} is not an increment to #{current_version}. Are you sure you want to continue? (Y/N): "
continue_input = STDIN.gets.chomp.upcase
if continue_input == 'Y'
return version
elsif continue_input == 'N'
continue = true
else
puts 'Invalid Input!'
end
end
end
end
end
end
class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(version = '')
self.major, self.minor, self.patch = if version.is_a?(String)
Version.version(version)
elsif version.is_a?(Array)
version
else
[0,0,0]
end
end
def increment_of?(other)
major_increment_of?(other) || minor_increment_of?(other) || patch_increment_of?(other)
end
def major=(value)
@major = valid_number(value)
end
def major_increment_of?(other)
part_increment_of?(:major, other) && minor == 0 && patch == 0
end
def next_major
Version.new([major + 1, 0, 0])
end
def next_minor
Version.new([major, minor + 1, 0])
end
def next_patch
Version.new([major, minor, patch + 1])
end
def minor=(value)
@minor = valid_number(value)
end
def minor_increment_of?(other)
major == other.major && patch == 0 && part_increment_of?(:minor, other)
end
def patch=(value)
@patch = valid_number(value)
end
def patch_increment_of?(other)
major == other.major && minor == other.minor && part_increment_of?(:patch, other)
end
def to_a
[major, minor, patch]
end
def valid?
to_a != [0, 0, 0]
end
def version_string
to_a.join('.')
end
alias_method :to_s, :version_string
def <=>(other)
if major < other.major && minor < other.minor && patch < other.patch
-1
elsif major > other.major && minor > other.minor && patch > other.patch
1
else
0
end
end
protected
def part_increment_of?(part, other)
self.send(part) - other.send(part) == 1
end
def valid_number(value)
value.to_i.abs
end
class << self
def valid?(version_string)
version_string.match(version_matcher)
end
def version(version_string)
if match = valid?(version_string)
[match[:major], match[:minor], match[:patch]].map(&:to_i)
else
[0,0,0]
end
end
protected
def version_matcher
/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d)+$/
end
end
end
CommandLineExecutor.default_execute!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment