View raw

1 #!/usr/bin/env ruby 2 3 # A sample pre-deploy hook 4 # 5 # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 # 7 # Fails unless the combined status is "success" 8 # 9 # These environment variables are available: 10 # KAMAL_RECORDED_AT 11 # KAMAL_PERFORMER 12 # KAMAL_VERSION 13 # KAMAL_HOSTS 14 # KAMAL_COMMAND 15 # KAMAL_SUBCOMMAND 16 # KAMAL_ROLE (if set) 17 # KAMAL_DESTINATION (if set) 18 19 # Only check the build status for production deployments 20 if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 exit 0 22 end 23 24 require "bundler/inline" 25 26 # true = install gems so this is fast on repeat invocations 27 gemfile(true, quiet: true) do 28 source "https://rubygems.org" 29 30 gem "octokit" 31 gem "faraday-retry" 32 end 33 34 MAX_ATTEMPTS = 72 35 ATTEMPTS_GAP = 10 36 37 def exit_with_error(message) 38 $stderr.puts message 39 exit 1 40 end 41 42 class GithubStatusChecks 43 attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 45 def initialize 46 @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") 47 @git_sha = `git rev-parse HEAD`.strip 48 @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 refresh! 50 end 51 52 def refresh! 53 @combined_status = github_client.combined_status(remote_url, git_sha) 54 end 55 56 def state 57 combined_status[:state] 58 end 59 60 def first_status_url 61 first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 first_status && first_status[:target_url] 63 end 64 65 def complete_count 66 combined_status[:statuses].count { |status| status[:state] != "pending"} 67 end 68 69 def total_count 70 combined_status[:statuses].count 71 end 72 73 def current_status 74 if total_count > 0 75 "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 else 77 "Build not started..." 78 end 79 end 80 end 81 82 83 $stdout.sync = true 84 85 puts "Checking build status..." 86 attempts = 0 87 checks = GithubStatusChecks.new 88 89 begin 90 loop do 91 case checks.state 92 when "success" 93 puts "Checks passed, see #{checks.first_status_url}" 94 exit 0 95 when "failure" 96 exit_with_error "Checks failed, see #{checks.first_status_url}" 97 when "pending" 98 attempts += 1 99 end 100 101 exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 102 103 puts checks.current_status 104 sleep(ATTEMPTS_GAP) 105 checks.refresh! 106 end 107 rescue Octokit::NotFound 108 exit_with_error "Build status could not be found" 109 end 110