egads 0.0.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZjFlZmRlNWRmNTUwYmM5MjAxM2Y1NjMzNTFmOGIyMTkxYWM1Njc3YQ==
5
- data.tar.gz: !binary |-
6
- M2U5MjdmMzc0YWFhMGIzZjM4Y2ViNWIwZTdhYjE3ZjMxZDAyMjBmMw==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- M2VhOWNlMmEwMjI0MTIzNjRkODFjMWYzNTU0YzY3OWIxYWZhNGEzMmI5MTgw
10
- MmU3NjdhMTk1Y2FjMDdmODExODMyNjkwOWFjNjVmZjE2YjU3ZWQxZDdjZTA2
11
- MjRmN2I3NDJkZGFmZmZiOTI4YzU1MDMzMjg3ODQ0Y2M0Nzg4ZDg=
12
- data.tar.gz: !binary |-
13
- NWM4MWQ5ZjQ2YmVkMzg2OTJhYjBhNDZmMTdkZDgxZTkwZjI5NTYwNWU5ZTcw
14
- MGM3OGY0ZTFjMzA3MjYzMDQ5OTE1N2U1MzNhMWNiODNiMjJjNjQxZjViMDdi
15
- MGI0OWI5ZTlhNWE5YTNiODY3M2NmYTI5OWIzM2I0MjhjZmU0M2Y=
2
+ SHA1:
3
+ metadata.gz: 42ec1036274bdc5a2f82a3e9b7cc104a13b026ed
4
+ data.tar.gz: 805811a6fcba6b004c181c7c0f0afbd281633904
5
+ SHA512:
6
+ metadata.gz: f6c59fa7fae40a6c6ef32422367b5f3deacddafdaace0320f23fad1b6495e5c5f13bb76ee0224b845b1cb574b4e90a600594eb30db0a015f9e5ecf090e662203
7
+ data.tar.gz: f5bdec599424d240ecae7aefce605614a7e1dde7fc0b4de35d76a1c3ba8e254d4d63bde4b41e24a2ab2474a1baf4049f477f63268b7dd716f9b3b5672713e1f7
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  coverage
6
+ Gemfile.lock
6
7
  InstalledFiles
7
8
  lib/bundler/man
8
9
  pkg
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
4
  gem 'debugger', require: nil
5
+ # Use my fork until https://github.com/guard/guard-minitest/pull/65 is released
6
+ gem 'guard-minitest', '>= 1.0.0.rc.2'
7
+ gem 'terminal-notifier-guard'
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'minitest' do
5
+ # with Minitest::Unit
6
+ #watch(%r|^test/(.*)\/?test_(.*)\.rb|)
7
+ #watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
8
+ #watch(%r|^test/test_helper\.rb|) { "test" }
9
+
10
+ # with Minitest::Spec
11
+ watch(%r|^spec/(.*)_spec\.rb|)
12
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
13
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
14
+
15
+ # Rails 3.2
16
+ # watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/controllers/#{m[1]}_test.rb" }
17
+ # watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
18
+ # watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
19
+
20
+ # Rails
21
+ # watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/functional/#{m[1]}_test.rb" }
22
+ # watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
23
+ # watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
24
+ end
data/bin/egads CHANGED
@@ -2,4 +2,4 @@
2
2
  # TODO: remove this
3
3
  $: << './lib'
4
4
  require 'egads'
5
- Egads::CLI.start
5
+ Egads::Command.start
data/egads.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_dependency "thor"
22
22
  s.add_development_dependency "rake"
23
23
  s.add_development_dependency "minitest"
24
+ #s.add_development_dependency "simple_mock" # Via http://tatey.com/2012/02/07/mocking-with-minitest-mock-and-simple-delegator/
24
25
 
