Last active
January 30, 2017 20:35
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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