takeoff 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +268 -0
  6. data/Rakefile +1 -0
  7. data/bin/takeoff +6 -0
  8. data/example.Launchfile +86 -0
  9. data/lib/takeoff/cli.rb +46 -0
  10. data/lib/takeoff/configuration.rb +58 -0
  11. data/lib/takeoff/ext/middleware_builder.rb +54 -0
  12. data/lib/takeoff/helpers.rb +44 -0
  13. data/lib/takeoff/plan/base.rb +62 -0
  14. data/lib/takeoff/plan/default.rb +36 -0
  15. data/lib/takeoff/plan/heroku.rb +41 -0
  16. data/lib/takeoff/stage/base.rb +17 -0
  17. data/lib/takeoff/stage/checkout_development_branch.rb +24 -0
  18. data/lib/takeoff/stage/heroku/disable_preboot.rb +37 -0
  19. data/lib/takeoff/stage/heroku/enable_maintenance_mode.rb +27 -0
  20. data/lib/takeoff/stage/heroku/migrate_database.rb +52 -0
  21. data/lib/takeoff/stage/heroku/precompile_and_sync_assets.rb +45 -0
  22. data/lib/takeoff/stage/heroku/push_to_server.rb +16 -0
  23. data/lib/takeoff/stage/heroku/remember_commits.rb +19 -0
  24. data/lib/takeoff/stage/heroku/scale_down_workers.rb +33 -0
  25. data/lib/takeoff/stage/heroku/verify_server_not_already_up_to_date.rb +17 -0
  26. data/lib/takeoff/stage/heroku/verify_staging_up_to_date.rb +28 -0
  27. data/lib/takeoff/stage/log.rb +28 -0
  28. data/lib/takeoff/stage/look_out_for_danger.rb +15 -0
  29. data/lib/takeoff/stage/point_checkpoint_to_development.rb +22 -0
  30. data/lib/takeoff/stage/push_to_github.rb +14 -0
  31. data/lib/takeoff/stage/stash_changes.rb +31 -0
  32. data/lib/takeoff/stage/verify_circle_ci_status.rb +46 -0
  33. data/lib/takeoff/stage/verify_github_up_to_date.rb +18 -0
  34. data/lib/takeoff/version.rb +3 -0
  35. data/lib/takeoff.rb +44 -0
  36. data/takeoff.gemspec +23 -0
  37. metadata +152 -0
@@ -0,0 +1,36 @@
1
+ require "takeoff/plan/base"
2
+
3
+ require "takeoff/stage/log"
4
+ require "takeoff/stage/look_out_for_danger"
5
+ require "takeoff/stage/stash_changes"
6
+ require "takeoff/stage/checkout_development_branch"
7
+ require "takeoff/stage/verify_github_up_to_date"
8
+ require "takeoff/stage/point_checkpoint_to_development"
9
+ require "takeoff/stage/push_to_github"
10
+
11
+ module Takeoff
12
+ module Plan
13
+ class Default < Base
14
+ env.merge!(
15
+ environment: "production",
16
+ github_remote: "github",
17
+ development_branch: "develop",
18
+ checkpoint_branch: "master"
19
+ )
20
+
21
+ stages do
22
+ use Stage::Log
23
+ use Stage::LookOutForDanger
24
+
25
+ use Stage::StashChanges
26
+ use Stage::CheckoutDevelopmentBranch
27
+
28
+ use Stage::VerifyGithubUpToDate
29
+
30
+ use Stage::PointCheckpointToDevelopment
31
+
32
+ use Stage::PushToGithub
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ require "takeoff/plan/default"
2
+
3
+ require "takeoff/stage/heroku/remember_commits"
4
+ require "takeoff/stage/heroku/verify_server_not_already_up_to_date"
5
+ require "takeoff/stage/heroku/disable_preboot"
6
+ require "takeoff/stage/heroku/scale_down_workers"
7
+ require "takeoff/stage/heroku/enable_maintenance_mode"
8
+ require "takeoff/stage/heroku/push_to_server"
9
+ require "takeoff/stage/heroku/migrate_database"
10
+
11
+ module Takeoff
12
+ module Plan
13
+ class Heroku < Default
14
+ env.merge!(
15
+ server_remote: "heroku"
16
+ )
17
+
18
+ stages do
19
+ insert_after Stage::Log,
20
+ Stage::Heroku::RememberCommits
21
+
22
+ insert_after Stage::VerifyGithubUpToDate,
23
+ Stage::Heroku::VerifyServerNotAlreadyUpToDate
24
+
25
+ insert_after Stage::PointCheckpointToDevelopment, [
26
+ Stage::Heroku::DisablePreboot,
27
+ Stage::Heroku::ScaleDownWorkers,
28
+ Stage::Heroku::EnableMaintenanceMode,
29
+
30
+ Stage::Heroku::PushToServer,
31
+
32
+ Stage::Heroku::MigrateDatabase
33
+ ]
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Takeoff.configure do
40
+ plan :heroku, Takeoff::Plan::Heroku
41
+ end
@@ -0,0 +1,17 @@
1
+ require "takeoff/helpers"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class Base
6
+ include Helpers
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class CheckoutDevelopmentBranch < Base
6
+ def call(env)
7
+ previous_branch = `git rev-parse --abbrev-ref HEAD`.strip
8
+ previous_branch = `git rev-parse --verify HEAD`.strip if previous_branch == "HEAD"
9
+
10
+ return @app.call(env) if previous_branch == env[:development_branch]
11
+
12
+ log "Checking out development branch"
13
+ execute "git checkout #{env[:development_branch]}"
14
+
15
+ begin
16
+ @app.call(env)
17
+ ensure
18
+ log "Checking out original branch '#{previous_branch}'"
19
+ execute "git checkout #{previous_branch}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class DisablePreboot < Base
7
+ def run?(env)
8
+ features = `heroku features --remote #{env[:server_remote]}`
9
+ preboot_line = features.split("\n").find { |feature| feature =~ /\A\[[+ ]\] preboot/ }
10
+ preboot_enabled = preboot_line.start_with?("[+]")
11
+
12
+ preboot_enabled
13
+ end
14
+
15
+ def call(env)
16
+ unless env[:dangerous]
17
+ log "Skipping disabling preboot because nothing dangerous is going on"
18
+
19
+ return @app.call(env)
20
+ end
21
+
22
+ return @app.call(env) unless run?(env)
23
+
24
+ log "Disabling preboot"
25
+ execute "heroku features:disable preboot --remote #{env[:server_remote]}"
26
+
27
+ begin
28
+ @app.call(env)
29
+ ensure
30
+ log "Enabling preboot"
31
+ execute "heroku features:enable preboot --remote #{env[:server_remote]}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class EnableMaintenanceMode < Base
7
+ def call(env)
8
+ unless env[:dangerous]
9
+ log "Skipping maintenance mode because nothing dangerous is going on"
10
+
11
+ return @app.call(env)
12
+ end
13
+
14
+ log "Enabling maintenance mode"
15
+ execute "heroku maintenance:on --remote #{env[:server_remote]}"
16
+
17
+ begin
18
+ @app.call(env)
19
+ ensure
20
+ log "Disabling maintenance mode"
21
+ execute "heroku maintenance:off --remote #{env[:server_remote]}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class MigrateDatabase < Base
7
+ def self.dangerous?(env)
8
+ new(nil).dangerous?(env)
9
+ end
10
+
11
+ def dangerous?(env)
12
+ return @dangerous if defined?(@dangerous)
13
+
14
+ return @dangerous = false unless run?(env)
15
+
16
+ diff = diff(env[:deployed_commit], env[:new_commit], ["db/migrate"])
17
+
18
+ unsafe_active_record_terms = /change_column|change_table|drop_table|remove_column|remove_index|rename_column|execute/
19
+ unsafe_mongoid_terms = /renameCollection|\.drop|$rename|$set|$unset|indexes\.create|indexes\.drop/
20
+ unsafe_terms = Regexp.union(unsafe_active_record_terms, unsafe_mongoid_terms)
21
+
22
+ @dangerous = diff.split("\n").any? do |line|
23
+ line =~ unsafe_terms && line !~ /#\s*safe/i
24
+ end
25
+ end
26
+
27
+ def run?(env)
28
+ files_have_changed?(env[:deployed_commit], env[:new_commit], ["db/migrate"])
29
+ end
30
+
31
+ def call(env)
32
+ unless run?(env)
33
+ log "Skipping database migrations"
34
+
35
+ return @app.call(env)
36
+ end
37
+
38
+ log "Running database migrations"
39
+ execute "heroku run rake db:migrate --remote #{env[:server_remote]}"
40
+
41
+ # If ActiveRecord needs to rebuild its column name cache, restart the app.
42
+ if dangerous?(env)
43
+ log "Restarting application"
44
+ execute "heroku restart --remote #{env[:server_remote]}"
45
+ end
46
+
47
+ @app.call(env)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class PrecompileAndSyncAssets < Base
7
+ def run?(env)
8
+ files = %w(app/assets lib/assets vendor/asset Gemfile.lock config/initializers/assets.rb)
9
+ files_have_changed?(env[:deployed_commit], env[:new_commit], files)
10
+ end
11
+
12
+ def call(env)
13
+ unless run?(env)
14
+ log "Skipping precompilation of assets"
15
+
16
+ return @app.call(env)
17
+ end
18
+
19
+ begin
20
+ log "Precompiling assets"
21
+ execute "RAILS_ENV=#{env[:environment]} bundle exec rake assets:precompile"
22
+
23
+ log "Syncing assets"
24
+ execute "RAILS_ENV=#{env[:environment]} bundle exec rake assets:sync"
25
+
26
+ if file_has_changed_locally?("public/assets/manifest-#{env[:environment]}.json")
27
+ log "Committing updated asset manifests"
28
+ execute "git add public/assets/manifest-#{env[:environment]}.json"
29
+ execute "git commit -m 'Update asset manifest for #{env[:environment]}.' -m '[ci skip]'"
30
+
31
+ log "Pushing development branch to GitHub"
32
+ execute "git push github #{env[:development_branch]}:#{env[:development_branch]} --force"
33
+ end
34
+ ensure
35
+ log "Deleting precompiled assets"
36
+ execute "git ls-files -o --exclude-standard public/assets | xargs rm"
37
+ # We can't use `rm -r ./public/assets` because we still need the manifest files.
38
+ end
39
+
40
+ @app.call(env)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class PushToServer < Base
7
+ def call(env)
8
+ log "Pushing to server"
9
+ execute "git push #{env[:server_remote]} #{env[:checkpoint_branch]}:master --force"
10
+
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class RememberCommits < Base
7
+ def call(env)
8
+ log "Fetching from server"
9
+ execute "git fetch #{env[:server_remote]}"
10
+
11
+ env[:deployed_commit] = latest_commit("#{env[:server_remote]}/master")
12
+ env[:new_commit] = latest_commit(env[:development_branch])
13
+
14
+ @app.call(env)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class ScaleDownWorkers < Base
7
+ def call(env)
8
+ unless env[:dangerous]
9
+ log "Skipping scaling down workers because nothing dangerous is going on"
10
+
11
+ return @app.call(env)
12
+ end
13
+
14
+ number_of_workers = execute(
15
+ "heroku ps --remote #{env[:server_remote]} | grep '^worker.' | wc -l | tr -d ' '"
16
+ ).to_i
17
+
18
+ return @app.call(env) if number_of_workers == 0
19
+
20
+ log "Scaling down workers"
21
+ execute "heroku scale worker=0 --remote #{env[:server_remote]}"
22
+
23
+ begin
24
+ @app.call(env)
25
+ ensure
26
+ log "Scaling up workers"
27
+ execute "heroku scale worker=#{number_of_workers || 0} --remote #{env[:server_remote]}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class VerifyServerNotAlreadyUpToDate < Base
7
+ def call(env)
8
+ if env[:deployed_commit] == env[:new_commit]
9
+ raise "The server is already up to date."
10
+ end
11
+
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ module Heroku
6
+ class VerifyStagingUpToDate < Base
7
+ def call(env)
8
+ unless Takeoff.plan?("staging")
9
+ log "WARNING: Skipping verification that the staging server is up to date because a launch plan for the staging environment has not been set up"
10
+
11
+ return @app.call(env)
12
+ end
13
+
14
+ staging_takeoff = Takeoff[:staging]
15
+
16
+ log "Fetching from staging server"
17
+ execute "git fetch #{staging_takeoff.env[:server_remote]}"
18
+
19
+ unless branches_up_to_date?("#{staging_takeoff.env[:server_remote]}/master", env[:development_branch])
20
+ raise "The staging server is not up to date with branch '#{env[:development_branch]}'. Deploy to staging first."
21
+ end
22
+
23
+ @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class Log < Base
6
+ def call(env)
7
+ log "Ready for takeoff! Deploying to #{env[:environment]} server..."
8
+
9
+ start_time = Time.now
10
+
11
+ begin
12
+ @app.call(env)
13
+ rescue
14
+ end_time = Time.now
15
+
16
+ log "Deploying failed in #{end_time - start_time} seconds. Takeoff unsuccessful."
17
+ puts
18
+
19
+ raise
20
+ else
21
+ end_time = Time.now
22
+
23
+ log "Deploying finished in #{end_time - start_time} seconds. Takeoff successful."
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class LookOutForDanger < Base
6
+ def call(env)
7
+ env[:stages] = env[:plan].stages.middlewares
8
+ env[:dangerous_stages] = env[:stages].select { |stage| stage.respond_to?(:dangerous?) && stage.dangerous?(env) }
9
+ env[:dangerous] = env[:dangerous_stages].count > 0
10
+
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class PointCheckpointToDevelopment < Base
6
+ def call(env)
7
+ return @app.call(env) if env[:development_branch] == env[:checkpoint_branch]
8
+
9
+ log "Pointing checkpoint branch to development branch"
10
+ execute "git checkout #{env[:checkpoint_branch]}"
11
+
12
+ begin
13
+ execute "git reset --hard #{env[:development_branch]}"
14
+ ensure
15
+ execute "git checkout #{env[:development_branch]}"
16
+ end
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class PushToGithub < Base
6
+ def call(env)
7
+ log "Pushing checkpoint branch to GitHub"
8
+ execute "git push github #{env[:checkpoint_branch]}:#{env[:checkpoint_branch]} --force"
9
+
10
+ @app.call(env)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class StashChanges < Base
6
+ def call(env)
7
+ status = `git status --untracked-files --short`
8
+ return @app.call(env) if status.blank?
9
+
10
+ stash_name = "Takeoff Auto-Stash: #{Time.now}"
11
+
12
+ log "Stashing uncommitted changes"
13
+ execute "git stash save -u #{Shellwords.escape(stash_name)}"
14
+
15
+ begin
16
+ @app.call(env)
17
+ ensure
18
+ log "Applying previously stashed uncommitted changes"
19
+
20
+ stashes = `git stash list`
21
+ matched_stash = stashes.split("\n").find { |stash| stash.include?(stash_name) }
22
+ label = matched_stash.match(/^([^:]+)/)
23
+
24
+ execute "git clean -fd"
25
+ execute "git stash apply #{label}"
26
+ execute "git stash drop #{label}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ require "uri"
2
+ require "net/http"
3
+ require "json"
4
+
5
+ require "takeoff/stage/base"
6
+
7
+ module Takeoff
8
+ module Stage
9
+ class VerifyCircleCiStatus < Base
10
+ def call(env)
11
+ unless env[:github_repo] && ENV["GITHUB_OAUTH_TOKEN"]
12
+ log "WARNING: Skipping verification of Circle CI status because GitHub repo or OAuth token isn't set."
13
+
14
+ return @app.call(env)
15
+ end
16
+
17
+ raise "A GitHub OAuth token is required to check the Circle CI status." unless ENV["GITHUB_OAUTH_TOKEN"]
18
+
19
+ sha = latest_commit(env[:development_branch])
20
+
21
+ uri = URI.parse("https://api.github.com/repos/#{env[:github_repo]}/statuses/#{sha}")
22
+
23
+ connection = Net::HTTP.new(uri.host, uri.port)
24
+ connection.use_ssl = true
25
+
26
+ request = Net::HTTP::Get.new(uri.request_uri)
27
+ request["Authorization"] = "token #{ENV["GITHUB_OAUTH_TOKEN"]}"
28
+ response = connection.request(request)
29
+
30
+ statuses = JSON.parse(response.body)
31
+
32
+ if statuses.find { |s| s["state"] == "success" }
33
+ # Success
34
+ elsif status = statuses.find { |s| %w(failure error).include?(s["state"]) }
35
+ raise "The Circle CI tests for branch '#{env[:development_branch]}' (commit #{sha}) failed. Fix them and try again. See #{status["target_url"]}"
36
+ elsif status = statuses.find { |s| s["state"] == "pending"}
37
+ raise "The Circle CI tests for branch '#{env[:development_branch]}' (commit #{sha}) are still running. Wait for them to finish successfully. See #{status["target_url"]}"
38
+ else
39
+ raise "The Circle CI tests for branch '#{env[:development_branch]}' (commit #{sha}) have not run yet. Wait for them to start and finish successfully."
40
+ end
41
+
42
+ @app.call(env)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ require "takeoff/stage/base"
2
+
3
+ module Takeoff
4
+ module Stage
5
+ class VerifyGithubUpToDate < Base
6
+ def call(env)
7
+ log "Fetching from GitHub"
8
+ execute "git fetch #{env[:github_remote]}"
9
+
10
+ unless branches_up_to_date?("#{env[:github_remote]}/#{env[:development_branch]}", env[:development_branch])
11
+ raise "GitHub is not up to date on branch '#{env[:development_branch]}'. Pull and push to synchronize your changes first."
12
+ end
13
+
14
+ @app.call(env)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Takeoff
2
+ VERSION = "0.0.1"
3
+ end
data/lib/takeoff.rb ADDED
@@ -0,0 +1,44 @@
1
+ require "active_support/core_ext"
2
+
3
+ require "takeoff/version"
4
+ require "takeoff/configuration"
5
+
6
+ require "takeoff/plan/base"
7
+ require "takeoff/plan/default"
8
+
9
+ module Takeoff
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure(source = nil, source_location = nil, &block)
16
+ if source
17
+ if source_location
18
+ configuration.instance_eval(source, source_location)
19
+ else
20
+ configuration.instance_eval(source)
21
+ end
22
+ end
23
+
24
+ if block
25
+ if block.arity == 1
26
+ block.call(configuration)
27
+ else
28
+ configuration.instance_eval(&block)
29
+ end
30
+ end
31
+ end
32
+
33
+ delegate :plans, :[], :plan?, :default_plan, to: :configuration
34
+
35
+ def logger
36
+ @logger ||= Logger.new
37
+ end
38
+ end
39
+
40
+ configure do
41
+ plan :base, Plan::Base
42
+ plan :default, Plan::Default
43
+ end
44
+ end
data/takeoff.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $:.unshift File.expand_path("../lib", __FILE__)
2
+ require "takeoff/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "takeoff"
6
+ spec.version = Takeoff::VERSION
7
+ spec.author = "Douwe Maan"
8
+ spec.email = "douwe@selenight.nl"
9
+ spec.summary = "Sit back, relax and let Takeoff deploy your app."
10
+ spec.description = "Takeoff is a command line tool that helps you deploy your web applications. Configure it once, and from then on Takeoff will take care of responsibly deploying your app, giving you time to get more coffee."
11
+ spec.homepage = "https://github.com/DouweM/takeoff"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = ["takeoff"]
16
+ spec.require_paths = ["lib"]
17
+
18
+ spec.add_dependency "activesupport"
19
+ spec.add_dependency "middleware"
20
+ spec.add_dependency "thor"
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end