25
26
  s.description = %s{
26
27
  A collection of scripts for making a deployable tarball of a git commit,
@@ -1,3 +1,4 @@
1
+ # AWS S3 config for generating signed URL for tarball
1
2
  s3:
2
3
  bucket: my-bucket
3
4
  access_key: mykey
@@ -10,9 +11,11 @@ release_to: /var/apps/my_project/current
10
11
 
11
12
  restart_command: /etc/init.d/rails_services restart
12
13
 
14
+ # Options to pass to bundler
15
+ bundler:
16
+ options: --deployment --quiet --without development:test --path /var/apps/my_project/shared/bundle
17
+
13
18
  # environment variables to set before executing commands
14
19
  env:
15
20
  RAILS_ENV: production
16
21
  SHARED_PATH: /var/apps/my_project/shared
17
- BUNDLE_PATH: /var/apps/my_project/shared/bundle
18
- BUNDLE_WITHOUT: development:test
data/lib/egads.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  require 'yaml'
2
2
  require 'fog'
3
3
  require 'thor'
4
+ require 'benchmark'
5
+
6
+ module Egads; end
7
+
4
8
  require 'egads/config'
5
9
  require 'egads/s3_tarball'
6
- require 'egads/cli'
7
- require 'egads/ext/thor_actions'
10
+ require 'egads/group'
11
+ require 'egads/command'
@@ -0,0 +1,18 @@
1
+ module Egads
2
+ class Command < Thor
3
+ require 'egads/command/build'
4
+ require 'egads/command/upload'
5
+ require 'egads/command/extract'
6
+ require 'egads/command/stage'
7
+ require 'egads/command/release'
8
+ require 'egads/command/trim'
9
+
10
+ register(Build, 'build', 'build [REV]', '[local] Compiles a deployable tarball of the current commit and uploads it to S3')
11
+ register(Upload, 'upload', 'upload SHA', '[local, plumbing] Uploads a tarball for SHA to S3')
12
+ register(Extract, 'extract', 'extract SHA', '[remote, plumbing] Downloads tarball for SHA from S3 and extracts it to the filesystem')
13
+ register(Stage, 'stage', 'stage SHA', '[remote, plumbing] Downloads tarball for SHA from S3 and extracts it to the filesystem')
14
+ register(Release, 'release', 'release SHA', '[remote, plumbing] Downloads tarball for SHA from S3 and extracts it to the filesystem')
15
+ register(Trim, 'trime', 'trim [N]', "[remote, plumbing] Deletes old releases, keeping the N most recent (by mtime)")
16
+
17
+ end
18
+ end
@@ -0,0 +1,114 @@
1
+ module Egads
2
+ class Build < Group
3
+ include Thor::Actions
4
+
5
+ desc "[local] Compiles a deployable tarball of the current commit and uploads it to S3"
6
+ class_option :force, type: :boolean, aliases: '-f', default: false, banner: "Build and overwrite existing tarball on S3"
7
+ class_option :wait, type: :boolean, aliases: '-w', default: false, banner: "Wait for the build to exist. Poll S3 every 2 seconds."
8
+ class_option 'no-upload', type: :boolean, default: false, banner: "Don't upload the tarball to S3"
9
+ argument :rev, type: :string, default: 'HEAD', desc: 'git revision to build'
10
+
11
+ def check_build
12
+ say_status :rev, "#{rev} parsed to #{sha}"
13
+
14
+ wait_for_build if options[:wait]
15
+ unless should_build?
16
+ say_status :done, "Tarball for #{sha} already exists. Pass --force to rebuild."
17
+ exit 0
18
+ end
19
+
20
+ say_status :rev, "#{rev} parsed to #{sha}"
21
+ exit 1 unless can_build?
22
+ end
23
+
24
+ def make_git_archive
25
+ say_status :build, "Making tarball for #{sha}", :yellow
26
+ FileUtils.mkdir_p(File.dirname(tarball.local_tar_path))
27
+ run_with_code "git archive #{sha} --format=tar > #{tarball.local_tar_path}"
28
+ end
29
+
30
+ def append_revision_file
31
+ File.open('REVISION', 'w') {|f| f << sha + "\n" }
32
+ run_with_code "tar -uf #{tarball.local_tar_path} REVISION"
33
+ end
34
+
35
+ def run_after_build_hooks
36
+ run_hooks_for(:build,:after)
37
+ end
38
+
39
+ def append_extra_paths
40
+ extra_paths = Config.build_extra_paths
41
+ if extra_paths.any?
42
+ run_with_code "tar -uf #{tarball.local_tar_path} #{extra_paths * " "}"
43
+ end
44
+ end
45
+
46
+ def gzip_archive
47
+ run_with_code "gzip -9f #{tarball.local_tar_path}"
48
+ end
49
+
50
+ def upload
51
+ invoke(Egads::Upload, [sha]) unless options['no-upload']
52
+ end
53
+
54
+ module BuildHelpers
55
+ def sha
56
+ @sha ||= run_with_code("git rev-parse --verify #{rev}").strip
57
+ end
58
+
59
+ def short_sha
60
+ sha[0,7]
61
+ end
62
+
63
+ def tarball
64
+ @tarball ||= S3Tarball.new(sha)
65
+ end
66
+
67
+ def should_build?
68
+ options[:force] || !tarball.exists?
69
+ end
70
+
71
+ def can_build?
72
+ sha_is_checked_out? && working_directory_is_clean?
73
+ end
74
+
75
+ def sha_is_checked_out?
76
+ head = run_with_code("git rev-parse --verify HEAD", capture: true).strip
77
+ short_head = head[0,7]
78
+ head == sha or error [
79
+ "Cannot build #{short_sha} because #{short_head} is checked out.",
80
+ "Run `git checkout #{short_sha}` and try again"
81
+ ]
82
+ end
83
+
84
+ def working_directory_is_clean?
85
+ run("git status -s", capture: true).empty? or
86
+ error [
87
+ "Cannot build #{short_sha} because the working directory is not clean.",
88
+ "Stash your changes with `git add . && git stash` and try again."
89
+ ]
90
+ end
91
+
92
+ def error(message)
93
+ lines = Array(message)
94
+ say_status :error, lines.shift, :red
95
+ lines.each {|line| say_status '', line }
96
+
97
+ false
98
+ end
99
+
100
+ def wait_for_build
101
+ say_status :wait, "Waiting for tarball to exist...", :yellow
102
+ loop do
103
+ start = Time.now
104
+ break if tarball.exists?
105
+ printf '.'
106
+ sleep [2 - (Time.now - start), 0].max
107
+ end
108
+ printf "\n"
109
+ end
110
+
111
+ end
112
+ include BuildHelpers
113
+ end
114
+ end
@@ -0,0 +1,68 @@
1
+ module Egads
2
+ class Extract < Group
3
+ include Thor::Actions
4
+
5
+ desc "[remote, plumbing] Downloads tarball for SHA from S3 and extracts it to the filesystem"
6
+ class_option :force, type: :boolean, default: false, banner: "Overwrite existing files"
7
+ argument :sha, type: :string, required: true, desc: 'git SHA to download and extract'
8
+
9
+ def setup_environment
10
+ RemoteConfig.setup_environment
11
+ end
12
+
13
+ def download
14
+ if should_download?
15
+ say_status :download, "Downloading tarball for #{sha}", :yellow
16
+ FileUtils.mkdir_p(release_dir)
17
+ duration = Benchmark.realtime do
18
+ File.open(path, 'w') {|f| f << tarball.contents }
19
+ end
20
+ size = File.size(path)
21
+ say_status :done, "Downloaded in %.1f seconds (%.1f KB/s)" % [duration, (size.to_f / 2**10) / duration]
22
+ else
23
+ say_status :done, "Tarball already downloaded. Use --force to overwrite"
24
+ end
25
+ end
26
+
27
+ def extract
28
+ # Check revision file to see if tarball is already extracted
29
+ if should_extract?
30
+ # Silence stderr warnings "Ignoring unknown extended header keyword"
31
+ # due to BSD/GNU tar.
32
+ inside(release_dir) { run_with_code "tar -zxf #{path} 2>/dev/null" }
33
+ else
34
+ say_status :done, "Tarball already extracted. Use --force to overwrite"
35
+ end
36
+ end
37
+
38
+ def mark_as_extracted
39
+ FileUtils.touch(extract_flag_path)
40
+ end
41
+
42
+ protected
43
+ def release_dir
44
+ RemoteConfig.release_dir(sha)
45
+ end
46
+
47
+ def path
48
+ File.join(release_dir, "#{sha}.tar.gz")
49
+ end
50
+
51
+ def tarball
52
+ @tarball ||= S3Tarball.new(sha, true)
53
+ end
54
+
55
+ def should_download?
56
+ options[:force] || File.zero?(path) || !File.exists?(path)
57
+ end
58
+
59
+ def extract_flag_path
60
+ File.join(release_dir, '.egads-extract-success')
61
+ end
62
+
63
+ def should_extract?
64
+ options[:force] || !File.exists?(extract_flag_path)
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ module Egads
2
+ class Release < Group
3
+ include Thor::Actions
4
+
5
+ desc "[remote] Symlinks SHA to current and restarts services. If needed, stages SHA"
6
+ class_option :force, type: :boolean, default: false, banner: "Overwrite existing release"
7
+ argument :sha, type: :string, required: true, desc: 'git SHA to stage'
8
+ def setup_environment
9
+ RemoteConfig.setup_environment
10
+ end
11
+
12
+ def stage
13
+ invoke(Egads::Stage, [sha], options)
14
+ end
15
+
16
+ def run_before_release_hooks
17
+ return unless should_release?
18
+ inside(dir) { run_hooks_for(:release, :before) }
19
+ end
20
+
21
+ def symlink_release
22
+ return unless should_release?
23
+ symlink_directory(dir, release_to)
24
+ end
25
+
26
+ def restart
27
+ return unless should_release?
28
+
29
+ inside release_to do
30
+ # Restart services
31
+ run_with_code(RemoteConfig.restart_command)
32
+ end
33
+ end
34
+
35
+ def run_after_release_hooks
36
+ inside release_to do
37
+ run_hooks_for(:release, :after)
38
+ end
39
+ end
40
+
41
+ def trim
42
+ FileUtils.touch(dir) # Ensure this release isn't trimmed
43
+ invoke(:trim, [4], {})
44
+ end
45
+
46
+ protected
47
+ def dir
48
+ RemoteConfig.release_dir(sha)
49
+ end
50
+
51
+ def release_to
52
+ RemoteConfig.release_to
53
+ end
54
+
55
+ def current_symlink_destination
56
+ File.readlink(RemoteConfig.release_to) rescue nil
57
+ end
58
+
59
+ def should_release?
60
+ @should_release = options[:force] || dir != current_symlink_destination unless defined?(@should_release)
61
+ @should_release
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,78 @@
1
+ module Egads
2
+ class Stage < Group
3
+ include Thor::Actions
4
+
5
+
6
+ desc "[remote] Readies SHA for release. If needed, generates URL for SHA and extracts"
7
+ class_option :force, type: :boolean, default: false, banner: "Overwrite existing files"
8
+ argument :sha, type: :string, required: true, desc: 'git SHA to stage'
9
+
10
+ def setup_environment
11
+ RemoteConfig.setup_environment
12
+ end
13
+
14
+ def extract
15
+ invoke(Egads::Extract, [sha], options)
16
+ end
17
+
18
+ def run_before_hooks
19
+ return unless should_stage?
20
+ inside(dir){ run_hooks_for(:stage, :before) }
21
+ end
22
+
23
+ def bundle
24
+ return unless should_stage?
25
+
26
+ inside(dir) do
27
+ run_with_code("bundle install #{RemoteConfig.bundler_options}") if File.readable?("Gemfile")
28
+ end
29
+ end
30
+
31
+ def symlink_system_paths
32
+ return unless should_stage? && shared_path
33
+ symlink_directory File.join(shared_path, 'system'), File.join(dir, 'public', 'system')
34
+ symlink_directory File.join(shared_path, 'log'), File.join(dir, 'log')
35
+ end
36
+
37
+ def symlink_config_files
38
+ return unless should_stage? && shared_path
39
+
40
+ shared_config = File.join(shared_path, 'config')
41
+ if File.directory?(shared_config)
42
+ Dir.glob("#{shared_config}/*").each do |source|
43
+ basename = File.basename(source)
44
+ destination = File.join(dir, 'config', basename)
45
+ symlink(source, destination)
46
+ end
47
+ end
48
+ end
49
+
50
+ def run_after_stage_hooks
51
+ return unless should_stage?
52
+ inside(dir) { run_hooks_for(:stage, :after) }
53
+ end
54
+
55
+ def mark_as_staged
56
+ FileUtils.touch(stage_flag_path)
57
+ end
58
+
59
+ protected
60
+ def dir
61
+ RemoteConfig.release_dir(sha)
62
+ end
63
+
64
+ def stage_flag_path
65
+ File.join(dir, '.egads-stage-success')
66
+ end
67
+
68
+ def should_stage?
69
+ options[:force] || !File.exists?(stage_flag_path)
70
+ end
71
+
72
+ def shared_path
73
+ ENV['SHARED_PATH']
74
+ end
75
+ end
76
+ end
77
+
78
+
@@ -0,0 +1,18 @@
1
+ module Egads
2
+ class Trim < Group
3
+ include Thor::Actions
4
+
5
+
6
+ desc "[remote, plumbing] Deletes old releases, keeping the N most recent (by mtime)"
7
+ def trim(n=4)
8
+ inside RemoteConfig.extract_to do
9
+ dirs = Dir.glob('*').sort_by{|path| File.mtime(path) }.reverse[n..-1].to_a
10
+ dirs.each do |dir|
11
+ say_status :trim, "Deleting #{dir}"
12
+ FileUtils.rm_rf(dir)
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module Egads
2
+ class Upload < Group
3
+ include Thor::Actions
4
+
5
+ desc "[local, plumbing] Uploads a tarball for SHA to S3"
6
+ argument :sha, type: :string, required: true, desc: 'git SHA to upload'
7
+
8
+ attr_reader :sha
9
+ def upload
10
+ @sha = sha
11
+ size = File.size(path)
12
+
13
+ say_status :upload, "Uploading tarball (%.1f MB)" % (size.to_f / 2**20), :yellow
14
+ duration = Benchmark.realtime do
15
+ tarball.upload(path)
16
+ end
17
+ say_status :done, "Uploaded in %.1f seconds (%.1f KB/s)" % [duration, (size.to_f / 2**10) / duration]
18
+
19
+ File.delete(path)
20
+ end
21
+
22
+ private
23
+ def tarball
24
+ @tarball ||= S3Tarball.new(sha)
25
+ end
26
+
27
+ def path
28
+ tarball.local_gzipped_path
29
+ end
30
+
31
+ end
32
+ end
data/lib/egads/config.rb CHANGED
@@ -56,10 +56,6 @@ module Egads
56
56
  path
57
57
  end
58
58
 
59
- def self.release_dir(sha)
60
- File.join(config['extract_to'], sha)
61
- end
62
-
63
59
  def self.release_to
64
60
  config['release_to']
65
61
  end
@@ -68,6 +64,10 @@ module Egads
68
64
  config['extract_to']
69
65
  end
70
66
 
67
+ def self.release_dir(sha)
68
+ File.join(config['extract_to'], sha)
69
+ end
70
+
71
71
  # Set environment variables from the config
72
72
  def self.setup_environment
73
73
  config['env'].each{|k,v| ENV[k] = v.to_s } if config['env']
@@ -76,5 +76,9 @@ module Egads
76
76
  def self.restart_command
77
77
  config['restart_command']
78
78
  end
79
+
80
+ def self.bundler_options
81
+ config['bundler']['options'] if config['bundler']
82
+ end
79
83
  end
80
84
  end
@@ -0,0 +1,45 @@
1
+ module Egads
2
+ class CommandError < Thor::Error; end
3
+
4
+ class Group < Thor::Group
5
+
6
+ protected
7
+ def run_with_code(command, config={})
8
+ result = nil
9
+ duration = Benchmark.realtime do
10
+ result = run(command, config.merge(capture: true))
11
+ end
12
+ say_status :done, "Finished in %.1f seconds" % duration
13
+
14
+ if $? != 0
15
+ raise CommandError.new("`#{command}` failed with exit status #{$?.exitstatus.inspect}")
16
+ end
17
+ result
18
+ end
19
+
20
+ # Run command hooks from config file
21
+ # E.g. run_hooks_for(:build, :after)
22
+ def run_hooks_for(cmd, hook)
23
+ say_status :hooks, "Running #{cmd} #{hook} hooks"
24
+ Config.hooks_for(cmd, hook).each do |command|
25
+ run_with_code command
26
+ end
27
+ end
28
+
29
+ # Symlinks a directory
30
+ # NB that `ln -f` doesn't work with directories.
31
+ # This is not atomic.
32
+ def symlink_directory(src, dest)
33
+ raise ArgumentError.new("#{src} is not a directory") unless File.directory?(src)
34
+ say_status :symlink, "from #{src} to #{dest}"
35
+ FileUtils.rm_rf(dest)
36
+ FileUtils.ln_s(src, dest)
37
+ end
38
+
39
+ def symlink(src, dest)
40
+ raise ArgumentError.new("#{src} is not a file") unless File.file?(src)
41
+ say_status :symlink, "from #{src} to #{dest}"
42
+ FileUtils.ln_sf(src, dest)
43
+ end
44
+ end
45
+ end
data/lib/egads/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Egads
2
- VERSION = '0.0.8'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,29 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Build" do
4
+ setup_configs!
5
+ subject { Egads::Build }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(check_build make_git_archive append_revision_file run_after_build_hooks append_extra_paths gzip_archive upload)
9
+ end
10
+
11
+ it 'takes one argument' do
12
+ subject.arguments.size.must_equal 1
13
+ end
14
+
15
+ it 'has a rev argument' do
16
+ rev = subject.arguments.detect{|arg| arg.name == 'rev'}
17
+ rev.default.must_equal 'HEAD'
18
+ rev.required.must_equal false
19
+ end
20
+
21
+ end
22
+
23
+ describe "Egags::Build instance" do
24
+ subject { Egads::Build.new }
25
+
26
+ it "has rev HEAD" do
27
+ subject.rev.must_equal 'HEAD'
28
+ end
29
+ end
@@ -8,16 +8,61 @@ describe Egads::Config do
8
8
  end
9
9
 
10
10
  describe "with an config file" do
11
- before { ENV['EGADS_CONFIG'] = "example/egads.yml" }
12
- after { ENV.delete('EGAGS_CONFIG') }
11
+ setup_configs!
12
+
13
+ let(:yml) { YAML.load_file("example/egads.yml") }
14
+
15
+ describe '#config' do
16
+ it 'is a hash' do
17
+ subject.config.must_equal yml
18
+ end
19
+ end
13
20
 
14
21
  it "has an S3 bucket" do
15
- subject.s3_bucket.key.must_equal 'my-bucket'
22
+ subject.s3_bucket.key.must_equal yml['s3']['bucket']
16
23
  end
17
24
 
18
25
  it "has an S3 prefix" do
19
- subject.s3_prefix.must_equal 'my_project'
26
+ subject.s3_prefix.must_equal yml['s3']['prefix']
20
27
  end
21
28
  end
22
29
 
23
30
  end
31
+
32
+ describe Egads::RemoteConfig do
33
+ subject { Egads::RemoteConfig }
34
+
35
+ it "raises ArgumentError for missing config" do
36
+ -> { subject.config_path }.must_raise(ArgumentError)
37
+ end
38
+
39
+ describe "with an config file" do
40
+ setup_configs!
41
+ let(:yml) { YAML.load_file("example/egads_remote.yml") }
42
+
43
+ describe '#config' do
44
+ it('is a hash') { subject.config.must_equal yml }
45
+ end
46
+
47
+ it('#release_to') { subject.release_to.must_equal yml['release_to'] }
48
+ it('#extract_to') { subject.extract_to.must_equal yml['extract_to'] }
49
+ it('#release_dir') { subject.release_dir('abc').must_equal File.join(yml['extract_to'], 'abc') }
50
+ it('#restart_command') { subject.restart_command.must_equal yml['restart_command'] }
51
+ it('#bundler_options') { subject.bundler_options.must_equal yml['bundler']['options'] }
52
+
53
+ describe '#setup_environment' do
54
+ before { subject.setup_environment }
55
+ after do
56
+ # Delete ENV from yaml data
57
+ yml['env'].keys.each{|key| ENV.delete(key) }
58
+ end
59
+
60
+ it 'should set ENV values' do
61
+ yml['env'].each do |key, value|
62
+ ENV[key].must_equal value
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Extract" do
4
+ setup_configs!
5
+ subject { Egads::Extract }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(setup_environment download extract mark_as_extracted)
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Release" do
4
+ setup_configs!
5
+ subject { Egads::Release }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(setup_environment stage run_before_release_hooks symlink_release restart run_after_release_hooks trim)
9
+ end
10
+ end
@@ -1,6 +1,7 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  describe Egads::S3Tarball do
4
+ setup_configs!
4
5
 
5
6
  before { ENV['EGADS_CONFIG'] = "example/egads.yml" }
6
7
  after { ENV.delete('EGAGS_CONFIG') }
@@ -18,11 +19,10 @@ describe Egads::S3Tarball do
18
19
 
19
20
  describe 'when uploaded' do
20
21
  before do
21
- Egads::Config.s3_bucket.save # Ensure bucket exists
22
22
  subject.upload(ENV['EGADS_CONFIG'])
23
23
  end
24
24
 
25
- it('should exist') { subject.exists?.wont_be_nil }
25
+ it('should exist') { (!! subject.exists?).must_equal true }
26
26
  end
27
27
 
28
28
 
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Stage" do
4
+ setup_configs!
5
+ subject { Egads::Stage }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(setup_environment extract run_before_hooks bundle symlink_system_paths symlink_config_files run_after_stage_hooks mark_as_staged)
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Trim" do
4
+ setup_configs!
5
+ subject { Egads::Trim }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(trim)
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe "Egads::Upload" do
4
+ setup_configs!
5
+ subject { Egads::Upload }
6
+
7
+ it 'should run the correct tasks' do
8
+ subject.commands.keys.must_equal %w(upload)
9
+ end
10
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,11 +7,29 @@ require "egads"
7
7
 
8
8
  Fog.mock!
9
9
 
10
+ SHA = 'deadbeef' * 5 # Test git sha
11
+
10
12
  begin
11
13
  require 'debugger'
12
14
  rescue LoadError
13
15
  puts "Skipping debugger"
14
16
  end
15
17
 
18
+ # Extensions
16
19
  class Minitest::Spec
20
+
21
+ def self.setup_configs!
22
+ before do
23
+ ENV['EGADS_CONFIG'] = "example/egads.yml"
24
+ ENV['EGADS_REMOTE_CONFIG'] = "example/egads_remote.yml"
25
+ Egads::Config.s3_bucket.save # Ensure bucket exists
26
+
27
+ end
28
+
29
+ after do
30
+ ENV.delete('EGADS_CONFIG')
31
+ ENV.delete('EGADS_REMOTE_CONFIG')
32
+ end
33
+
34
+ end
17
35
  end
metadata CHANGED
@@ -1,73 +1,75 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: egads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Suggs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-18 00:00:00.000000000 Z
11
+ date: 2013-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fog
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - '>='
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- description: ! "\n A collection of scripts for making a deployable tarball of a
70
- git commit,\n uploading it to Amazon S3, and downloading it to your servers."
69
+ description: |2-
70
+
71
+ A collection of scripts for making a deployable tarball of a git commit,
72
+ uploading it to Amazon S3, and downloading it to your servers.
71
73
  email:
72
74
  - aaron@ktheory.com
73
75
  executables:
@@ -78,7 +80,7 @@ extra_rdoc_files:
78
80
  files:
79
81
  - .gitignore
80
82
  - Gemfile
81
- - Gemfile.lock
83
+ - Guardfile
82
84
  - README.md
83
85
  - Rakefile
84
86
  - bin/egads
@@ -87,13 +89,25 @@ files:
87
89
  - example/egads_remote.yml
88
90
  - lib/egads.rb
89
91
  - lib/egads/capistrano.rb
90
- - lib/egads/cli.rb
92
+ - lib/egads/command.rb
93
+ - lib/egads/command/build.rb
94
+ - lib/egads/command/extract.rb
95
+ - lib/egads/command/release.rb
96
+ - lib/egads/command/stage.rb
97
+ - lib/egads/command/trim.rb
98
+ - lib/egads/command/upload.rb
91
99
  - lib/egads/config.rb
92
- - lib/egads/ext/thor_actions.rb
100
+ - lib/egads/group.rb
93
101
  - lib/egads/s3_tarball.rb
94
102
  - lib/egads/version.rb
103
+ - spec/egads_build_spec.rb
95
104
  - spec/egads_config_spec.rb
105
+ - spec/egads_extract_spec.rb
106
+ - spec/egads_release_spec.rb
96
107
  - spec/egads_s3_tarball_spec.rb
108
+ - spec/egads_stage_spec.rb
109
+ - spec/egads_trim_spec.rb
110
+ - spec/egads_upload_spec.rb
97
111
  - spec/spec_helper.rb
98
112
  homepage: https://github.com/kickstarter/egads
99
113
  licenses: []
@@ -105,22 +119,27 @@ require_paths:
105
119
  - lib
106
120
  required_ruby_version: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - ! '>='
122
+ - - '>='
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
125
  required_rubygems_version: !ruby/object:Gem::Requirement
112
126
  requirements:
113
- - - ! '>='
127
+ - - '>='
114
128
  - !ruby/object:Gem::Version
115
129
  version: '0'
116
130
  requirements: []
117
131
  rubyforge_project:
118
- rubygems_version: 2.0.3
132
+ rubygems_version: 2.0.2
119
133
  signing_key:
120
134
  specification_version: 4
121
135
  summary: Extensible Git Archive Deploy Strategy
122
136
  test_files:
137
+ - spec/egads_build_spec.rb
123
138
  - spec/egads_config_spec.rb
139
+ - spec/egads_extract_spec.rb
140
+ - spec/egads_release_spec.rb
124
141
  - spec/egads_s3_tarball_spec.rb
142
+ - spec/egads_stage_spec.rb
143
+ - spec/egads_trim_spec.rb
144
+ - spec/egads_upload_spec.rb
125
145
  - spec/spec_helper.rb
126
- has_rdoc:
data/Gemfile.lock DELETED
@@ -1,49 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- egads (0.0.1)
5
- fog
6
- thor
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- builder (3.2.2)
12
- columnize (0.3.6)
13
- debugger (1.5.0)
14
- columnize (>= 0.3.1)
15
- debugger-linecache (~> 1.2.0)
16
- debugger-ruby_core_source (~> 1.2.0)
17
- debugger-linecache (1.2.0)
18
- debugger-ruby_core_source (1.2.0)
19
- excon (0.22.1)
20
- fog (1.11.1)
21
- builder
22
- excon (~> 0.20)
23
- formatador (~> 0.2.0)
24
- json (~> 1.7)
25
- mime-types
26
- net-scp (~> 1.1)
27
- net-ssh (>= 2.1.3)
28
- nokogiri (~> 1.5.0)
29
- ruby-hmac
30
- formatador (0.2.4)
31
- json (1.8.0)
32
- mime-types (1.23)
33
- minitest (5.0.3)
34
- net-scp (1.1.1)
35
- net-ssh (>= 2.6.5)
36
- net-ssh (2.6.7)
37
- nokogiri (1.5.9)
38
- rake (10.0.4)
39
- ruby-hmac (0.4.0)
40
- thor (0.18.1)
41
-
42
- PLATFORMS
43
- ruby
44
-
45
- DEPENDENCIES
46
- debugger
47
- egads!
48
- minitest
49
- rake
data/lib/egads/cli.rb DELETED
@@ -1,221 +0,0 @@
1
- module Egads
2
- class CLI < Thor
3
- include Thor::Actions
4
- ##
5
- # Local commands
6
-
7
- desc "build", "[local] Compiles a deployable tarball of the current commit and uploads it to S3"
8
- method_option :force, type: :boolean, aliases: '-f', default: false, banner: "Build and overwrite existing tarball on S3"
9
- method_option 'no-upload', type: :boolean, default: false, banner: "Don't upload the tarball to S3"
10
- def build(rev='HEAD')
11
- sha = run_or_die("git rev-parse --verify #{rev}", capture: true).strip
12
- tarball = S3Tarball.new(sha)
13
- if !options[:force] && tarball.exists?
14
- say "Tarball for #{sha} already exists. Pass --force to rebuild."
15
- return
16
- end
17
-
18
- say "Building tarball for #{sha}..."
19
- # Check if we're on sha, if not, ask to check it out
20
- head = run_or_die("git rev-parse --verify HEAD", capture: true).strip
21
- unless head == sha
22
- say "** Error **"
23
- say "Trying to build #{sha[0,7]}, but #{head[0,7]} is checked out."
24
- say "Run `git checkout #{head[0,7]}` and try again."
25
- exit 1
26
- end
27
-
28
- # Ensure clean working directory
29
- unless run("git status -s", capture: true).empty?
30
- say "** Error **"
31
- say "Working directory is not clean."
32
- say "Stash your changes with `git add . && git stash` and try again."
33
- exit 1
34
- end
35
-
36
- # Make git archive
37
- FileUtils.mkdir_p(File.dirname(tarball.local_tar_path))
38
- run_or_die "git archive #{sha} --format=tar > #{tarball.local_tar_path}"
39
-
40
- # Write REVISION and add to tarball
41
- File.open('REVISION', 'w') {|f| f << sha + "\n" }
42
- run_or_die "tar -uf #{tarball.local_tar_path} REVISION"
43
-
44
- run_hooks_for(:build, :after)
45
-
46
- extra_paths = Config.build_extra_paths
47
- if extra_paths.any?
48
- run_or_die "tar -uf #{tarball.local_tar_path} #{extra_paths * " "}"
49
- end
50
-
51
- run_or_die "gzip -9f #{tarball.local_tar_path}"
52
-
53
- invoke(:upload, [sha], force: options[:force]) unless options['no-upload']
54
- end
55
-
56
- method_option :force, type: :boolean, aliases: '-f', default: false, banner: "Overwrite existing tarball on S3"
57
- desc "upload SHA", "[local, plumbing] Uploads a tarball for SHA to S3"
58
- def upload(sha)
59
- tarball = S3Tarball.new(sha)
60
- if !options[:force] && tarball.exists?
61
- say "Tarball for #{sha} already exists. Pass --force to upload again."
62
- return
63
- end
64
-
65
- path = tarball.local_gzipped_path
66
- size = File.size(path)
67
-
68
- say "Uploading tarball (%.1f MB)" % (size.to_f / 2**20)
69
- duration = Benchmark.realtime do
70
- tarball.upload(path)
71
- end
72
- say "Uploaded in %.1f seconds (%.1f KB/s)" % [duration, (size.to_f / 2**10) / duration]
73
-
74
- File.delete(path)
75
- end
76
-
77
- ##
78
- # Remote commands
79
-
80
- desc "extract SHA", "[remote, plumbing] Downloads tarball for SHA from S3 and extracts it to the filesystem"
81
- method_option :force, type: :boolean, default: false, banner: "Overwrite existing files"
82
- def extract(sha)
83
- RemoteConfig.setup_environment
84
- dir = RemoteConfig.release_dir(sha)
85
- path = File.join(dir, "#{sha}.tar.gz")
86
- tarball = S3Tarball.new(sha, true)
87
-
88
- inside dir do
89
- if options[:force] || File.zero?(path) || !File.exists?(path)
90
- say "Downloading tarball"
91
- duration = Benchmark.realtime do
92
- File.open(path, 'w') {|f| f << tarball.contents }
93
- end
94
- size = File.size(path)
95
- say "Downloaded in %.1f seconds (%.1f KB/s)" % [duration, (size.to_f / 2**10) / duration]
96
- else
97
- say "Tarball already downloaded. Use --force to overwrite"
98
- end
99
-
100
- # Check revision file to see if tarball is already extracted
101
- extract_flag_path = File.join(dir, '.egads-extract-success')
102
- if options[:force] || !File.exists?(extract_flag_path)
103
- say "Extracting tarball"
104
- run_or_die "tar -zxf #{path}"
105
- else
106
- say "Tarball already extracted. Use --force to overwrite"
107
- end
108
- FileUtils.touch(extract_flag_path)
109
- end
110
- end
111
-
112
- desc "stage SHA", "[remote] Readies SHA for release. If needed, generates URL for SHA and extracts"
113
- method_option :force, type: :boolean, default: false, banner: "Overwrite existing files"
114
- def stage(sha)
115
- RemoteConfig.setup_environment
116
- invoke(:extract, [sha], options)
117
- dir = RemoteConfig.release_dir(sha)
118
- stage_flag_path = File.join(dir, '.egads-stage-success')
119
- if options[:force] || !File.exists?(stage_flag_path)
120
- inside dir do
121
- run_hooks_for(:stage, :before)
122
-
123
- # Bundler
124
- if File.readable?("Gemfile")
125
- bundler_args = %w(--deployment --quiet)
126
- # Hack to force bundle options overridden by --deployment
127
- bundler_args << "--without #{ENV['BUNDLE_WITHOUT']}" if ENV['BUNDLE_WITHOUT']
128
- bundler_args << "--path #{ENV['BUNDLE_PATH']}" if ENV['BUNDLE_PATH']
129
-
130
- run_or_die("bundle install #{bundler_args * ' '}")
131
- end
132
-
133
- if shared_path = ENV['SHARED_PATH']
134
- symlink_directory File.join(shared_path, 'system'), File.join(dir, 'public', 'system')
135
- symlink_directory File.join(shared_path, 'log'), File.join(dir, 'log')
136
-
137
- # Symlink config files
138
- shared_config = File.join(shared_path, 'config')
139
- if File.directory?(shared_config)
140
- Dir.glob("#{shared_config}/*").each do |source|
141
- basename = File.basename(source)
142
- destination = File.join(dir, 'config', basename)
143
- symlink(source, destination)
144
- end
145
- end
146
- end
147
-
148
- run_hooks_for(:stage, :after)
149
- end
150
- else
151
- say "Already staged. Use --force to overwrite"
152
- end
153
- FileUtils.touch(stage_flag_path)
154
- end
155
-
156
- desc "release SHA", "[remote] Symlinks SHA to current and restarts services. If needed, stages SHA"
157
- method_option :force, type: :boolean, default: false, banner: "Overwrite existing files while staging"
158
- def release(sha)
159
- RemoteConfig.setup_environment
160
- invoke(:stage, [sha], options)
161
- dir = RemoteConfig.release_dir(sha)
162
- inside dir do
163
- run_hooks_for(:release, :before)
164
- end
165
-
166
- # destination of the current symlink
167
- current_release = File.readlink(RemoteConfig.release_to) rescue nil
168
- unless dir == current_release
169
- # Symlink this release to the release_to
170
- symlink_directory(dir, RemoteConfig.release_to) unless dir == current_release
171
- end
172
-
173
- inside RemoteConfig.release_to do
174
- # Restart services
175
- run_or_die(RemoteConfig.restart_command)
176
- run_hooks_for(:release, :after)
177
- end
178
-
179
- FileUtils.touch(dir) # Ensure this release isn't trimmed
180
- invoke(:trim, [4])
181
- end
182
-
183
- desc "trim N", "[remote, plumbing] Deletes old releases, keeping the N most recent (by mtime)"
184
- method_option :force, type: :boolean, default: false, banner: "No op, compatible with release"
185
- def trim(n=4)
186
- dirs = Dir.glob('*').sort_by{|path| File.mtime(path) }.reverse[n..-1].to_a
187
- dirs.each do |dir|
188
- say_status :trim, "Deleting #{dir}"
189
- FileUtils.rm_rf(dir)
190
- end
191
- end
192
-
193
- private
194
- # Run command hooks from config file
195
- # E.g. run_hooks_for(:build, :after)
196
- def run_hooks_for(cmd, hook)
197
- say_status :hooks, "Running #{cmd} #{hook} hooks"
198
- Config.hooks_for(cmd, hook).each do |command|
199
- say "Running `#{command}`"
200
- run_or_die command
201
- end
202
- end
203
-
204
- # Symlinks a directory
205
- # NB that `ln -f` doesn't work with directories.
206
- # This is not atomic.
207
- def symlink_directory(src, dest)
208
- raise ArgumentError.new("#{src} is not a directory") unless File.directory?(src)
209
- say_status :symlink, "from #{src} to #{dest}"
210
- FileUtils.rm_rf(dest)
211
- FileUtils.ln_s(src, dest)
212
- end
213
-
214
- def symlink(src, dest)
215
- raise ArgumentError.new("#{src} is not a file") unless File.file?(src)
216
- say_status :symlink, "from #{src} to #{dest}"
217
- FileUtils.ln_sf(src, dest)
218
- end
219
-
220
- end
221
- end
@@ -1,25 +0,0 @@
1
- require 'thor'
2
- require 'benchmark'
3
- class Thor
4
- class CommandFailedError < Error; end
5
-
6
- module Actions
7
- # runs command, raises CommandFailedError unless exit status is 0.
8
- # Also logs duration
9
- def run_or_die(command, config={})
10
- result = nil
11
- duration = Benchmark.realtime do
12
- result = run(command, config)
13
- end
14
- if behavior == :invoke && $?.exitstatus != 0
15
- message = "#{command} failed with %s" % ($?.exitstatus ? "exit status #{$?.exitstatus}" : "no exit status (likely force killed)")
16
- raise Thor::CommandFailedError.new(message)
17
- end
18
-
19
- say_status :done, "in %.1f seconds" % duration, config.fetch(:verbose, true)
20
-
21
- result
22
- end
23
-
24
- end
25
- end