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: "", 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: "", 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: "", 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
124 125 126 |
# File 'lib/hatchet/app.rb', line 124 def self.config @config ||= Config.new end |
.default_buildpack ⇒ Object
115 116 117 |
# File 'lib/hatchet/app.rb', line 115 def self.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
162 163 164 165 166 167 168 169 |
# File 'lib/hatchet/app.rb', line 162 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
119 120 121 |
# File 'lib/hatchet/app.rb', line 119 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
448 449 450 |
# File 'lib/hatchet/app.rb', line 448 def api_key @api_key ||= ENV['HEROKU_API_KEY'] ||= `heroku auth:token`.chomp end |
#api_rate_limit ⇒ Object
531 532 533 534 |
# File 'lib/hatchet/app.rb', line 531 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
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/hatchet/app.rb', line 326 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
349 350 351 |
# File 'lib/hatchet/app.rb', line 349 def commit! local_cmd_exec!('git add .; git commit --allow-empty -m next') end |
#config ⇒ Object
128 129 130 |
# File 'lib/hatchet/app.rb', line 128 def config self.class.config end |
#couple_pipeline(app_name, pipeline_id) ⇒ Object
501 502 503 |
# File 'lib/hatchet/app.rb', line 501 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
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/hatchet/app.rb', line 279 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
497 498 499 |
# File 'lib/hatchet/app.rb', line 497 def create_pipeline api_rate_limit.call.pipeline.create(name: @name) end |
#create_source ⇒ Object
510 511 512 513 514 515 516 517 |
# File 'lib/hatchet/app.rb', line 510 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
265 266 267 |
# File 'lib/hatchet/app.rb', line 265 def debug? @debug || ENV['HATCHET_DEBUG'] || false end |
#delete_pipeline(pipeline_id) ⇒ Object
519 520 521 522 523 524 |
# File 'lib/hatchet/app.rb', line 519 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
409 410 411 412 413 414 415 416 417 418 419 |
# File 'lib/hatchet/app.rb', line 409 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
275 276 277 |
# File 'lib/hatchet/app.rb', line 275 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
139 140 141 142 |
# File 'lib/hatchet/app.rb', line 139 def get_config # heroku.get_config_vars(name).body api_rate_limit.call.config_var.info_for_app(name) end |
#get_labs ⇒ Object
148 149 150 151 |
# File 'lib/hatchet/app.rb', line 148 def get_labs # heroku.get_features(name).body api_rate_limit.call.app_feature.list(name) end |
#heroku ⇒ Object
452 453 454 |
# File 'lib/hatchet/app.rb', line 452 def heroku raise "Not supported, use `platform_api` instead." end |
#in_directory ⇒ Object
370 371 372 373 374 375 376 377 378 379 380 381 |
# File 'lib/hatchet/app.rb', line 370 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
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/hatchet/app.rb', line 387 def in_directory_fork(&block) Tempfile.create("stdout") do |tmp_file| pid = fork do $stdout.reopen(tmp_file, "w") $stderr.reopen(tmp_file, "w") $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? puts File.read(tmp_file) else raise File.read(tmp_file) end end end |
#lab_is_installed?(lab) ⇒ Boolean
144 145 146 |
# File 'lib/hatchet/app.rb', line 144 def lab_is_installed?(lab) get_labs.any? {|hash| hash["name"] == lab } end |
#not_debugging? ⇒ Boolean Also known as: no_debug?
270 271 272 |
# File 'lib/hatchet/app.rb', line 270 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
444 445 446 |
# File 'lib/hatchet/app.rb', line 444 def output @output end |
#pipeline_id ⇒ Object
493 494 495 |
# File 'lib/hatchet/app.rb', line 493 def pipeline_id @pipeline_id end |
#platform_api ⇒ Object
526 527 528 529 |
# File 'lib/hatchet/app.rb', line 526 def platform_api api_rate_limit return @platform_api end |
#push ⇒ Object Also known as: push!, push_with_retry
421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/hatchet/app.rb', line 421 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
353 354 355 |
# File 'lib/hatchet/app.rb', line 353 def push_without_retry! raise NotImplementedError end |
#retry_error_message(error, attempt) ⇒ Object
436 437 438 439 440 441 442 |
# File 'lib/hatchet/app.rb', line 436 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
174 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 |
# File 'lib/hatchet/app.rb', line 174 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
456 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 |
# File 'lib/hatchet/app.rb', line 456 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
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/hatchet/app.rb', line 240 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
132 133 134 135 136 137 |
# File 'lib/hatchet/app.rb', line 132 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
157 158 159 160 |
# File 'lib/hatchet/app.rb', line 157 def set_lab(lab) # heroku.post_feature(lab, name) api_rate_limit.call.app_feature.update(name, lab, enabled: true) end |
#set_labs! ⇒ Object
153 154 155 |
# File 'lib/hatchet/app.rb', line 153 def set_labs! @labs.each {|lab| set_lab(lab) } end |
#setup! ⇒ Object Also known as: setup
creates a new heroku app via the API
302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/hatchet/app.rb', line 302 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
505 506 507 508 |
# File 'lib/hatchet/app.rb', line 505 def source_get_url create_source @source_get_url end |
#teardown! ⇒ Object
357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/hatchet/app.rb', line 357 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
296 297 298 299 |
# File 'lib/hatchet/app.rb', line 296 def update_stack(stack_name) @stack = stack_name api_rate_limit.call.app.update(name, build_stack: @stack) end |