#!/usr/bin/env ruby # A sample pre-deploy hook # # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. # # Fails unless the combined status is "success" # # These environment variables are available: # KAMAL_RECORDED_AT # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS # KAMAL_COMMAND # KAMAL_SUBCOMMAND # KAMAL_ROLE (if set) # KAMAL_DESTINATION (if set) # Only check the build status for production deployments if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" exit 0 end require "bundler/inline" # true = install gems so this is fast on repeat invocations gemfile(true, quiet: true) do source "https://rubygems.org" gem "octokit" gem "faraday-retry" end MAX_ATTEMPTS = 72 ATTEMPTS_GAP = 10 def exit_with_error(message) $stderr.puts message exit 1 end class GithubStatusChecks attr_reader :remote_url, :git_sha, :github_client, :combined_status def initialize @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") @git_sha = `git rev-parse HEAD`.strip @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) refresh! end def refresh! @combined_status = github_client.combined_status(remote_url, git_sha) end def state combined_status[:state] end def first_status_url first_status = combined_status[:statuses].find { |status| status[:state] == state } first_status && first_status[:target_url] end def complete_count combined_status[:statuses].count { |status| status[:state] != "pending"} end def total_count combined_status[:statuses].count end def current_status if total_count > 0 "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." else "Build not started..." end end end $stdout.sync = true puts "Checking build status..." attempts = 0 checks = GithubStatusChecks.new begin loop do case checks.state when "success" puts "Checks passed, see #{checks.first_status_url}" exit 0 when "failure" exit_with_error "Checks failed, see #{checks.first_status_url}" when "pending" attempts += 1 end exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS puts checks.current_status sleep(ATTEMPTS_GAP) checks.refresh! end rescue Octokit::NotFound exit_with_error "Build status could not be found" end