egads 0.0.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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