Class: Hatchet::App
- Inherits:
-
Object
- Object
- Hatchet::App
- Defined in:
- lib/hatchet/app.rb
Defined Under Namespace
Classes: FailedDeploy, FailedDeployError, FailedReleaseError
Constant Summary collapse
- HATCHET_BUILDPACK_BASE =
-> { ENV.fetch('HATCHET_BUILDPACK_BASE') { warn "ENV HATCHET_BUILDPACK_BASE is not set. It currently defaults to the ruby buildpack. In the future this env var will be required" "https://github.com/heroku/heroku-buildpack-ruby.git" } }
- HATCHET_BUILDPACK_BRANCH =
-> { ENV['HATCHET_BUILDPACK_BRANCH'] || ENV['HEROKU_TEST_RUN_BRANCH'] || Hatchet.git_branch }
- SkipDefaultOption =
Object.new
- DEFAULT_REPO_NAME =
Object.new
- DefaultCommand =
Object.new
Instance Attribute Summary collapse
-
#app_config ⇒ Object
readonly
Returns the value of attribute app_config.
-
#buildpacks ⇒ Object
readonly
Returns the value of attribute buildpacks.
-
#max_retries_count ⇒ Object
readonly
Returns the value of attribute max_retries_count.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#reaper ⇒ Object
readonly
Returns the value of attribute reaper.
-
#repo_name ⇒ Object
readonly
Returns the value of attribute repo_name.
-
#stack ⇒ Object
readonly
Returns the value of attribute stack.
Class Method Summary collapse
-
.config ⇒ Object
config is read only, should be threadsafe.
- .default_buildpack ⇒ Object
Instance Method Summary collapse
- #add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") ⇒ Object
- #allow_failure? ⇒ Boolean
- #annotate_failures ⇒ Object
- #api_key ⇒ Object
- #api_rate_limit ⇒ Object
- #before_deploy(behavior = :default, &block) ⇒ Object
- #commit! ⇒ Object
- #config ⇒ Object
- #couple_pipeline(app_name, pipeline_id) ⇒ Object
- #create_app ⇒ Object
- #create_pipeline ⇒ Object
- #create_source ⇒ Object
-
#debug? ⇒ Boolean
(also: #debugging?)
set debug: true when creating app if you don’t want it to be automatically destroyed, useful for debugging…bad for app limits.
- #delete_pipeline(pipeline_id) ⇒ Object
- #deploy(&block) ⇒ Object
- #deployed? ⇒ Boolean
- #directory ⇒ Object
- #get_config ⇒ Object
- #get_labs ⇒ Object
- #heroku ⇒ Object
- #in_directory ⇒ Object
-
#in_directory_fork(&block) ⇒ Object
A safer alternative to in_directory this method is used to run code that may mutate the current process anything run in this block is executed in a different fork.
-
#initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: , retries: RETRIES, config: {}) ⇒ App
constructor
A new instance of App.
- #lab_is_installed?(lab) ⇒ Boolean
- #not_debugging? ⇒ Boolean (also: #no_debug?)
- #original_source_code_directory ⇒ Object
- #output ⇒ Object
- #pipeline_id ⇒ Object
- #platform_api ⇒ Object
- #push ⇒ Object (also: #push!, #push_with_retry)
- #push_without_retry! ⇒ Object
- #retry_error_message(error, attempt) ⇒ Object
-
#run(cmd_type, command = DefaultCommand, options = {}, &block) ⇒ Object
runs a command on heroku similar to ‘$ heroku run #foo` but programatically and with more control.
- #run_ci(timeout: 900, &block) ⇒ Object
-
#run_multi(command, options = {}, &block) ⇒ Object
Allows multiple commands to be run concurrently in the background.
- #set_config(options = {}) ⇒ Object
- #set_lab(lab) ⇒ Object
- #set_labs! ⇒ Object
-
#setup! ⇒ Object
(also: #setup)
creates a new heroku app via the API.
- #source_get_url ⇒ Object
- #teardown! ⇒ Object
- #update_stack(stack_name) ⇒ Object
Constructor Details
#initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: , retries: RETRIES, config: {}) ⇒ App
Returns a new instance of App.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/hatchet/app.rb', line 51 def initialize(repo_name = DEFAULT_REPO_NAME, stack: ENV["HATCHET_DEFAULT_STACK"], name: default_name, debug: nil, debugging: nil, allow_failure: false, labs: [], buildpack: nil, buildpacks: nil, buildpack_url: nil, before_deploy: nil, run_multi: ENV["HATCHET_RUN_MULTI"], retries: RETRIES, config: {} ) raise "You tried creating a Hatchet::App instance without source code, pass in a path to an app to deploy or the name of an app in your hatchet.json" if repo_name == DEFAULT_REPO_NAME @repo_name = repo_name @directory = self.config.path_for_name(@repo_name) @name = name @stack = stack @debug = debug || debugging @allow_failure = allow_failure @labs = ([] << labs).flatten.compact @buildpacks = buildpack || buildpacks || buildpack_url || self.class.default_buildpack @buildpacks = Array(@buildpacks) @buildpacks.map! {|b| b == :default ? self.class.default_buildpack : b} @run_multi = run_multi @max_retries_count = retries if run_multi && !ENV["HATCHET_EXPENSIVE_MODE"] raise "You're attempting to enable `run_multi: true` mode, but have not enabled `HATCHET_EXPENSIVE_MODE=1` env var to verify you understand the risks" end @run_multi_array = [] @already_in_dir = nil @app_is_setup = nil @before_deploy_array = [] @before_deploy_array << before_deploy if before_deploy @app_config = config @reaper = Reaper.new(api_rate_limit: api_rate_limit) end |
Instance Attribute Details
#app_config ⇒ Object (readonly)
Returns the value of attribute app_config.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def app_config @app_config end |
#buildpacks ⇒ Object (readonly)
Returns the value of attribute buildpacks.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def buildpacks @buildpacks end |
#max_retries_count ⇒ Object (readonly)
Returns the value of attribute max_retries_count.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def max_retries_count @max_retries_count end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def name @name end |
#reaper ⇒ Object (readonly)
Returns the value of attribute reaper.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def reaper @reaper end |
#repo_name ⇒ Object (readonly)
Returns the value of attribute repo_name.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def repo_name @repo_name end |
#stack ⇒ Object (readonly)
Returns the value of attribute stack.
16 17 18 |
# File 'lib/hatchet/app.rb', line 16 def stack @stack end |
Class Method Details
.config ⇒ Object
config is read only, should be threadsafe
125 126 127 |
# File 'lib/hatchet/app.rb', line 125 def self.config @config ||= Config.new end |
.default_buildpack ⇒ Object
116 117 118 |
# File 'lib/hatchet/app.rb', line 116 def self.default_buildpack @default_buildpack ||= [HATCHET_BUILDPACK_BASE.call, HATCHET_BUILDPACK_BRANCH.call].join("#") end |
Instance Method Details
#add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") ⇒ Object
163 164 165 166 167 168 169 170 |
# File 'lib/hatchet/app.rb', line 163 def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGRESQL_[A-Z]+_URL") max_retries_count.times.retry do # heroku.post_addon(name, plan_name) api_rate_limit.call.addon.create(name, plan: plan_name ) _, value = get_config.detect {|k, v| k.match(/#{match_val}/) } set_config('DATABASE_URL' => value) end end |
#allow_failure? ⇒ Boolean
120 121 122 |
# File 'lib/hatchet/app.rb', line 120 def allow_failure? @allow_failure end |
#annotate_failures ⇒ Object
99 100 101 102 103 |
# File 'lib/hatchet/app.rb', line 99 def annotate_failures yield rescue *test_failure_classes => e raise e, "App: #{name} (#{@repo_name})\n#{e.}" end |
#api_key ⇒ Object
449 450 451 |
# File 'lib/hatchet/app.rb', line 449 def api_key @api_key ||= ENV['HEROKU_API_KEY'] ||= `heroku auth:token 2> /dev/null`.chomp end |
#api_rate_limit ⇒ Object
532 533 534 535 |
# File 'lib/hatchet/app.rb', line 532 def api_rate_limit @platform_api ||= PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null)) @api_rate_limit ||= ApiRateLimit.new(@platform_api) end |
#before_deploy(behavior = :default, &block) ⇒ Object
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/hatchet/app.rb', line 327 def before_deploy(behavior = :default, &block) raise "block required" unless block case behavior when :default, :replace if @before_deploy_array.any? && behavior == :default STDERR.puts "Calling App#before_deploy multiple times will overwrite the contents. If you intended this: use `App#before_deploy(:replace)`" STDERR.puts "In the future, calling this method with no arguements will default to `App#before_deploy(:append)` behavior.\n#{caller.join("\n")}" end @before_deploy_array.clear @before_deploy_array << block when :prepend @before_deploy_array = [block] + @before_deploy_array when :append @before_deploy_array << block else raise "Unrecognized behavior: #{behavior.inspect}, valid inputs are :append, :prepend, and :replace" end self end |
#commit! ⇒ Object
350 351 352 |
# File 'lib/hatchet/app.rb', line 350 def commit! local_cmd_exec!('git add .; git commit --allow-empty -m next') end |
#config ⇒ Object
129 130 131 |
# File 'lib/hatchet/app.rb', line 129 def config self.class.config end |
#couple_pipeline(app_name, pipeline_id) ⇒ Object
502 503 504 |
# File 'lib/hatchet/app.rb', line 502 def couple_pipeline(app_name, pipeline_id) api_rate_limit.call.pipeline_coupling.create(app: app_name, pipeline: pipeline_id, stage: "development") end |
#create_app ⇒ Object
280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/hatchet/app.rb', line 280 def create_app 3.times.retry do begin hash = { name: name, stack: stack } hash.delete_if { |k,v| v.nil? } heroku_api_create_app(hash) rescue => e @reaper.cycle(app_exception_message: e.) raise e end end end |
#create_pipeline ⇒ Object
498 499 500 |
# File 'lib/hatchet/app.rb', line 498 def create_pipeline api_rate_limit.call.pipeline.create(name: @name) end |
#create_source ⇒ Object
511 512 513 514 515 516 517 518 |
# File 'lib/hatchet/app.rb', line 511 def create_source @create_source ||= begin result = api_rate_limit.call.source.create @source_get_url = result["source_blob"]["get_url"] @source_put_url = result["source_blob"]["put_url"] @source_put_url end end |
#debug? ⇒ Boolean Also known as: debugging?
set debug: true when creating app if you don’t want it to be automatically destroyed, useful for debugging…bad for app limits. turn on global debug by setting HATCHET_DEBUG=true in the env
266 267 268 |
# File 'lib/hatchet/app.rb', line 266 def debug? @debug || ENV['HATCHET_DEBUG'] || false end |
#delete_pipeline(pipeline_id) ⇒ Object
520 521 522 523 524 525 |
# File 'lib/hatchet/app.rb', line 520 def delete_pipeline(pipeline_id) api_rate_limit.call.pipeline.delete(pipeline_id) rescue Excon::Error::Forbidden warn "Error deleting pipeline id: #{pipeline_id.inspect}, status: 403" # Means the pipeline likely doesn't exist, not sure why though end |
#deploy(&block) ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/hatchet/app.rb', line 410 def deploy(&block) in_directory do annotate_failures do in_dir_setup! self.push_with_retry! block.call(self, api_rate_limit.call, output) if block_given? end end ensure self.teardown! if block_given? end |
#deployed? ⇒ Boolean
276 277 278 |
# File 'lib/hatchet/app.rb', line 276 def deployed? api_rate_limit.call.formation.list(name).detect {|ps| ps["type"] == "web"} end |
#directory ⇒ Object
105 106 107 108 109 |
# File 'lib/hatchet/app.rb', line 105 def directory warn "Calling App#directory returns the original location of the app's source code that should not be modified, if this is really what you want use `original_source_code_directory` instead." warn caller @directory end |
#get_config ⇒ Object
140 141 142 143 |
# File 'lib/hatchet/app.rb', line 140 def get_config # heroku.get_config_vars(name).body api_rate_limit.call.config_var.info_for_app(name) end |
#get_labs ⇒ Object
149 150 151 152 |
# File 'lib/hatchet/app.rb', line 149 def get_labs # heroku.get_features(name).body api_rate_limit.call.app_feature.list(name) end |
#heroku ⇒ Object
453 454 455 |
# File 'lib/hatchet/app.rb', line 453 def heroku raise "Not supported, use `platform_api` instead." end |
#in_directory ⇒ Object
371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/hatchet/app.rb', line 371 def in_directory yield and return if @already_in_dir Dir.mktmpdir do |tmpdir| FileUtils.cp_r("#{original_source_code_directory}/.", "#{tmpdir}/.") Dir.chdir(tmpdir) do @already_in_dir = true yield @already_in_dir = false end end end |
#in_directory_fork(&block) ⇒ Object
A safer alternative to in_directory this method is used to run code that may mutate the current process anything run in this block is executed in a different fork
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/hatchet/app.rb', line 388 def in_directory_fork(&block) Tempfile.create("stdout") do |tmp_file| pid = fork do $stdout.reopen(tmp_file, "a") $stderr.reopen(tmp_file, "a") $stdout.sync = true $stderr.sync = true in_directory do |dir| yield dir end Kernel.exit!(0) # needed for https://github.com/seattlerb/minitest/pull/683 end Process.waitpid(pid) if $?.success? print File.read(tmp_file) else raise File.read(tmp_file) end end end |
#lab_is_installed?(lab) ⇒ Boolean
145 146 147 |
# File 'lib/hatchet/app.rb', line 145 def lab_is_installed?(lab) get_labs.any? {|hash| hash["name"] == lab } end |
#not_debugging? ⇒ Boolean Also known as: no_debug?
271 272 273 |
# File 'lib/hatchet/app.rb', line 271 def not_debugging? !debug? end |
#original_source_code_directory ⇒ Object
111 112 113 |
# File 'lib/hatchet/app.rb', line 111 def original_source_code_directory @directory end |
#output ⇒ Object
445 446 447 |
# File 'lib/hatchet/app.rb', line 445 def output @output end |
#pipeline_id ⇒ Object
494 495 496 |
# File 'lib/hatchet/app.rb', line 494 def pipeline_id @pipeline_id end |
#platform_api ⇒ Object
527 528 529 530 |
# File 'lib/hatchet/app.rb', line 527 def platform_api api_rate_limit return @platform_api end |
#push ⇒ Object Also known as: push!, push_with_retry
422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/hatchet/app.rb', line 422 def push retry_count = allow_failure? ? 1 : max_retries_count retry_count.times.retry do |attempt| begin @output = self.push_without_retry! rescue StandardError => error puts (error, attempt) unless retry_count == 1 raise error end end end |
#push_without_retry! ⇒ Object
354 355 356 |
# File 'lib/hatchet/app.rb', line 354 def push_without_retry! raise NotImplementedError end |
#retry_error_message(error, attempt) ⇒ Object
437 438 439 440 441 442 443 |
# File 'lib/hatchet/app.rb', line 437 def (error, attempt) attempt += 1 return "" if attempt == max_retries_count msg = "\nRetrying failed Attempt ##{attempt}/#{max_retries_count} to push for '#{name}' due to error: \n"<< "#{error.class} #{error.}\n #{error.backtrace.join("\n ")}" return msg end |
#run(cmd_type, command = DefaultCommand, options = {}, &block) ⇒ Object
runs a command on heroku similar to ‘$ heroku run #foo` but programatically and with more control
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/hatchet/app.rb', line 175 def run(cmd_type, command = DefaultCommand, = {}, &block) case command when Hash .merge!(command) command = cmd_type.to_s when nil STDERR.puts "Calling App#run with an explicit nil value in the second argument is deprecated." STDERR.puts "You can pass in a hash directly as the second argument now.\n#{caller.join("\n")}" command = cmd_type.to_s when DefaultCommand command = cmd_type.to_s else command = command.to_s end allow_run_multi! if @run_multi run_obj = Hatchet::HerokuRun.new( command, app: self, retry_on_empty: .fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]), heroku: [:heroku], raw: [:raw] ).call return run_obj.output end |
#run_ci(timeout: 900, &block) ⇒ Object
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/hatchet/app.rb', line 457 def run_ci(timeout: 900, &block) in_directory do max_retries_count.times.retry do result = create_pipeline @pipeline_id = result["id"] end # When the CI run finishes, the associated ephemeral app created for the test run internally gets removed almost immediately # the system then sees a pipeline with no apps, and deletes it, also almost immediately # that would, with bad timing, mean our test run info poll in wait! would 403, and/or the delete_pipeline at the end # that's why we create an app explictly (or maybe it already exists), and then associate it with with the pipeline # the app will be auto cleaned up later in_dir_setup! max_retries_count.times.retry do couple_pipeline(@name, @pipeline_id) end test_run = TestRun.new( token: api_key, buildpacks: @buildpacks, timeout: timeout, app: self, pipeline: @pipeline_id, api_rate_limit: api_rate_limit ) max_retries_count.times.retry do test_run.create_test_run end test_run.wait!(&block) end ensure teardown! if block_given? delete_pipeline(@pipeline_id) if @pipeline_id @pipeline_id = nil end |
#run_multi(command, options = {}, &block) ⇒ Object
Allows multiple commands to be run concurrently in the background.
WARNING! Using the feature requres that the underlying app is not on the “free” Heroku tier. This requires scaling up the dyno which is not free. If an app is scaled up and left in that state it can incur large costs.
Enabling this feature should be done with extreme caution.
Example:
Hatchet::Runner.new("default_ruby", run_multi: true)
app.run_multi("ls") { |out| expect(out).to include("Gemfile") }
app.run_multi("ruby -v") { |out| expect(out).to include("ruby") }
end
This example will run ‘heroku run ls` as well as `ruby -v` at the same time in the background. The return result will be yielded to the block after they finish running.
Order of execution is not guaranteed.
If you need to assert a command was successful, you can yield a second status object like this:
Hatchet::Runner.new("default_ruby", run_multi: true)
app.run_multi("ls") do |out, status|
expect(status.success?).to be_truthy
expect(out).to include("Gemfile")
end
app.run_multi("ruby -v") do |out, status|
expect(status.success?).to be_truthy
expect(out).to include("ruby")
end
end
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/hatchet/app.rb', line 241 def run_multi(command, = {}, &block) raise "Block required" if block.nil? allow_run_multi! run_thread = Thread.new do run_obj = Hatchet::HerokuRun.new( command, app: self, retry_on_empty: .fetch(:retry_on_empty, !ENV["HATCHET_DISABLE_EMPTY_RUN_RETRY"]), heroku: [:heroku], raw: [:raw] ).call yield run_obj.output, run_obj.status end run_thread.abort_on_exception = true @run_multi_array << run_thread true end |
#set_config(options = {}) ⇒ Object
133 134 135 136 137 138 |
# File 'lib/hatchet/app.rb', line 133 def set_config( = {}) .each do |key, value| # heroku.put_config_vars(name, key => value) api_rate_limit.call.config_var.update(name, key => value) end end |
#set_lab(lab) ⇒ Object
158 159 160 161 |
# File 'lib/hatchet/app.rb', line 158 def set_lab(lab) # heroku.post_feature(lab, name) api_rate_limit.call.app_feature.update(name, lab, enabled: true) end |
#set_labs! ⇒ Object
154 155 156 |
# File 'lib/hatchet/app.rb', line 154 def set_labs! @labs.each {|lab| set_lab(lab) } end |
#setup! ⇒ Object Also known as: setup
creates a new heroku app via the API
303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/hatchet/app.rb', line 303 def setup! return self if @app_is_setup puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}" create_app set_labs! buildpack_list = @buildpacks.map { |pack| { buildpack: pack } } api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list) set_config @app_config @app_is_setup = true self end |
#source_get_url ⇒ Object
506 507 508 509 |
# File 'lib/hatchet/app.rb', line 506 def source_get_url create_source @source_get_url end |
#teardown! ⇒ Object
358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/hatchet/app.rb', line 358 def teardown! return false unless @app_is_setup if @run_multi_is_setup @run_multi_array.map(&:join) platform_api.formation.update(name, "web", {"size" => "free"}) end ensure @app_update_info = platform_api.app.update(name, { maintenance: true }) if @app_is_setup @reaper.cycle if @app_is_setup end |
#update_stack(stack_name) ⇒ Object
297 298 299 300 |
# File 'lib/hatchet/app.rb', line 297 def update_stack(stack_name) @stack = stack_name api_rate_limit.call.app.update(name, build_stack: @stack) end |