Skip to content

Instantly share code, notes, and snippets.

@Taytay
Created February 8, 2017 15:14
Show Gist options
  • Save Taytay/8a66bf77aeb9eec82a31364381b8a6a0 to your computer and use it in GitHub Desktop.
Save Taytay/8a66bf77aeb9eec82a31364381b8a6a0 to your computer and use it in GitHub Desktop.
A more robust version of `heroku run --exit-code`
#!/usr/bin/env expect
# This script requires `expect` to be installed on your system.
# This script runs a remote command on Heroku in detached mode,
# and exits with the same exit code that the remote command does.
# Note that this is superior to `heroku run --exit-code`, since that has a bug that
# often ends the command prematurely, and doesn't always return the correct exit code.
# This also has the advantage of running in detached mode, which will log the output of the command
# to Heroku's logplex. An attached `heroku run` command does not have its output logged.
#
# Usage: heroku_run.sh <APP> "<COMMAND_STRING>"
#
set herokuApp [lindex $argv 0];
set commandToExecute [lindex $argv 1];
if {$commandToExecute == "" || $herokuApp == ""} {
puts "Usage: heroku_run.sh <APP> '<COMMAND_STRING>'"
exit 1
}
puts "Command: $commandToExecute"
puts "Heroku App: $herokuApp"
# Each of these commands will be allowed 60s with no matching activity
set timeout 60
spawn heroku run:detached --app $herokuApp $commandToExecute
# Output will look something like:
# Running <COMMAND> on ⬢ <APP>... done, run.5742 (Hobby)
# Run heroku logs --app <APP> --dyno run.5742 to view the output.
expect {
# Wait for the dyno number to be echoed.
-re {dyno run.(\d+)} {
set dynoNumber $expect_out(1,string)
}
}
if {[info exists dynoNumber]} {
# `heroku logs --tail` is a bit finicky.
# Sometimes the connection will get severed, and expect will freak out because we didn't get the exit code we wanted
# So we need to keep retrying this logs command until we get what we want!
set attempt 1
# Let's try this 10 times - why 10? why not?
set max_attempts 10
while {$attempt <= $max_attempts} {
# We want to make sure we haven't missed too much of the logs, so we tell it to give us the last 1k lines of the log
spawn heroku logs --num 1000 --tail --app $herokuApp --dyno "run.$dynoNumber"
# 2017-01-11T20:48:08.482974+00:00 heroku[run.5245]: Starting process with command `echo 'create_test_accounts(2, "[email protected]", true)' | rails c`
# 2017-01-11T20:48:09.073859+00:00 heroku[run.5245]: State changed from starting to up
# 2017-01-11T20:48:14.569735+00:00 heroku[run.5245]: source=run.5245 dyno=heroku.26025121.2f854147-bcb7-4f79-bf99-4e5ba21bd906 sample#memory_total=85.91MB sample#memory_rss=85.91MB sample#memory_cache=0.00MB sample#memory_swap=0.00MB sample#memory_pgpgin=23370pages sample#memory_pgpgout=1376pages sample#memory_quota=512.00MB
# 2017-01-11T20:48:15.527630+00:00 app[run.5245]: Loading production environment (Rails 4.2.7.1)
# We're waiting for text like this:
# "Process exited with status 0"
expect {
-re {Process exited with status (\d+)} {
set exitCode $expect_out(1,string)
exit $exitCode
}
# All lines of output are prefixed with this: heroku[run.5245] or this: app[run.5245]
# As long as we see this occasionally, we know that we're progressing, so we do this so that we don't time out
"run.$dynoNumber" {
exp_continue
}
timeout {
puts "Timeout when waiting for heroku logs. More than $timeout seconds elapsed with no output.";
# In this case, let's just abort and retry the command
close
}
}
puts "Something went wrong when tailing the logs.";
incr attempt
if { $attempt <= $max_attempts } {
puts "We'll try again..."
}
}
# If we made it here, we weren't able to parse and use the exit code
puts "Couldn't find exit code of Heroku process after $max_attempts tries. Giving up."
exit 1
} else {
puts "Couldn't find dyno number for Heroku run"
exit 1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment