Skip to content

Commit

Permalink
Merge pull request #72 from heroku/schneems/multi-commands-run
Browse files Browse the repository at this point in the history
[Close #71] Do not escape to local system shell
  • Loading branch information
schneems authored Apr 16, 2020
2 parents 0df866b + 014c343 commit 9c07a53
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 20 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
## HEAD

## 5.0.0

- Shelling out to `heroku` commands no longer uses `Bundler.with_clean_env` (https://github.com/heroku/hatchet/pull/74)
- CI runs can now happen multiple times against the same app/pipeline (https://github.com/heroku/hatchet/pull/75)
- Breaking change: Do not allow App#run to escape to system shell by default (https://github.com/heroku/hatchet/pull/72)

> Note: If you were relying on this behavior, use the `raw: true` option.
- ReplRunner use with App#run is now deprecated (https://github.com/heroku/hatchet/pull/72)
- Calling App#run with a nil in the second argument is now deprecated (https://github.com/heroku/hatchet/pull/72)

## 4.1.2

Expand Down
61 changes: 52 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,19 +242,62 @@ Hatchet::Runner.new("rails3_mri_193").deploy do |app|
end
```

This is the prefered way to run commands against the app. You can also string together commands in a session, but it's less deterministic due to difficulties in driving a REPL programatically via [repl_runner](https://github.com/schneems/repl_runner).
By default commands will be shell escaped (to prevent commands from escaping the `heroku run` command), if you want to manage your own quoting you can use the `raw: true` option:

```
app.run('echo \$HELLO \$NAME', raw: true)
```

```ruby
Hatchet::Runner.new("rails3_mri_193").deploy do |app|
app.run("bash") do |bash|
bash.run("ls") {|result| assert_match "Gemfile.lock", result }
bash.run("cat Procfile") {|result| assert_match "web:", result }
end
end
You can specify Heroku flags to the `heroku run` command by passing in the `heroku:` key along with a hash.

```
app.run("nproc", heroku: { "size" => "performance-l" })
# => 8
```

You can see a list of Heroku flags by running:

```
$ heroku run --help
run a one-off process inside a heroku dyno
USAGE
$ heroku run
OPTIONS
-a, --app=app (required) app to run command against
-e, --env=env environment variables to set (use ';' to split multiple vars)
-r, --remote=remote git remote of app to use
-s, --size=size dyno size
-x, --exit-code passthrough the exit code of the remote command
--no-notify disables notification when dyno is up (alternatively use HEROKU_NOTIFICATIONS=0)
--no-tty force the command to not run in a tty
--type=type process type
```

Please read the docs on [repl_runner](https://github.com/schneems/repl_runner) for more info. The only interactive commands that are supported out of the box are `rails console`, `bash`, and `irb`. It is fairly easy to add your own though.
By default Hatchet will set the app name and the exit code


```
app.run("exit 127")
puts $?.exitcode
# => 127
```

To skip a value you can use the constant:

```
app.run("exit 127", heroku: { "exit-code" => Hatchet::App::SkipDefaultOption})
puts $?.exitcode
# => 0
```

To specify a flag that has no value (such as `--no-notify`, `no-tty`, or `--exit-code`) pass a `nil` value:

```
app.run("echo 'foo'", heroku: { "no-notify" => nil })
# This is the same as `heroku run echo 'foo' --no-notify`
```

## Modify Application Files on Disk

Expand Down
22 changes: 20 additions & 2 deletions lib/hatchet/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,25 @@ def add_database(plan_name = 'heroku-postgresql:dev', match_val = "HEROKU_POSTGR
end
end

DefaultCommand = Object.new
# runs a command on heroku similar to `$ heroku run #foo`
# but programatically and with more control
def run(cmd_type, command = nil, options = {}, &block)
command = cmd_type.to_s if command.nil?
def run(cmd_type, command = DefaultCommand, options = {}, &block)
case command
when Hash
options.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
command = command.shellescape unless options.delete(:raw)

default_options = { "app" => name, "exit-code" => nil }
heroku_options = (default_options.merge(options.delete(:heroku) || {})).map do |k,v|
next if v == Hatchet::App::SkipDefaultOption # for forcefully removing e.g. --exit-code, a user can pass this
Expand All @@ -122,7 +137,10 @@ def run(cmd_type, command = nil, options = {}, &block)
arg
end.join(" ")
heroku_command = "heroku run #{heroku_options} -- #{command}"

if block_given?
STDERR.puts "Using App#run with a block is deprecated, support for ReplRunner is being removed.\n#{caller}"
# When we deprecated this we can get rid of the "cmd_type" from the method signature
require 'repl_runner'
ReplRunner.new(cmd_type, heroku_command, options).run(&block)
else
Expand Down
2 changes: 1 addition & 1 deletion lib/hatchet/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Hatchet
VERSION = "4.1.2"
VERSION = "5.0.0"
end
24 changes: 16 additions & 8 deletions test/hatchet/app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,24 @@ def test_run
app.deploy do
assert_match(/ls: cannot access 'foo bar #baz': No such file or directory\s+Gemfile/, app.run("ls -a Gemfile 'foo bar #baz'"))
assert (0 != $?.exitstatus) # $? is from the app.run use of backticks
sleep(3)
app.run("ls erpderp", nil, { :heroku => { "exit-code" => Hatchet::App::SkipDefaultOption } } )
sleep(4) # Dynos don't exit right away and free dynos can't have more than one running at a time, wait before calling `run` again

app.run("ls erpderp", { :heroku => { "exit-code" => Hatchet::App::SkipDefaultOption } } )
assert (0 == $?.exitstatus) # $? is from the app.run use of backticks, but we asked the CLI not to return the program exit status by skipping the default "exit-code" option
sleep(3)
app.run("ls erpderp", nil, { :heroku => { "no-tty" => nil } } )
sleep(4)

app.run("ls erpderp", { :heroku => { "no-tty" => nil } } )
assert (0 != $?.exitstatus) # $? is from the app.run use of backticks
sleep(3)
assert_match(/ohai world/, app.run('echo \$HELLO \$NAME', nil, { :heroku => { "env" => "HELLO=ohai;NAME=world" } } ))
sleep(3)
refute_match(/ohai world/, app.run('echo \$HELLO \$NAME', nil, { :heroku => { "env" => "" } } ))
sleep(4)

assert_match(/ohai world/, app.run('echo \$HELLO \$NAME', { raw: true, :heroku => { "env" => "HELLO=ohai;NAME=world" } } ))
sleep(4)

refute_match(/ohai world/, app.run('echo \$HELLO \$NAME', { raw: true, :heroku => { "env" => "" } } ))
sleep(4)

random_name = SecureRandom.hex
assert_match(/#{random_name}/, app.run("mkdir foo; touch foo/#{random_name}; ls foo/"))
end
end
end

0 comments on commit 9c07a53

Please sign in to comment.