Skip to content

Instantly share code, notes, and snippets.

@smashedlife
Last active August 29, 2015 14:01
Show Gist options
  • Save smashedlife/ed4436568137bc153abe to your computer and use it in GitHub Desktop.
Save smashedlife/ed4436568137bc153abe to your computer and use it in GitHub Desktop.
git-deploy
#!/usr/bin/env ruby
if ARGV[0].nil? || ARGV[0].empty?
deploy = 'deploy.yml'
else
deploy = ARGV[0] + '.yml'
end
unless File.exists? deploy
puts "File does not exist: " + deploy
exit
end
require 'rubygems'
require 'timeout'
require 'yaml'
require 'uri'
require 'net/ftp'
require 'net/ssh'
require 'net/sftp'
require 'tempfile'
require 'socket'
STDOUT.sync = true
# Ignored files
$ignore_file_list = ["deploy.yml", ".gitignore"]
# Store the current revision
revision = `git rev-parse HEAD`.chomp.strip
revision_file = Tempfile.new revision
revision_file.write revision
revision_file.close
service_failures = {}
services = YAML.load_file deploy
settings = services.delete('settings') || {}
trap 'INT' do
raise Interrupt, nil
end
module Git
class << self
def diff_uncommitted(local_path)
command = "git diff --name-status; git diff --cached --name-status"
changed_files(command)
end
# Figures out only which files need updating
# so don't upload duplicate files
def diff_committed(local_path, revision = nil)
remote_files = {}
if revision
command = "git diff --name-status #{revision} -- #{local_path}"
changed_files(command)
else
remote_files = {}
`git ls-files`.split("\n").each {|file| remote_files[file] = 'M'}
remote_files
end
end
def changed_files(command, init = nil)
files = `#{command}`
puts command
case $?.exitstatus
when 0, 141
# pass
else
return false
end
remote_files = {}
files.split("\n").reverse.each do |line|
c = line[0..0]
c = 'M' if c == 'A'
c = 'M' if c == 'C'
if init
c = 'M' if c == 'H'
end
next unless c == 'M' || c == 'D'
file = line[2..-1]
if not $ignore_file_list.include? file
if remote_files.key? file
if remote_files[file] == 'M' && c == 'D'
remote_files[file] = 'D'
elsif remote_files[file] == 'D' && c == 'M'
remote_files[file] = 'M'
end
else
remote_files[file] = c
end
end
end
remote_files
end
end
class Service
attr_reader :failures
def initialize(uri, options)
@uri = uri
@host = options['host']
@options = options
@scheme = options['scheme']
@failures = []
@max_retries = options['retries'] || 1
@max_retries = 1 if @max_retries <= 0
end
def automate
upload_maintenance_file = @options['maintenance_file'] && @options['maintenance_deploy_to']
tries = 1
until tries > @max_retries
self.connect
begin
if ! self.diff
self.disconnect
return
end
if upload_maintenance_file
puts "Uploading Maintenance file"
self.upload_maintenance_file
end
self.upload
self.upload_revision_file
if upload_maintenance_file
puts "Removing Maintenance file"
self.remove_maintenance_file
end
self.disconnect
return true
rescue Interrupt, Errno::ETIMEDOUT, Errno::ECONNRESET, Timeout::Error => e
tries += 1
@failures << e
puts "Interrupting...going to retry"
if e.is_a? Interrupt
begin
Timeout::timeout(5) do
self.disconnect
end
rescue
end
end
if tries == @max_tries
puts "Not retrying (#{tries} out of #{@max_retries})"
else
puts "Retrying (#{tries} out of #{@max_retries})"
end
rescue Net::FTPTempError, Net::FTPPermError => e
@failures << e
return false
end
end
tries != @max_retries
end
def diff
if @remote_files.is_a? Hash
return @remote_files
end
# Decide upload strategy
if ARGV[0] == 'diff' || ARGV[0] == 'd'
@remote_files = Git.diff_uncommitted(@options['local_path'])
else
@remote_files = self.diff_committed
end
end
def diff_committed
remote_revision = self.get_remote_revision
if !remote_revision
if !dir_empty?
puts 'Missing remote revision in non empty directory'
exit
else
remote_files = Git.diff_committed(@options['local_path'])
end
elsif @options['revision'] == remote_revision
if !@options['overwrite_if_same_revision']
puts "Same revision, ignoring"
return false
end
puts "Same revision, overwriting"
remote_files = Git.diff_committed(@options['local_path'])
else
remote_files = Git.diff_committed(@options['local_path'], "#{remote_revision}..HEAD")
end
end
def upload
length = @options['length']
@remote_files.each do |local_file, modifier|
if length != 0
if local_file[0...length] != @options['local_path']
next
else
remote_file = local_file[length..-1]
end
else
remote_file = local_file
end
case modifier
when 'A', 'M', 'C'
puts "Uploading #{local_file}"
self.put(local_file, remote_file)
when 'D'
puts "Deleting #{local_file}"
self.delete(remote_file)
end
@remote_files.delete(local_file)
end
end
def upload_revision_file
puts 'Uploading REVISION'
self.put(@options['revision_file'].path, "REVISION")
end
def upload_maintenance_file
self.put(@options['maintenance_file'], @options['maintenance_deploy_to'])
end
def remove_maintenance_file
self.put(@options['maintenance_deploy_to'], @options['maintenance_deploy_to'])
end
end
class SFTP < Service
def initialize(uri, options)
super(uri, options)
@path = options['path'].empty? ? options['chdir'] : options['path']
@remote_directories = {}
end
def connect
puts "Connecting to #{@host}"
sftp_options = {}
# sftp_options[:verbose] = :debug
sftp_options[:port] = @uri.port if @uri.port
sftp_options[:password] = @options['password'] if @options['password']
@sftp = Net::SFTP.start(@options['host'], @options['user'], sftp_options)
end
def disconnect
# @sftp.close!
end
def put(local_file, remote_file)
dir = File.dirname("#{@path}/#{remote_file}")
unless @remote_directories[dir]
self.mkdir_p(dir)
@remote_directories[dir] = true
end
@sftp.upload!(local_file, "#{@path}/#{remote_file}")
end
def delete(remote_file)
@sftp.remove!("#{@remote_file}")
end
def get_remote_revision
remote_revision = false
begin
@sftp.file.open("#{@path}/REVISION", "r") do |f|
remote_revision = f.gets.strip
end
puts "Remote Revision: #{remote_revision}"
rescue Net::SFTP::StatusException => e
raise e unless e.code == 2 && e.description == "no such file"
remote_revision = false
end
remote_revision
end
def dir_empty?
@sftp.dir.list("#{@path}", '*').empty?
end
def mkdir_p(directory)
begin
parent = File.dirname(directory)
files = @sftp.dir.glob(parent, "*")
if directory == "." || files.any? { |a| a == directory }
return
else
puts "Creating Directory #{directory}"
@sftp.mkdir(directory)
end
rescue Net::SFTP::StatusException
self.mkdir_p(File.dirname(directory))
begin
@sftp.mkdir(directory)
rescue Net::SFTP::StatusException
# pass
end
rescue Net::SFTP::StatusException
begin
puts "Creating Directory #{directory}"
@sftp.mkdir(directory)
rescue Net::SFTP::StatusException => e
self.mkdir_p(File.dirname(directory))
@sftp.mkdir(directory)
end
end
end
end
class FTP < Service
def initialize(uri, options)
super(uri, options)
@remote_directories = {}
@port = options['port'] || uri.port || 21
end
def connect
puts "Connecting to #{@host}"
@ftp = Net::FTP.new
if @options['passive']
@ftp.passive = true
end
if @options['debug_mode']
@ftp.debug_mode = true
end
@ftp.binary = true
@ftp.connect(@options['host'], @options['port'])
@ftp.login(@options['user'], @options['password'])
@ftp.chdir(@options['path'].empty? ? @options['chdir'] : '/' + @options['path'])
end
def disconnect
@ftp.quit
end
def put(local_file, remote_file)
dir = File.dirname(remote_file)
unless @remote_directories[dir]
self.mkdir_p(dir)
@remote_directories[dir] = true
end
@ftp.put(local_file, remote_file)
end
def delete(remote_file)
begin
@ftp.delete(remote_file)
rescue Net::FTPPermError, Net::FTPReplyError => e
#
end
end
def get_remote_revision
remote_revision = false
begin
@ftp.get('REVISION', Tempfile.new(@host).path) do |line|
remote_revision = line.strip
end
puts "Remote Revision: #{remote_revision}"
rescue Net::FTPPermError => e
raise e unless e.message[0..2] == '550'
remote_revision = false
end
remote_revision
end
def dir_empty?
@ftp.list('*').empty?
end
def mkdir_p(directory)
begin
parent = File.dirname(directory)
files = @ftp.nlst(parent)
if directory == "." || files.any? { |a| a == directory }
return
else
puts "Creating Directory #{directory}"
@ftp.mkdir(directory)
end
rescue Net::FTPPermError
self.mkdir_p(File.dirname(directory))
begin
@ftp.mkdir(directory)
rescue Net::FTPPermError
# pass
end
rescue Net::FTPTempError
begin
puts "Creating Directory #{directory}"
@ftp.mkdir(directory)
rescue Net::FTPPermError => e
self.mkdir_p(File.dirname(directory))
@ftp.mkdir(directory)
end
end
end
end
class FTPS < Service
def initialize(uri, options)
super(uri, options)
@remote_directories = {}
@port = options['port'] || uri.port || 21
end
def connect
puts "Connecting to #{@host} with TLS"
@ftps = Net::FTPTLS.new
if @options['passive']
@ftps.passive = true
end
if @options['debug_mode']
@ftps.debug_mode = true
end
@ftps.binary = true
@ftps.connect(@options['host'], @options['port'])
@ftps.login(@options['user'], @options['password'])
@ftps.chdir(@options['path'].empty? ? @options['chdir'] : '/' + @options['path'])
end
def disconnect
@ftps.quit
end
def put(local_file, remote_file)
dir = File.dirname(remote_file)
unless @remote_directories[dir]
self.mkdir_p(dir)
@remote_directories[dir] = true
end
@ftps.put(local_file, remote_file)
end
def delete(remote_file)
begin
@ftps.delete(remote_file)
rescue Net::FTPPermError, Net::FTPReplyError => e
end
end
def get_remote_revision
remote_revision = false
begin
@ftps.get('REVISION', Tempfile.new(@host).path) do |line|
remote_revision = line.strip
end
puts "Remote Revision: #{remote_revision}"
rescue Net::FTPPermError => e
raise e unless e.message[0..2] == '550'
remote_revision = @options['revision']
end
remote_revision
end
def mkdir_p(directory)
begin
parent = File.dirname(directory)
files = @ftps.nlst(parent)
if directory == "." || files.any? { |a| a == directory }
return
else
puts "Creating Directory #{directory}"
@ftps.mkdir(directory)
end
rescue Net::FTPPermError
self.mkdir_p(File.dirname(directory))
begin
@ftps.mkdir(directory)
rescue Net::FTPPermError
# pass
end
rescue Net::FTPTempError
begin
puts "Creating Directory #{directory}"
@ftps.mkdir(directory)
rescue Net::FTPPermError => e
self.mkdir_p(File.dirname(directory))
@ftps.mkdir(directory)
end
end
end
end
end
module Net
class FTPTLS < FTP
def login(user = "anonymous", passwd = nil, acct = nil)
@ctx = OpenSSL::SSL::SSLContext.new('SSLv3')
@ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
voidcmd("AUTH TLS")
@sock = OpenSSL::SSL::SSLSocket.new(@sock, @ctx)
@sock.connect
super(user, passwd, acct)
voidcmd("PBSZ 0")
# voidcmd("PROT P") # uncomment if you want data encypted too.
end
end
end
services.each do |uri, options|
next if options['skip']
puts uri
uri = URI.parse(uri)
options['revision'] = revision
options['revision_file' ] = revision_file
options['scheme'] = uri.scheme if uri.scheme
options['user'] = uri.user if uri.user
options['password'] = uri.password if uri.password
options['host'] = uri.host if uri.host
options['port'] = uri.port if uri.port
options['path'] = uri.path if uri.path
$ignore_file_list = ($ignore_file_list | options['ignore_file_list']) if options['ignore_file_list']
options.merge!(settings)
pwd = Dir.pwd
local_path = options['local_path']
if local_path
if local_path[0..0] == '/'
raise "Field `local_path` cannot be an absolute path"
end
else
local_path = ''
end
if local_path.length != 0
if local_path[-1..-1] != '/'
local_path += '/'
end
length = local_path.length
else
length = 0
end
options['length'] = length
service = case options['scheme']
when 'sftp' then Git::SFTP.new(uri, options)
when 'ftp' then Git::FTP.new(uri, options)
when 'ftps' then Git::FTPS.new(uri, options)
end
service.automate
unless service.failures.empty?
service_failures[uri] = service.failures
end
end
unless service_failures.empty?
puts "Failures"
puts service_failures.inspect
service_failures.each do |uri, failures|
failures.each do |failure|
puts "#{uri}: #{failure.class} #{failure}"
end
end
end
revision_file.delete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment