heroku_hatchet 5.0.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +69 -0
  3. data/.gitignore +2 -0
  4. data/CHANGELOG.md +32 -1
  5. data/Gemfile +0 -1
  6. data/README.md +772 -205
  7. data/bin/hatchet +11 -4
  8. data/etc/ci_setup.rb +21 -15
  9. data/etc/setup_heroku.sh +0 -2
  10. data/hatchet.gemspec +4 -5
  11. data/hatchet.json +6 -2
  12. data/hatchet.lock +12 -8
  13. data/lib/hatchet.rb +1 -2
  14. data/lib/hatchet/api_rate_limit.rb +13 -24
  15. data/lib/hatchet/app.rb +159 -53
  16. data/lib/hatchet/config.rb +1 -1
  17. data/lib/hatchet/git_app.rb +27 -1
  18. data/lib/hatchet/reaper.rb +159 -56
  19. data/lib/hatchet/reaper/app_age.rb +49 -0
  20. data/lib/hatchet/reaper/reaper_throttle.rb +55 -0
  21. data/lib/hatchet/shell_throttle.rb +71 -0
  22. data/lib/hatchet/test_run.rb +16 -9
  23. data/lib/hatchet/version.rb +1 -1
  24. data/{test → repo_fixtures}/different-folder-for-checked-in-repos/default_ruby/Gemfile +0 -0
  25. data/spec/hatchet/allow_failure_git_spec.rb +40 -0
  26. data/spec/hatchet/app_spec.rb +226 -0
  27. data/spec/hatchet/ci_spec.rb +67 -0
  28. data/spec/hatchet/config_spec.rb +34 -0
  29. data/spec/hatchet/edit_repo_spec.rb +17 -0
  30. data/spec/hatchet/git_spec.rb +9 -0
  31. data/spec/hatchet/heroku_api_spec.rb +30 -0
  32. data/spec/hatchet/local_repo_spec.rb +26 -0
  33. data/spec/hatchet/lock_spec.rb +30 -0
  34. data/spec/spec_helper.rb +25 -0
  35. data/spec/unit/reaper_spec.rb +153 -0
  36. data/spec/unit/shell_throttle.rb +28 -0
  37. metadata +57 -86
  38. data/.travis.yml +0 -16
  39. data/test/fixtures/buildpacks/null-buildpack/bin/compile +0 -4
  40. data/test/fixtures/buildpacks/null-buildpack/bin/detect +0 -5
  41. data/test/fixtures/buildpacks/null-buildpack/bin/release +0 -3
  42. data/test/fixtures/buildpacks/null-buildpack/hatchet.json +0 -4
  43. data/test/fixtures/buildpacks/null-buildpack/readme.md +0 -41
  44. data/test/hatchet/allow_failure_git_test.rb +0 -16
  45. data/test/hatchet/app_test.rb +0 -96
  46. data/test/hatchet/ci_four_test.rb +0 -19
  47. data/test/hatchet/ci_test.rb +0 -11
  48. data/test/hatchet/ci_three_test.rb +0 -9
  49. data/test/hatchet/ci_too_test.rb +0 -19
  50. data/test/hatchet/config_test.rb +0 -51
  51. data/test/hatchet/edit_repo_test.rb +0 -20
  52. data/test/hatchet/git_test.rb +0 -16
  53. data/test/hatchet/heroku_api_test.rb +0 -30
  54. data/test/hatchet/labs_test.rb +0 -20
  55. data/test/hatchet/local_repo_test.rb +0 -26
  56. data/test/hatchet/lock_test.rb +0 -9
  57. data/test/hatchet/multi_cmd_runner_test.rb +0 -30
  58. data/test/test_helper.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d315483e6cd1509b1ea90279b800beb2a969f57956a44f7df08c5844ec9f539b
4
- data.tar.gz: 5f66b2f956bd2820b80dc634d68cea6dc84c7e23079253ef990fe5be8d131d0c
3
+ metadata.gz: 89dec86ebf0199547ebf527242257a88b26c2741b272aa73945b4c907b72f51a
4
+ data.tar.gz: fe0bd8c520fc7102e13ad6f91da96b837158f0ba8eaa324c846e3acc92cb233f
5
5
  SHA512:
6
- metadata.gz: 03570dbbf7075600e686b4f36d1a15d51c812582ab558574c28bb4eb3f80f76618c20bb1bb6d4473a644078fcff0d831360cbfbe12a653727d6609d5a8c66853
7
- data.tar.gz: 107c8c3accdc9690cb7daafd1a0c9a20ea21e2730d9618de2913e6d7c5ff1d6a6191f552890272436ff0833c1456bce67ae8fbafc4eaa29da667a795c5fdd404
6
+ metadata.gz: bad4717807a5cddb7b0cdd2750bdd1811b8b012e767d92173f364795eb1da3a8af41fddb28269d14c67ad8d2cfe7e7d4c7f9cefbbafd575172408787cca41142
7
+ data.tar.gz: b2f7905e07091c4b66ad7b6ba4ac00b8e304e32f10c01a259723cb5e12e592160f9ca30cb66f5485c3bc59dffa26d99bbba070f73be161022d85e63c6afd4409
@@ -0,0 +1,69 @@
1
+ version: 2
2
+ references:
3
+ unit: &unit
4
+ run:
5
+ name: Run test suite
6
+ command: PARALLEL_SPLIT_TEST_PROCESSES=25 bundle exec parallel_split_test spec/
7
+ environment:
8
+ HATCHET_EXPENSIVE_MODE: 1 # !!!! WARNING !!!! ONLY RUN THIS IF YOU WORK FOR HEROKU !!!! WARNING !!!!
9
+ restore: &restore
10
+ restore_cache:
11
+ keys:
12
+ - v1_bundler_deps-{{ .Environment.CIRCLE_JOB }}
13
+ save: &save
14
+ save_cache:
15
+ paths:
16
+ - ./vendor/bundle
17
+ key: v1_bundler_deps-{{ .Environment.CIRCLE_JOB }} # CIRCLE_JOB e.g. "ruby-2.5"
18
+ hatchet_setup: &hatchet_setup
19
+ run:
20
+ name: Hatchet setup
21
+ command: |
22
+ bundle exec hatchet ci:setup
23
+ bundle: &bundle
24
+ run:
25
+ name: install dependencies
26
+ command: |
27
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
28
+ bundle update
29
+ bundle clean
30
+
31
+ jobs:
32
+ "ruby-2.5":
33
+ docker:
34
+ - image: circleci/ruby:2.5
35
+ steps:
36
+ - checkout
37
+ - <<: *restore
38
+ - <<: *bundle
39
+ - <<: *hatchet_setup
40
+ - <<: *unit
41
+ - <<: *save
42
+ "ruby-2.6":
43
+ docker:
44
+ - image: circleci/ruby:2.6
45
+ steps:
46
+ - checkout
47
+ - <<: *restore
48
+ - <<: *bundle
49
+ - <<: *hatchet_setup
50
+ - <<: *unit
51
+ - <<: *save
52
+ "ruby-2.7":
53
+ docker:
54
+ - image: circleci/ruby:2.7
55
+ steps:
56
+ - checkout
57
+ - <<: *restore
58
+ - <<: *bundle
59
+ - <<: *hatchet_setup
60
+ - <<: *unit
61
+ - <<: *save
62
+
63
+ workflows:
64
+ version: 2
65
+ build:
66
+ jobs:
67
+ - "ruby-2.5"
68
+ - "ruby-2.6"
69
+ - "ruby-2.7"
data/.gitignore CHANGED
@@ -1,8 +1,10 @@
1
1
  .DS_Store
2
2
  test/fixtures/repos/*
3
+ repo_fixtures/repos/*
3
4
  *.gem
4
5
 
5
6
 
6
7
  Gemfile.lock
7
8
  debug.rb
8
9
  .ruby-version
10
+ .rspec_status
@@ -1,5 +1,36 @@
1
1
  ## HEAD
2
2
 
3
+ ## 7.0.0
4
+
5
+ - ActiveSupport's Object#blank? and Object#present? are no longer provided by default (https://github.com/heroku/hatchet/pull/107)
6
+ - Remove deprecated support for passing a block to `App#run` (https://github.com/heroku/hatchet/pull/105)
7
+ - Ignore 403 on app delete due to race condition (https://github.com/heroku/hatchet/pull/101)
8
+ - The hatchet.lock file can now be locked to "main" in addition to "master" (https://github.com/heroku/hatchet/pull/86)
9
+ - Allow concurrent one-off dyno runs with the `run_multi: true` flag on apps (https://github.com/heroku/hatchet/pull/94)
10
+ - Apps are now marked as being "finished" by enabling maintenance mode on them when `teardown!` is called. Finished apps can be reaped immediately (https://github.com/heroku/hatchet/pull/97)
11
+ - Applications that are not marked as "finished" will be allowed to live for a HATCHET_ALIVE_TTL_MINUTES duration before they're deleted by the reaper to protect against deleting an app mid-deploy, default is seven minutes (https://github.com/heroku/hatchet/pull/97)
12
+ - The HEROKU_APP_LIMIT env var no longer does anything, instead hatchet application reaping is manually executed if an app cannot be created (https://github.com/heroku/hatchet/pull/97)
13
+ - App#deploy without a block will no longer run `teardown!` automatically (https://github.com/heroku/hatchet/pull/97)
14
+ - Calls to `git push heroku` are now rate throttled (https://github.com/heroku/hatchet/pull/98)
15
+ - Calls to `app.run` are now rate throttled (https://github.com/heroku/hatchet/pull/99)
16
+ - Deployment now raises and error when the release failed (https://github.com/heroku/hatchet/pull/93)
17
+
18
+ ## 6.0.0
19
+
20
+ - Rate throttling is now provided directly by `platform-api` (https://github.com/heroku/hatchet/pull/82)
21
+
22
+ ## 5.0.3
23
+
24
+ - Allow repos to be "locked" to master instead of a specific commit (https://github.com/heroku/hatchet/pull/80)
25
+
26
+ ## 5.0.2
27
+
28
+ - Fix `before_deploy` use with ci test runs (https://github.com/heroku/hatchet/pull/78)
29
+
30
+ ## 5.0.1
31
+
32
+ - Circle CI support in `ci:setup` command (https://github.com/heroku/hatchet/pull/76)
33
+
3
34
  ## 5.0.0
4
35
 
5
36
  - Shelling out to `heroku` commands no longer uses `Bundler.with_clean_env` (https://github.com/heroku/hatchet/pull/74)
@@ -232,7 +263,7 @@
232
263
 
233
264
  ## 1.1.1
234
265
 
235
- - Ensure external commands run inside of `bundle_exec` block are run with propper environment variables set.
266
+ - Ensure external commands run inside of `bundle_exec` block are run with proper environment variables set.
236
267
 
237
268
  ## 1.1.0
238
269
 
data/Gemfile CHANGED
@@ -2,4 +2,3 @@ source "http://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'm'
data/README.md CHANGED
@@ -2,101 +2,130 @@
2
2
 
3
3
  ![](http://f.cl.ly/items/2M2O2Q2I2x0e1M1P2936/Screen%20Shot%202013-01-06%20at%209.59.38%20PM.png)
4
4
 
5
- Hatchet is a an integration testing library for developing Heroku buildpacks.
6
-
7
- [![Build Status](https://travis-ci.org/heroku/hatchet.svg?branch=master)](https://travis-ci.org/heroku/hatchet)
5
+ Hatchet is an integration testing library for developing Heroku buildpacks.
8
6
 
9
7
  ## Install
10
8
 
11
- First run:
12
-
13
- $ bundle install
9
+ To get started, add this gem to your Gemfile:
14
10
 
15
- This library uses the heroku CLI and API. You will need to make your API key available to the system. If you're running on a CI platform, you'll need to generate an OAuth token and make it available on the system you're running on.
11
+ ```
12
+ gem "heroku_hatchet"
13
+ ```
16
14
 
17
- To get a token, install the https://github.com/heroku/heroku-cli-oauth#creating plugin. Then run:
15
+ Then run:
18
16
 
19
- ```sh
20
- $ heroku authorizations:create --description "For Travis"
21
- Creating OAuth Authorization... done
22
- Client: <none>
23
- ID: <id value>
24
- Description: For Travis
25
- Scope: global
26
- Token: <token>
17
+ ```
18
+ $ bundle install
27
19
  ```
28
20
 
29
- You'll set the `<token>` value to the `HEROKU_API_KEY` env var. For example, you could add it on Travis like this:
21
+ This library uses the Heroku CLI and API. You will need to make your API key available to the system (`$ heroku login`). If you're running on a CI platform, you'll need to generate an OAuth token and make it available on the system you're running on see the "CI" section below.
30
22
 
31
- ```sh
32
- $ travis encrypt HEROKU_API_KEY=<token> --add
33
- ```
23
+ ## About Hatchet
34
24
 
35
- You'll also need an email address that goes with your token:
25
+ ### Why Test a Buildpack?
36
26
 
37
- ```sh
38
- $ travis encrypt HEROKU_API_USER=<example@example.com> --add
39
- ```
27
+ Testing a buildpack prevents regressions, and pushes out new features faster and easier.
40
28
 
41
- If you're running locally, your system credentials will be pulled from `heroku auth:token`.
29
+ ### What can Hatchet test?
42
30
 
43
- You'll also need to trigger a "setup" step for CI tasks. You can do it on Travis CI like this:
31
+ Hatchet can easily test certain operations: deployment of buildpacks, getting the build output and running arbitrary interactive processes (e.g. `heroku run bash`). Hatchet can also test running Heroku CI against an app.
32
+
33
+ ### How does Hatchet test a buildpack?
34
+
35
+ To be able to check the behavior of a buildpack, you have to execute it. Hatchet does this by creating new Heroku apps `heroku create`, setting them to use your branch of the buildpack (must be available publicly) `heroku buildpacks:set https://github.com/your/buildpack-url#branch-name`, then deploying the app `git push heroku master`. It has built-in features such as API rate throttling (so your deploys slow down instead of fail) and internal retry mechanisms. Once deployed, it can `heroku run <command>` for you to allow you to assert behavior.
36
+
37
+ ### Can I use Hatchet to test my buildpack locally?
38
+
39
+ Yes, but the workflow is less than ideal since Heroku (and by extension, Hatchet) need your work to be available at a public URL. Let's say you're doing TDD and have already written a single failing test. You are developing on a branch and have already committed the test to that branch. To test your new code, you'll need to commit what you've got, push it to your public source repository.
44
40
 
45
41
  ```
46
- # .travis.yml
47
- before_script: bundle exec hatchet ci:setup
42
+ $ git add -P
43
+ $ git commit -m "[ci skip] WIP"
44
+ $ git push origin <current-branchname>
45
+ $ bundle exec rspec spec/path-to-your-test.rb:5 # This syntax focus runs a single test on line number 5
48
46
  ```
49
47
 
50
- and on Heroku CI like this:
48
+ Now when the tests execute Hatchet will use your code on your public branch. If you don't like a bunch of ugly "wip" commits you can keep amending the same commit over and over while you're iterating, alternatively you can [rebase your commits when you're done](https://www.codetriage.com/rebase).
51
49
 
52
- ```json
53
- {
54
- "environments": {
55
- "test": {
56
- "scripts": {
57
- "test-setup": "bundle exec hatchet ci:setup",
58
- }
59
- }
60
- }
61
- }
62
- ```
50
+ ### Isn't deploying an app to Heroku overkill for testing? I want to go faster.
63
51
 
64
- ## Run the Tests
52
+ Hatchet is for integration testing. You can also unit test your code if you want your tests to execute much quicker. If your buildpack is written in bash, there is [shUnit2](https://github.com/kward/shunit2/), for example. It is recommended that you use both integration and unit tests.
65
53
 
66
- $ bundle exec rake test
54
+ But can't you integration test the buildpack by calling `bin/compile` directly without having to jump through deploying a Heroku app? It is possible to call your `bin/compile` script from your machine locally without Hatchet, but you'll not have access to config vars, addons, release phase, `heroku run`, and many more features. Also, calling `bin/compile` is very slow, and a medium to large buildpack can have upwards of 70 different integration test cases. If each were to take 1 minute optimistically, it would take over an hour to run your whole suite. Since Hatchet can be safely run via a parallel runner, it can execute most of these builds in parallel, and the whole suite would take roughly 5 minutes when running on CI.
67
55
 
68
- ## Why Test a Buildpack?
56
+ In addition to speed, Hatchet provides isolation. Suppose you're executing `bin/compile` locally. In that case, you need to be very careful not to pollute the environment or local disk between runs, or you'll end up with odd failures that are seemingly impossible to hunt down.
69
57
 
70
- Testing a buildpack prevents regressions, and pushes out new features faster and easier.
58
+ ## Quicklinks
59
+
60
+ - Concepts
61
+ - [Tell Hatchet how to find your buildpack](#specify-buildpack)
62
+ - [Give Hatchet some example apps to deploy](#example-apps)
63
+ - [Use Hatchet to deploy app](#deploying-apps)
64
+ - [Use Hatchet to test runtime behavior and environment](#build-versus-run-testing)
65
+ - [How to update or modify test app files safely in parallel](#modifying-apps-on-disk-before-deploy)
66
+ - [Understand how Hatchet (does and does not) clean up apps](#app-reaping)
67
+ - [How to re-deploy the same app](#deploying-multiple-times)
68
+ - [How to test your buildpack on Heroku CI](#testing-ci)
69
+ - [How to safely test locally without modifying disk or your environment](#testing-on-local-disk-without-deploying)
70
+ - [How to set up your buildpack on a Continuous Integration (CI) service](#running-your-buildpack-tests-on-a-ci-service)
71
71
 
72
- ## What can Hatchet Test?
72
+ - Reference Docs:
73
+ - Method arguments to `Hatchet::Runner.new` [docs](#init-options)
74
+ - Method documentation for `Hatchet::Runner` and `TestRun` objects [docs](#app-methods)
75
+ - All ENV vars and what they do [docs](#env-vars)
73
76
 
74
- Hatchet can easily test certain operations: deployment of buildpacks, getting the build output, and running arbitrary interactive processes (e.g. `heroku run bash`). Hatchet can also test running CI against an app.
77
+ - Ruby language and ecosystem basics
78
+ - [Introduction to the Rspec testing framework for non-rubyists](#basic-rspec)
79
+ - [Introduction to Ruby for non-rubyists](#basic-ruby)
75
80
 
76
- ## Writing Tests
81
+ ## Concepts
77
82
 
78
- Hatchet assumes a test framework doesn't exist. [This project](https://github.com/heroku/hatchet) uses `Test::Unit` to run it's own tests. While the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby) uses rspec.
83
+ ### Specify buildpack
79
84
 
80
- Running `focused: true` in rspec allows you to choose which test to run and to tag tests. Rspec has useful plugins, such as `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) to decrease false negatives in your tests.
85
+ Tell Hatchet what buildpack you want to use by default by setting environment variables, this is commonly done in the `spec_helper.rb` file:
86
+
87
+ ```ruby
88
+ ENV["HATCHET_BUILDPACK_BASE"] = "https://github.com/path-to-your/buildpack"
89
+ require 'hatchet'`
90
+ ```
81
91
 
82
- Whatever testing framework you chose, we recommend using a parallel test runner when running the full suite. [Parallel_tests](https://github.com/grosser/parallel_tests) works with rspec and test::unit and is amazing.
92
+ If you do not specify `HATCHET_BUILDPACK_URL` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on CI (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
83
93
 
84
- If you're unfamiliar with the ruby testing eco-system or want some help, start by looking at existing projects.
94
+ The workflow generally looks like this:
85
95
 
86
- *Spoilers: There is a section below on getting Hatchet to work on Travis.
96
+ 1. Make a change to the codebase
97
+ 2. Commit it and push to GitHub so it's publicly available
98
+ 3. Execute your test suite or individual test
99
+ 4. Repeat until you're happy
100
+ 5. Be happy
87
101
 
88
- ## Testing a Buildpack
102
+ ### Example apps:
89
103
 
90
- Hatchet was built for testing the Ruby buildpack, but Hatchet can test any buildpack provided your tests are written in Ruby.
104
+ Hatchet works by deploying example apps to Heroku's production service first you'll need an app to deploy that works with the buildpack you want to test. This method is preferred if you've got a very small app that might only need one or two files. There are two ways to give Hatchet a test app, you can either specify a remote app or a local directory.
105
+
106
+ - **Local directory use of Hatchet:**
107
+
108
+ ```ruby
109
+ Hatchet::Runner.new("path/to/local/directory").deploy do |app|
110
+ end
111
+ ```
91
112
 
92
- You will need copies of applications that can be deployed by your buildpack. You can see the ones for the Hatchet unit tests (and the Ruby buildpack) https://github.com/sharpstone. Hatchet does not require that you keep these apps checked into your git repo which would make fetching your buildpack slow, instead declare them in a `hatchet.json` file (see below).
113
+ An example of this is the [heroku/nodejs buildpack tests](https://github.com/heroku/heroku-buildpack-nodejs/blob/9898b875f45639d9fe0fd6959f42aea5214504db/spec/ci/node_10_spec.rb#L6).
93
114
 
94
- Hatchet will automate retrieving these files `$ hatchet install`, deploy these files using your local copy of the buildpack, retrieve the build output and run commands against deploying applications.
115
+ You can either check in your apps to your source control or, you can use code to generate them, for example:
95
116
 
117
+ - [Generating an example app](https://github.com/sharpstone/force_absolute_paths_buildpack/blob/53c3cffb039fd366b5abb4524fb32983c11f9344/spec/hatchet/buildpack_spec.rb#L5-L20)
118
+ - [Source code for `generate_fixture_app`](https://github.com/sharpstone/force_absolute_paths_buildpack/blob/53c3cffb039fd366b5abb4524fb32983c11f9344/spec/spec_helper.rb#L34-L64)
96
119
 
97
- ## Hatchet.json
120
+ If you generate example apps programmatically, then add the folder you put them in to your `.gitignore`.
98
121
 
99
- Hatchet expects a json file in the root of your buildpack called `hatchet.json`. You can configure install options using the `"hatchet"` key. In this example, we're telling Hatchet to install the given repos to our `test/fixtures` directory instead of the default current directory.
122
+ > Note: If you're not using the `hatchet.json` you'll still need an empty one in your project with contents `{}`
123
+
124
+ - **Github app use of Hatchet:**
125
+
126
+ Instead of storing your apps locally or generating them, you can point Hatchet at a remote github repo. This method of storing apps on GitHub is preferred is you have an app that is large or has many files (for example, a Rails app).
127
+
128
+ Hatchet expects a json file in the root of your buildpack called `hatchet.json`. You can configure the install options using the `"hatchet"` key. In this example, we're telling Hatchet to install the given repos to our `test/fixtures` directory instead of the current default directory.
100
129
 
101
130
  ```
102
131
  {
@@ -127,310 +156,826 @@ You can reference one of these applications in your test by using it's git name:
127
156
  Hatchet::Runner.new('no_lockfile')
128
157
  ```
129
158
 
130
- If you have conflicting names, use full paths.
159
+ If you have conflicting names, use full paths like `Hatchet::RUnner.new("sharpstone/no_lockfile")`.
131
160
 
132
- To test with fixtures that are checked in locally, add the fixture directory to the path and skip the `hatchet install`:
161
+ When you run `hatchet install` it will lock all the Repos to a specific commit. This is done so that if a repo changes upstream that introduces an error the test suite won't automatically pick it up. For example in https://github.com/sharpstone/lock_fail/commit/e61ba47043fbae131abb74fd74added7e6e504df an error is added, but this will only cause a failure if your project intentionally locks to commit `e61ba47043fbae131abb74fd74added7e6e504df` or later.
162
+
163
+ You can re-lock your projects by running `hatchet lock`. This modifies the `hatchet.lock` file. For example:
133
164
 
134
165
  ```
135
- Hatchet::Runner.new("spec/fixtures/repos/node-10-metrics")
166
+ ---
167
+ - - test/fixtures/repos/bundler/no_lockfile
168
+ - 1947ce9a9c276d5df1c323b2ad78d1d85c7ab4c0
169
+ - - test/fixtures/repos/ci/rails5_ci_fails_no_database
170
+ - 3044f05febdfbbe656f0f5113cf5968ca07e34fd
171
+ - - test/fixtures/repos/ci/rails5_ruby_schema_format
172
+ - 3e63c3e13f435cf4ab11265e9abd161cc28cc552
173
+ - - test/fixtures/repos/default/default_ruby
174
+ - 6e642963acec0ff64af51bd6fba8db3c4176ed6e
175
+ - - test/fixtures/repos/lock/lock_fail
176
+ - da748a59340be8b950e7bbbfb32077eb67d70c3c
177
+ - - test/fixtures/repos/lock/lock_fail_main
178
+ - main
179
+ - - test/fixtures/repos/rails2/rails2blog
180
+ - b37357a498ae5e8429f5601c5ab9524021dc2aaa
181
+ - - test/fixtures/repos/rails3/rails3_mri_193
182
+ - 88c5d0d067cfd11e4452633994a85b04627ae8c7
136
183
  ```
137
184
 
138
- Be careful when including repos inside of your test directory. If you're using a runner that looks for patterns such as `*_test.rb` to run your hatchet tests, it may run the tests inside of the repos. To prevent this problem, move your repos directory out of `test/` or into specific directories such as `test/hatchet`. Then change your pattern. If you are using `Rake::TestTask`, it might look like this:
185
+ > Note: If you don't want to lock to a specific commit, you can always use the latest commit by specifying `main` manually as seen above. This will always give you the latest commit on the `main` branch. The `master` keyword is supported as well.
139
186
 
140
- t.pattern = 'test/hatchet/**/*_test.rb'
187
+ ### Deploying apps
141
188
 
142
- When basing tests on external repos, do not change the tests or they may spontaneously fail. We may create a hatchet.lockfile or something to declare the commit in the future.
189
+ Once you've got an app and have set up your buildpack, you can deploy an app and assert based on the output (all examples use rspec for testing framework).
143
190
 
191
+ ```ruby
192
+ Hatchet::Runner.new("default_ruby").deploy do |app|
193
+ expect(app.output).to match("Installing dependencies using bundler")
194
+ end
195
+ ```
144
196
 
145
- ## Deploying apps
197
+ By default, an error will be raised if the deploy doesn't work, which forces the test to fail. If you're trying to test failing behavior (for example you want to test that an app without a `Gemfile.lock` fails to build), you can manually allow failures:
146
198
 
199
+ ```ruby
200
+ Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
201
+ expect(app).not_to be_deployed
202
+ expect(app.output).to include("Gemfile.lock required")
203
+ end
204
+ ```
147
205
 
148
- You can specify the location of your public buildpack url in an environment variable:
206
+ ### Build versus run testing
149
207
 
150
- ```sh
151
- HATCHET_BUILDPACK_BASE=https://github.com/heroku/heroku-buildpack-ruby.git
152
- HATCHET_BUILDPACK_BRANCH=master
208
+ In addition to testing what the build output was, the next most common thing to assert is that behavior at runtime produces expected results. Hatchet provides a helper for calling `heroku run <cmd>` and asserting against it. For example:
209
+
210
+ ```ruby
211
+ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app, heroku|
212
+ expect(app.run("which node")).to match("/app/bin/node")
213
+ end
153
214
  ```
154
215
 
155
- If you do not specify `HATCHET_BUILDPACK_URL` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on travis (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
216
+ In this example, Hatchet is calling `heroku run which node` and passing the results back to the test so we can assert against it.
156
217
 
157
- If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries, an error will be raised.
218
+ - **Asserting exit status:**
158
219
 
159
- If you are testing an app that is supposed to fail deployment, you can set the `allow_failure: true` flag when creating the app:
220
+ In ruby the way you assert a command you ran on the shell was successful or not is by using the `$?` "magic object". By default calling `app.run` will set this variable which can be used in your tests:
160
221
 
161
222
  ```ruby
162
- Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
223
+ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app, heroku|
224
+ expect(app.run("which node")).to match("/app/bin/node")
225
+ expect($?.exitstatus).to eq(0)
226
+ expect($?.success?).to be_truthy
227
+
228
+ # In Ruby all objects except `nil` and `false` are "truthy" in this case it could also be tested using `be_true` but
229
+ # it's best practice to use this test helper in rspec
230
+ end
163
231
  ```
164
232
 
165
- After the block finishes, your app will be queued to be removed from heroku. If you are investigating a deploy, you can add the `debug: true` flag to your app:
233
+ You can disable this behavior [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
234
+
235
+ - **Escaping and raw mode:**
236
+
237
+ By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
238
+
239
+ - **Heroku options:**
240
+
241
+ You can use all the options available to `heroku run bash` such as `heroku run bash --env FOO=bar` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
242
+
243
+ ### Modifying apps on disk before deploy
244
+
245
+ Hatchet is designed to play nicely with running tests in parallel via threads or processes. To support this the code that is executed in the `deploy` block is being run in a new directory. This allows you to modify files on disk safely without having to worry about race conditions. Still, it introduces the unexpected behavior that changes might not work like you think they will.
246
+
247
+ One typical pattern is to have a minimal example app, and then to modify it as needed before your tests. You can do this safely using the `before_deploy` block.
166
248
 
167
249
  ```ruby
168
- Hatchet::Runner.new("rails3_mri_193", debug: true).deploy do |app|
250
+ Hatchet::Runner.new("default_ruby").tap do |app|
251
+ app.before_deploy do
252
+ out = `echo 'ruby "2.7.1"' >> Gemfile`
253
+ raise "Echo command failed: #{out}" unless $?.success?
254
+ end
255
+ app.deploy do |app|
256
+ expect(app.output).to include("Using Ruby version: ruby-2.6.6")
257
+ end
258
+ end
169
259
  ```
170
260
 
171
- After Hatchet is done deploying your app, it will remain on Heroku. It will also output the name of the app into your test logs so that you can `heroku run bash` into it for detailed postmortem.
261
+ This example will add the string `ruby "2.7.1"` to the end of the Gemfile on disk. It accomplishes this by shelling out to `echo`. If you prefer, you can directly use `File.open` to write contents to disk.
262
+
263
+ > Note: The above [tap method in ruby](https://ruby-doc.org/core-2.4.0/Object.html#method-i-tap) returns itself in a block, it makes this example cleaner.
264
+
265
+ > Note: that we're checking the status code of the shell command we're running (shell commands are executed via backticks in ruby), a common pattern is to write a simple helper function to automate this:
266
+
267
+ ```ruby
268
+ # spec_helper.rb
269
+
270
+ def run!(cmd)
271
+ out = `#{cmd}`
272
+ raise "Command #{cmd} failed with output #{out}" unless $?.success?
273
+ out
274
+ end
275
+ ```
172
276
 
173
- If you are wanting to run a test against a specific app without deploying to it, you can specify the app name like this:
277
+ Then you can use it in your tests:
174
278
 
175
279
  ```ruby
176
- app = Hatchet::Runner.new("rails3_mri_193", name: "testapp")
280
+ Hatchet::Runner.new("default_ruby").tap do |app|
281
+ app.before_deploy do
282
+ run!(%Q{echo 'ruby "2.7.1"'})
283
+ end
284
+ app.deploy do |app|
285
+ expect(app.output).to include("Using Ruby version: ruby-2.6.6")
286
+ end
287
+ end
177
288
  ```
178
289
 
179
- Deploying the app takes a few minutes. You may want to skip deployment to make debugging faster.
290
+ > Note: that `%Q{}` is a method of creating a string in Ruby if we didn't use it here we could escape the quotes:
291
+
292
+ ```ruby
293
+ run!("echo 'ruby \"2.7.1\"'")
294
+ ```
180
295
 
181
- If you need to deploy using a different buildpack you can specify one manually:
296
+ In Ruby double quotes allow for the insert operator in strings, but single quotes do not:
182
297
 
183
298
  ```ruby
299
+ name = "schneems"
300
+ puts "Hello #{name}" # => Hello schneems
301
+ puts 'Hello #{name}' # => Hello #{name}
302
+ puts "Hello '#{name}'" # => Hello 'schneems'
303
+ puts %Q{Hello "#{name}"} # => Hello "schneems"
304
+ ```
184
305
 
185
- def test_deploy
186
- Hatchet::Runner.new("rails3_mri_193", buildpack: "https://github.com/heroku/heroku-buildpack-ruby.git").deploy do |app|
187
- # ...
306
+ ### App reaping
307
+
308
+ When your tests are running you'll see hatchet output some details about what it's doing:
309
+
310
+ ```
311
+ Hatchet setup: "hatchet-t-bed73940a6" for "rails51_webpacker"
188
312
  ```
189
313
 
190
- You can specify multiple buildpacks by passing in an array. When you do that you also need to tell hatchet where to place your buildpack. Since hatchet needs to build your buildpack from a branch you should not hardcode a path like `heroku/ruby` instead Hatchet has a replacement mechanism. Use the `:default` symbol where you want your buildpack to execute. For example:
314
+ And later:
191
315
 
192
316
  ```
193
- Hatchet::Runner.new("default_ruby", buildpacks: [:default, "https://github.com/pgbouncer/pgbouncer"])
317
+ Destroying "hatchet-t-fd25e3626b". Hatchet app limit: 80
194
318
  ```
195
319
 
196
- That will expand your buildpack and branch. For example if you're on the `update_readme` branch of the `heroku-buildpack-ruby` buildpack it would expand to:
320
+ By default, Hatchet does not destroy your app at the end of the test run, that way if your test failed unexpectedly if it's not destroyed yet, you can:
197
321
 
198
322
  ```
199
- Hatchet::Runner.new("default_ruby", buildpacks: ["https://github.com/heroku/heroku-buildpack-ruby#update_readme", "https://github.com/pgbouncer/pgbouncer"])
323
+ $ heroku run bash -a hatchet-t-bed73940a6
200
324
  ```
201
325
 
202
- You can also specify a stack:
326
+ And use that to debug. Hatchet deletes old apps on demand. You tell it what your limits are and it will stay within those limits:
203
327
 
204
328
  ```
205
- Hatchet::Runner.new("rails3_mri_193", stack: "cedar-14").deploy do |app|
329
+ HATCHET_APP_LIMIT=20
206
330
  ```
207
331
 
208
- ## Getting Deploy Output
332
+ With these env vars, Hatchet will "reap" older hatchet apps when it sees there are 20 or more hatchet apps. For CI, it's recommended that you increase the `HATCHET_APP_LIMIT` to 80-100. Hatchet will mark apps as safe for deletion once they've finished, and the `teardown!` method has been called on them (it tracks this by enabling maintenance mode on apps). Hatchet only tracks its apps. Hatchet uses a regex pattern on the name of apps to see which ones it can manage. If your account has reached the maximum number of global Heroku apps, you'll need to remove some manually.
209
333
 
210
- After Hatchet deploys your app you can get the output by using `app.output`
334
+ If an app is not marked as being in maintenance mode for some reason, it can be deleted, but only after it has been allowed to live for some time. This behavior is configured by the `HATCHET_ALIVE_TTL_MINUTES` env var. For example, if you set it for `7`, Hatchet will ensure that any apps that are not marked as being in maintenance mode are allowed to live for at least seven minutes. This should give the app time to finish the test's execution, so it is not deleted mid-deploy. When this deletion happens, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. This deletion-mid-test behavior might otherwise be triggered if you have multiple CI runs executing at the same time.
335
+
336
+ It's recommended you don't use your personal Heroku API key for running tests on a CI server since the hatchet apps count against your account maximum limits. Running tests using your account locally is fine for debugging one or two tests.
337
+
338
+ If you find your local account has hit your maximum app limit, one handy trick is to get rid of any old "default" Heroku apps you've created. This plugin (https://github.com/hunterloftis/heroku-destroy-temp) can help:
339
+
340
+ ```
341
+ $ heroku plugins:install heroku-destroy-temp
342
+ $ heroku apps:destroy-temp
343
+ ```
344
+
345
+ > This won't detect hatchet apps, but it's still handy for cleaning up other unused apps.
346
+
347
+ ### Deploying multiple times
348
+
349
+ If your buildpack uses the cache, you'll likely want to deploy multiple times against the same app to assert the cache was used. Here's an example of how to do that:
211
350
 
212
351
  ```ruby
213
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
214
- puts app.output
352
+ Hatchet::Runner.new("python_default").deploy do |app|
353
+ expect(app.output).to match(/Installing pip/)
354
+
355
+ # Redeploy with changed requirements file
356
+ run!(%Q{echo "pygments" >> requirements.txt})
357
+ app.commit!
358
+
359
+ app.push! # <======= HERE
360
+
361
+ expect(app.output).to match("Requirements file has been changed, clearing cached dependencies")
215
362
  end
216
363
  ```
217
364
 
218
- If you told Hatchet to `allow_failure: true`, then the full output of the failed build will be in `app.output` even though the app was not deployed. It is a good idea to test against the output for text that should be present. Using a testing framework such as `Test::Unit` a failed test output may look like this:
365
+ ### Testing CI
366
+
367
+ You can run an app against CI using the `run_ci` command (instead of `deploy`). You can re-run tests against the same app with the `run_again` command.
219
368
 
220
369
  ```ruby
221
- Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
222
- assert_match "Gemfile.lock required", app.output
370
+ Hatchet::Runner.new("python_default").run_ci do |test_run|
371
+ expect(test_run.output).to match("Downloading nose")
372
+ expect(test_run.status).to eq(:succeeded)
373
+
374
+ test_run.run_again
375
+
376
+ expect(test_run.output).to match("installing from cache")
377
+ expect(test_run.output).to_not match("Downloading nose")
223
378
  end
224
379
  ```
225
380
 
226
- Since an error will be raised on failed deploys you don't need to check for a deployed status (the error will automatically fail the test for you).
381
+ > Note: That thing returned by the `run_ci` command is not an "app" object but rather a `test_run` object.
227
382
 
228
- ## Running Processes
383
+ - `test_run.output` will have the setup and test output of your tests.
384
+ - `test_run.app` has a reference to the "app" you're testing against, however currently no `heroku create` is run (as it's not needed to run tests, only a pipeline and a blob of code).
229
385
 
230
- Often times asserting output of a build can only get you so far, and you will need to actually run a task on the dyno. To run a non-interactive command such as `heroku run ls`, you can use the `app.run()` command without passing a block:
386
+ An exception will be raised if either the test times out or a status of `:errored` or `:failed` is returned. If you expect your test to fail, you can pass in `allow_failure: true` when creating your hatchet runner. If you do that, you'll also get access to different statuses:
231
387
 
232
- ```ruby
233
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
234
- assert_match "applications.css", app.run("ls public/assets")
235
- ```
388
+ - `test_run.status` will return a symbol of the status of your test. Statuses include, but are not limited to `:pending`, `:building`, `:errored`, `:creating`, `:succeeded`, and `:failed`
236
389
 
237
- This is useful for checking the existence of generated files such as assets. To run an interactive session such as `heroku run bash` or `heroku run rails console`, run the command and pass a block:
390
+ You can pass in a different timeout to the `run_ci` method `run_ci(timeout: 300)`.
391
+
392
+ You probably need an `app.json` in the root directory of the app you're deploying. For example:
238
393
 
394
+ ```json
395
+ {
396
+ "environments": {
397
+ "test": {
398
+ "addons":[
399
+ "heroku-postgresql"
400
+ ]
401
+ }
402
+ }
403
+ }
239
404
  ```
240
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
241
- app.run("cat Procfile")
405
+
406
+ This is on [a Rails5 test app](https://github.com/sharpstone/rails5_ruby_schema_format/blob/master/app.json) that needs the database to run.
407
+
408
+ Do **NOT** specify a `buildpacks` key in the `app.json` because Hatchet will automatically do this for you. If you need to set buildpacks, you can pass them into the `buildpacks:` keyword argument:
409
+
410
+ ```ruby
411
+ buildpacks = [
412
+ "https://github.com/heroku/heroku-buildpack-pgbouncer.git",
413
+ :default
414
+ ]
415
+
416
+ Hatchet::Runner.new("rails5_ruby_schema_format", buildpacks: buildpacks).run_ci do |test_run|
417
+ # ...
242
418
  end
243
419
  ```
244
420
 
245
- 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:
421
+ > Note that the `:default` symbol (like a singleton string object in Ruby) can be used for where you want your buildpack inserted, it will be replaced with your app's repo and git branch you're testing against.
246
422
 
423
+ ### Testing on local disk without deploying
424
+
425
+ Sometimes you might want to assert something against a test app without deploying. This modification is tricky if you're modifying files or the environment in your test. To help out there's a helper `in_directory_fork`:
426
+
427
+ ```ruby
428
+ Hatchet::App.new('rails6-basic').in_directory_fork do
429
+ require 'language_pack/rails5'
430
+ require 'language_pack/rails6'
431
+
432
+ expect(LanguagePack::Rails5.use?).to eq(false)
433
+ expect(LanguagePack::Rails6.use?).to eq(true)
434
+ end
247
435
  ```
248
- app.run('echo \$HELLO \$NAME', raw: true)
436
+
437
+ ## Running your buildpack tests on a CI service
438
+
439
+ Once you've got your tests working locally, you'll likely want to get them running on CI. For reference, see the [Circle CI config from this repo](https://github.com/heroku/hatchet/blob/master/.circleci/config.yml) and the [Heroku CI config from the ruby buildpack](https://github.com/heroku/heroku-buildpack-ruby/blob/master/app.json).
440
+
441
+ To make running on CI easier, there is a setup script in Hatchet that can be run on your CI server each time before your tests are executed:
442
+
443
+ ```yml
444
+ bundle exec hatchet ci:setup
249
445
  ```
250
446
 
251
- You can specify Heroku flags to the `heroku run` command by passing in the `heroku:` key along with a hash.
447
+ If you're a Heroku employee, see [private instructions for setting up test users](https://github.com/heroku/languages-team/blob/master/guides/create_test_users_for_ci.md) to generate a user a grab the API token.
448
+
449
+ Once you have an API token you'll want to set up these env vars with your CI provider:
252
450
 
253
451
  ```
254
- app.run("nproc", heroku: { "size" => "performance-l" })
255
- # => 8
452
+ HATCHET_APP_LIMIT=100
453
+ HATCHET_RETRIES=2
454
+ HEROKU_API_KEY=<redacted>
455
+ HEROKU_API_USER=<redacted@example.com>
256
456
  ```
257
457
 
258
- You can see a list of Heroku flags by running:
458
+ You can reference this PR for getting a buildpack set up from scratch with tests to see what kinds of files you might need: https://github.com/sharpstone/force_absolute_paths_buildpack/pull/2.
459
+
460
+ ## Reference docs
461
+
462
+ The `Hatchet::Runner.new` takes several arguments.
259
463
 
464
+ ### Init options
465
+
466
+ - stack (String): The stack you want to deploy to on Heroku.
467
+
468
+ ```ruby
469
+ Hatchet::Runner.new("default_ruby", stack: "heroku-16").deploy do |app|
470
+ # ...
471
+ end
260
472
  ```
261
- $ heroku run --help
262
- run a one-off process inside a heroku dyno
263
473
 
264
- USAGE
265
- $ heroku run
474
+ - name (String): The name of an app you want to use. If you choose to provide your own app name, then Hatchet will not reap it, you'll have to delete it manually.
475
+ - allow_failure (Boolean): If set to a truthy value then the test won't error if the deploy fails
476
+ - labs (Array): Heroku has "labs" that are essentially features that are not enabled by default, one of the most popular ones is "preboot" https://devcenter.heroku.com/articles/preboot.
477
+ - buildpacks (Array): Pass in the buildpacks you want to use against your app
266
478
 
267
- OPTIONS
268
- -a, --app=app (required) app to run command against
269
- -e, --env=env environment variables to set (use ';' to split multiple vars)
270
- -r, --remote=remote git remote of app to use
271
- -s, --size=size dyno size
272
- -x, --exit-code passthrough the exit code of the remote command
273
- --no-notify disables notification when dyno is up (alternatively use HEROKU_NOTIFICATIONS=0)
274
- --no-tty force the command to not run in a tty
275
- --type=type process type
479
+ ```ruby
480
+ Hatchet::Runner.new("default_ruby", buildpacks: ["heroku/nodejs", :default]).deploy do |app|
481
+ # ...
482
+ end
276
483
  ```
277
484
 
278
- By default Hatchet will set the app name and the exit code
485
+ In this example, the app would use the nodejs buildpack, and then `:default` gets replaced by your Git url and branch name.
279
486
 
487
+ - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization:
280
488
 
489
+ ```ruby
490
+ Hatchet::Runner.new("default_ruby", before_deploy: ->{ FileUtils.touch("foo.txt")}).deploy do
491
+ # Assert stuff
492
+ end
493
+ ```
494
+
495
+ A block in ruby is essentially an un-named method. Think of it as code to be executed later. See docs below for more info on blocks, procs and lambdas.
496
+
497
+ - config (Hash): You can set config vars against your app:
498
+
499
+ ```ruby
500
+ config = { "DEPLOY_TASKS" => "run:bloop", "FOO" => "bar" }
501
+ Hatchet::Runner.new('default_ruby', config: config).deploy do |app|
502
+ expect(app.run("echo $DEPLOY_TASKS").to match("run:bloop")
503
+ end
281
504
  ```
282
- app.run("exit 127")
283
- puts $?.exitcode
284
- # => 127
505
+
506
+ > A hash in Ruby is like a dict in python. It is a set of key/value pairs. The syntax `=>` is called a "hashrocket" and is an alternative syntax to "json" syntax for hashes. It is used to allow for string keys instead of symbol keys.
507
+
508
+ - `run_multi` (Boolean): Allows you to run more than a single "one-off" dyno at a time (the `HATCHET_EXPENSIVE_MODE` env var must be set to use this feature). By default, "free" Heroku apps are restricted to only allowing one dyno to run at a time. You can increase this limit by scaling an application to paid application, but it will incur charges against your application:
509
+
510
+ ```ruby
511
+ Hatchet::Runner.new("default_ruby", run_multi: true).deploy do |app|
512
+ # This code runs in the background
513
+ app.run_multi("ls") do |out, status|
514
+ expect(status.success?).to be_truthy
515
+ expect(out).to include("Gemfile")
516
+ end
517
+
518
+ # This code runs in the background in parallel
519
+ app.run_multi("ruby -v") do |out, status|
520
+ expect(status.success?).to be_truthy
521
+ expect(out).to include("ruby")
522
+ end
523
+
524
+ # This line will be reached before either of the above blocks finish
525
+ end
285
526
  ```
286
527
 
287
- To skip a value you can use the constant:
528
+ In this example, the `heroku run ls` and `heroku run ruby -v` will be executed concurrently. The order that the `run_multi` blocks execute is not guaranteed. You can toggle this `run_multi` setting on globally by using `HATCHET_RUN_MULTI=1`. Without this setting enabled, you might need to add a `sleep` between multiple `app.run` invocations.
529
+
530
+ WARNING: Enabling `run_multi` setting on an app will charge your Heroku account 🤑.
531
+ WARNING: Do not use `run_multi` if you're not using the `deploy` block syntax or manually call `teardown!` inside the text context [more info about how behavior does not work with the `after` block syntax in rspec](https://github.com/heroku/hatchet/issues/110).
532
+ WARNING: To work, `run_multi` requires your application to have a `web` process associated with it.
533
+
534
+ ### App methods:
288
535
 
536
+ - `app.set_config()`: Updates the configuration on your app taking in a hash
537
+
538
+ You can also update your config using the `set_config` method:
539
+
540
+ ```ruby
541
+ app = Hatchet::Runner.new("default_ruby")
542
+ app.set_config({"DEPLOY_TASKS" => "run:bloop", "FOO" => "bar"})
543
+ app.deploy do
544
+ expect(app.run("echo $DEPLOY_TASKS").to match("run:bloop")
545
+ end
289
546
  ```
290
- app.run("exit 127", heroku: { "exit-code" => Hatchet::App::SkipDefaultOption})
291
- puts $?.exitcode
292
- # => 0
547
+
548
+ - `app.get_config()`: returns the Heroku value for a specific env var:
549
+
550
+ ```ruby
551
+ app = Hatchet::Runner.new("default_ruby")
552
+ app.set_config({"DEPLOY_TASKS" => "run:bloop", "FOO" => "bar"})
553
+ app.get_config("DEPLOY_TASKS") # => "run:bloop"
554
+ ```
555
+
556
+ - `app.set_lab()`: Enables the specified lab/feature on the app
557
+ - `app.add_database()`: adds a database to the app, defaults to the "dev" database
558
+ - `app.run()`: Runs a `heroku run bash` session with the arguments, covered above.
559
+ - `app.run_multi()`: Runs a `heroku run bash` session in the background and yields the results. This requires the `run_multi` flag of the app to be set to `true`, which will charge your application (the `HATCHET_EXPENSIVE_MODE` env var must also be set to use this feature). Example above.
560
+ - `app.create_app`: Can be used to manually create the app without deploying it (You probably want `setup!` though)
561
+ - `app.setup!`: Gets the application in a state ready for deploy.
562
+ - Creates the Heroku app
563
+ - Sets up any specified labs (from initialization)
564
+ - Sets up any specified buildpacks
565
+ - Sets up specified config
566
+ - Calls the contents of the `before_deploy` block
567
+ - `app.before_deploy`: Allows you to update the `before_deploy` block
568
+
569
+ ```ruby
570
+ Hatchet::Runner.new("default_ruby").tap do |app|
571
+ app.before_deploy do
572
+ FileUtils.touch("foo.txt")
573
+ end
574
+ app.deploy do
575
+ end
576
+ end
293
577
  ```
294
578
 
295
- To specify a flag that has no value (such as `--no-notify`, `no-tty`, or `--exit-code`) pass a `nil` value:
579
+ Has the same result as:
580
+
581
+ ```ruby
582
+ before_deploy_proc = Proc.new do
583
+ FileUtils.touch("foo.txt")
584
+ end
296
585
 
586
+ Hatchet::Runner.new("default_ruby", before_deploy: before_deploy_proc).deploy do |app|
587
+ end
297
588
  ```
298
- app.run("echo 'foo'", heroku: { "no-notify" => nil })
299
- # This is the same as `heroku run echo 'foo' --no-notify`
589
+
590
+ - `app.commit!`: Will updates the contents of your local git dir if you've modified files on disk
591
+
592
+ ```ruby
593
+ Hatchet::Runner.new("python_default").deploy do |app|
594
+ expect(app.output).to match(/Installing pip/)
595
+
596
+ # Redeploy with changed requirements file
597
+ run!(%Q{echo "" >> requirements.txt})
598
+ run!(%Q{echo "pygments" >> requirements.txt})
599
+
600
+ app.commit! # <=== Here
601
+
602
+ app.push!
603
+ end
300
604
  ```
301
605
 
302
- ## Modify Application Files on Disk
606
+ > Note: Any changes to disk from a `before_deploy` block will be committed automatically after the block executes
303
607
 
304
- While template apps provided from your `hatchet.json` can provide different test cases, you may want to test minor varriations of an app. You can do this by using the `before_deploy` hook to modify files on disk inside of an app in a threadsafe way that will only affect the app's local instance:
608
+ - `app.in_directory`: Runs the given block in a temp directory (but in the same process). One advanced debugging technique is to indefinitely pause test execution after outputting the directory so you can `cd` there and manually debug:
305
609
 
306
610
  ```ruby
307
- Hatchet::App.new("default_ruby", before_deploy: { FileUtils.touch("foo.txt")}).deploy do
308
- # Assert stuff
611
+ Hatchet::Runner.new("python_default").in_directory do |app|
612
+ puts "Temp dir is: #{Dir.pwd}"
613
+ STDIN.gets("foo") # <==== Pauses tests until stdin receives "foo"
309
614
  end
310
615
  ```
311
616
 
312
- After the `before_deploy` block fires, the results will be committed to git automatically before the app deploys.
617
+ > Note: If you want to execute tests in this temp directory, you likely want to use `in_directory_fork` otherwise, you might accidentally contaminate the current environment's variables if you modify them.
313
618
 
314
- You can also manually call the `before_deploy` method:
619
+ - `app.in_directory_fork`: Runs the given block in a temp directory and inside of a forked process, an example given above.
620
+ - `app.directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
621
+ - `app.deploy`: Your main method takes a block to execute after the deploy is successful. If no block is provided, you must manually call `app.teardown!` (see below for an example).
622
+ - `app.output`: The output contents of the deploy
623
+ - `app.platform_api`: Returns an instance of the [platform-api Heroku client](https://github.com/heroku/platform-api). If Hatchet doesn't give you access to a part of Heroku that you need, you can likely do it with the platform-api client.
624
+ - `app.push!`: Push code to your Heroku app. It can be used inside of a `deploy` block to re-deploy.
625
+ - `app.run_ci`: Runs Heroku CI against the app returns a TestRun object in the block
626
+ - `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will clean up resources, mark the app as being finished (by setting `{"maintenance" => true}` on the app) so that the reaper knows it is safe to delete later. Here is an example of a test that creates and deploys an app manually, then later tears it down manually. If you deploy an application without calling `teardown!` then Hatchet will not know it is safe to delete and may keep it around for much longer than required for the test to finish.
315
627
 
316
628
  ```ruby
317
- app = Hatchet::App.new("default_ruby")
318
- app.before_deploy do
319
- FileUtils.touch("foo.txt")
629
+ before(:each) do
630
+ @app = Hatchet::Runner.new("default_ruby")
631
+ @app.deploy
320
632
  end
321
- app.deploy do
322
- # Assert stuff
633
+
634
+ after(:each) do
635
+ @app.teardown! if @app
636
+ end
637
+
638
+ it "uses ruby" do
639
+ expect(@app.run("ruby -v")).to match("ruby")
323
640
  end
324
641
  ```
325
642
 
326
- Note: If you're going to shell out in this `before_deploy` section, you should check the success of your command, for example:
643
+ - `test_run.run_again`: Runs the app again in Heroku CI
644
+ - `test_run.status`: Returns the status of the CI run (possible values are `:pending`, `:building`, `:creating`, `:succeeded`, `:failed`, `:errored`)
645
+ - `test_run.output`: The output of a given test run
646
+
647
+ ### ENV vars
648
+
649
+ ```sh
650
+ HATCHET_BUILDPACK_BASE=https://github.com/heroku/heroku-buildpack-nodejs.git
651
+ HATCHET_BUILDPACK_BRANCH=<branch name if you dont want Hatchet to set it for you>
652
+ HATCHET_RETRIES=2
653
+ HATCHET_APP_LIMIT=(set to something low like 20 locally, set higher like 80-100 on CI)
654
+ HEROKU_API_KEY=<redacted>
655
+ HEROKU_API_USER=<redacted@redacted.com>
656
+ HATCHET_ALIVE_TTL_MINUTES=7
657
+
658
+ # HATCHET_RUN_MULTI=1 # WARNING: Setting this env var will incur charges against your account. To use this env var you must also enable `HATCHET_EXPENSIVE_MODE`
659
+ # HATCHET_EXPENSIVE_MODE=1 # WARNING: Do not set this environment variable unless you're okay with possibly large bills
660
+ ```
661
+
662
+ > The syntax to set an env var in Ruby is `ENV["HATCHET_RETRIES"] = "2"` all env vars are strings.
663
+
664
+ - `HATCHET_BUILDPACK_BASE`: This is the URL where Hatchet can find your buildpack. It must be public for Heroku to be able to use your buildpack.
665
+ - `HATCHET_BUILDPACK_BRANCH`: By default, Hatchet will use your current git branch name. If, for some reason, git is not available or you want to manually specify it like `ENV["HATCHET_BUILDPACK_BRANCH'] = ENV[`MY_CI_BRANCH`]` then you can.
666
+ - `HATCHET_RETRIES` If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries, an error will be raised. The downside of a larger number is that your suite will keep running for much longer when there are legitimate failures.
667
+ - `HATCHET_APP_LIMIT`: The maximum number of **hatchet** apps that Hatchet will allow in the given account before running the reaper. For local execution, keep this low as you don't want your account dominated by hatchet apps. For CI, you want it to be much larger, 80-100 since it's not competing with non-hatchet apps. Your test runner account needs to be a dedicated account.
668
+ - `HEROKU_API_KEY`: The API key of your test account user. If you run locally without this set, it will use your personal credentials.
669
+ - `HEROKU_API_USER`: The email address of your user account. If you run locally without this set, it will use your personal credentials.
670
+ - `HATCHET_RUN_MULTI`: If enabled, this will scale up deployed apps to "standard-1x" once deployed instead of running on the free tier. This enables the `run_multi` method capability, however scaling up is not free. WARNING: Setting this env var will incur charges to your Heroku account. We recommended never to enable this setting unless you work for Heroku. To use this you must also set `HATCHET_EXPENSIVE_MODE=1`
671
+ - `HATCHET_EXPENSIVE_MODE`: This is intended to be a "safety" environment variable. If it is not set, Hatchet will prevent you from using the `run_multi: true` setting or the `HATCHET_RUN_MULTI` environment variables. There are still ways to incur charges without this feature, but unless you're absolutely confident your test setup will not leave "orphan" apps that are billing you, do not enable this setting. Even then, only set this value if you work for Heroku. To recap WARNING: setting this is expensive.
672
+
673
+ ## Basic
674
+
675
+ ### Basic rspec
676
+
677
+ Hatchet needs to run inside of a test framework such as minitest or rspec. Here's an example of some existing test suites that use Hatchet: [This project](https://github.com/heroku/hatchet) uses rspec to run it's own tests you can use these as a reference as well as the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby). If you're new to Ruby, testing, or Hatchet, it is recommended to reference other project's tests heavily. If you can't pick between minitest and rspec, go with rspec since that's what most reference tests use.
678
+
679
+ Whatever testing framework you chose, we recommend using a parallel test runner when running the full suite. [parallel_split_test](https://github.com/grosser/parallel_split_test).
680
+
681
+ **rspec plugins** - Rspec has useful plugins, such as `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) to decrease false negatives in your tests when running on CI.
682
+
683
+ Rspec is a testing framework for Ruby. It allows you to "describe" your tests using strings and blocks. This section is intended to be a brief introduction and includes a few pitfalls but is not comprehensive.
684
+
685
+ In your directory rspec assumes a `spec/` folder. It's common to have a `spec_helper.rb` in the root of that folder:
686
+
687
+ - **spec/spec_helper.rb**
688
+
689
+ Here's an example of a `spec_helper.rb`: https://github.com/sharpstone/force_absolute_paths_buildpack/blob/master/spec/spec_helper.rb
690
+
691
+ In this file, you'll require files you need to set up the project. You can also set environment variables like `ENV["HATCHET_BUILDPACK_BASE"]`. You can use it to configure your app. Any methods you define in this file will be available to your tests. For example:
327
692
 
328
693
  ```ruby
329
- before_deploy = Proc.new do
330
- cmd = "bundle update"
331
- output = `#{cmd}`
332
- raise "Command #{cmd.inspect} failed unexpectedly with output: #{output}"
694
+ def run!(cmd)
695
+ out = `#{cmd}`
696
+ raise "Error running #{cmd}, output: #{out}" unless $?.success?
697
+ out
333
698
  end
334
- Hatchet::App.new("default_ruby", before_deploy: before_deploy).deploy do
335
- # Assert stuff
699
+ ```
700
+
701
+ - **spec/hatchet/buildpack_spec.rb**
702
+
703
+ Rspec knows a file is a test file or not by the name. It looks for files that end in `spec.rb` you can have as many as you want. I recommend putting them in a "spec/hatchet" sub-folder.
704
+
705
+ - **File contents**
706
+
707
+ In rspec you can group several tests under a "description" using `Rspec.describe`. Here's an example: https://github.com/sharpstone/force_absolute_paths_buildpack/blob/master/spec/hatchet/buildpack_spec.rb
708
+
709
+ An empty example of `spec/hatchet/buildpack_spec.rb` would look like this:
710
+
711
+ ```ruby
712
+ require_relative "../spec_helper.rb"
713
+
714
+ RSpec.describe "This buildpack" do
715
+ it "accepts absolute paths at build and runtime" do
716
+ # expect(true).to eq(true)
717
+ end
336
718
  end
337
719
  ```
338
720
 
339
- It's helpful to make a helper function in your library if this pattern happens a lot in your app.
721
+ Each `it` block represents a test case. If you ever get an error about no method `expect` it might be that you've forgotten to put your test case inside of a "describe" block.
340
722
 
341
- ## Heroku CI
723
+ - **expect syntax**
342
724
 
343
- Hatchet supports testing Heroku CI.
725
+ Once inside of a test, you can assert an expected value against an actual value:
344
726
 
345
727
  ```ruby
346
- Hatchet::Runner.new("rails5_ruby_schema_format").run_ci do |test_run|
347
- assert_match "Ruby buildpack tests completed successfully", test_run.output
728
+ value = true
729
+ expect(value).to eq(true)
730
+ ```
731
+
732
+ This might look like a weird syntax, but it's valid ruby. It's shorthand for this:
733
+
734
+ ```ruby
735
+ expect(value).to(eq(true))
736
+ ```
737
+
738
+ Where `eq` is a method.
739
+
740
+ If you want to assert the opposite, you can use `to_not`:
741
+
742
+ ```ruby
743
+ expect(value).to_not eq(false)
744
+ ```
745
+
746
+ - **matcher syntax**
747
+
748
+ In the above example, the `eq` is called a "matcher". You're matching it against an object. In this case, you're looking for equality `==`.
749
+
750
+ There are other matchers: https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers
751
+
752
+ ```ruby
753
+ expect(value).to be_truthy
754
+
755
+ value = "hello there"
756
+ expect(value).to include("there")
757
+ ```
758
+
759
+ Rspec uses some "magic" to convert anything you pass to
760
+
761
+ Since most values in Hatchet are strings, the ones I use the most are:
762
+
763
+ - Rspec matchers
764
+ - include https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers/include-matcher#string-usage
765
+ - match https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers/match-matcher
766
+
767
+ Generally, I use the include when I know the exact value I want to assert against, I use match when there are dynamic values, and I want to be able to use a regular expression.
348
768
 
349
- test_run.run_again # Runs tests again, for example to make sure the cache was used
769
+ For building regular expressions, I like to use the tool https://rubular.com/ for developing and testing regular expressions. Ruby's regular expression engine is mighty.
350
770
 
351
- assert_match "Using rake", test_run.output
771
+
772
+ - **Keep it simple**
773
+
774
+ Rspec is a massive library with a host of features. It's possible to quickly make your tests unmaintainable and unreadable in the efforts to keep your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). I recommend sticking to only the features mentioned here at first before trying to do anything fancy.
775
+
776
+ - **What to test**
777
+
778
+ Here's a PR with a description of several standard failure modes that lots of buildpacks should be aware of and reference implementations:
779
+
780
+ https://github.com/heroku/heroku-buildpack-python/pull/969
781
+
782
+ - **before(:all) gotcha**
783
+
784
+ In rspec you can use `before` blocks to execute before a test, and `after` blocks to execute after a test. This might sound like you can deploy a hatchet app once and then write multiple tests against that app. However if `before(:all)` can be executed N times if you're running via parallel processes. Example:
785
+
786
+ ```ruby
787
+ # Warning running `before(:all)` in a multi-process test runner context likely executes your
788
+ # block N times where N is the number of tests in that context: https://github.com/grosser/parallel_split_test/pull/22/files
789
+ before(:all) do
790
+ @app = Hatchet::Runner.new("default_ruby") # Warning: This is a gotcha
791
+ @app.deploy
792
+ end
793
+
794
+ after(:all) do
795
+ @app.teardown! if @app # Warning: This is a gotcha
796
+ end
797
+
798
+ it "tests app somehow" do
799
+ expect(@app.run("ruby -v")).to match("ruby") # Warning: This is a gotcha
800
+ end
801
+
802
+
803
+ it "tests app somehow 2" do
804
+ expect(@app.run("ls")).to match("Gemfile") # Warning: This is a gotcha
352
805
  end
353
806
  ```
354
807
 
355
- Call the `run_ci` method on the hatchet `Runner`. The object passed to the block is a `Hatchet::TestRun` object. You can call:
808
+ Running this via the parallel_split_test gem will cause the `before(:all)` block to be invoked multiple times:
356
809
 
357
- - `test_run.output` will have the setup and test output of your tests.
358
- - `test_run.app` has a reference to the "app" you're testing against, however currently no `heroku create` is run (as it's not needed to run tests, only a pipeline and a blob of code).
810
+ ```
811
+ $ PARALLEL_SPLIT_TEST_PROCESSES=3 bundle exec parallel_split_test spec/
812
+ Hatchet setup: "hatchet-t-af7dffc006"
813
+ Hatchet setup: "hatchet-t-bf7dffc006"
814
+ ```
359
815
 
360
- An exception will be raised if either the test times out or a status of `:errored` or `:failed` is returned. If you expect your test to fail, you can pass in `allow_failure: true` when creating your hatchet runner. If you do that, you'll also get access to different statuses:
816
+ It would result in 2 apps being deployed. You can find more information [on the documentation](https://github.com/grosser/parallel_split_test#beforeall-rspec-hooks). For clarity of what will happen behind the scenes when running with multiple processes, it's recommended to use `before(:each)` instead of `before(:all)`.
361
817
 
362
- - `test_run.status` will return a symbol of the status of your test. Statuses include, but are not limited to `:pending`, `:building`, `:errored`, `:creating`, `:succeeded`, and `:failed`
818
+ ### Basic Ruby
363
819
 
364
- You can pass in a different timeout to the `run_ci` method `run_ci(timeout: 300)`.
820
+ If you're not a Ruby specialist, not to worry. Here are a few things you might want to do:
365
821
 
366
- You probably need an `app.json` in the root directory of the app you're deploying. For example:
822
+ - **Write a file and manipulate disk**
367
823
 
368
- ```json
369
- {
370
- "environments": {
371
- "test": {
372
- "addons":[
373
- "heroku-postgresql"
374
- ]
375
- }
376
- }
377
- }
824
+ ```ruby
825
+ File.open("facts.txt", "w+") do |f|
826
+ f.write("equal does not mean equitable")
827
+ end
378
828
  ```
379
829
 
380
- This is on [a Rails5 test app](https://github.com/sharpstone/rails5_ruby_schema_format/blob/master/app.json) that needs the database to run.
830
+ The first argument is the file name, and the second is the object "mode", here `"w+"` means open for writing and create the file if it doesn't exist. If you want to append to a file instead you can use the mode `"a"`.
381
831
 
382
- Do **NOT** specify a `buildpacks` key in the `app.json` because Hatchet will automatically do this for you. If you need to set buildpacks you can pass them into the `buildpacks:` keword argument:
832
+ The file name can be a relative or absolute path. My personal favorite though is using the Pathname class to represent files on disk [ruby Pathname api docs](https://ruby-doc.org/stdlib-2.7.1/libdoc/pathname/rdoc/Pathname.html). You can also use a pathname object to write and manipulate the disk directly:
383
833
 
834
+ ```ruby
835
+ require 'pathname'
836
+ Pathname.new("facts.txt").write("equal does not mean equitable")
384
837
  ```
385
- buildpacks = []
386
- buildpacks << "https://github.com/heroku/heroku-buildpack-pgbouncer.git"
387
- buildpacks << [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
388
838
 
389
- Hatchet::Runner.new("rails5_ruby_schema_format", buildpacks: buildpacks).run_ci do |test_run|
390
- # ...
839
+ - API docs:
840
+ - [File](https://ruby-doc.org/core-2.7.0/File.html)
841
+ - [FileUtils](https://ruby-doc.org/stdlib-2.7.1/libdoc/fileutils/rdoc/FileUtils.html)
842
+ - [Pathname](https://ruby-doc.org/stdlib-2.7.1/libdoc/pathname/rdoc/Pathname.html)
843
+ - [Dir](https://ruby-doc.org/core-2.7.1/Dir.html)
844
+
845
+ - **HEREDOC**
846
+
847
+ You can define a multi-line string in Ruby using `<<~EOM` with a closing `EOM`. Technically, `EOM` can be any string, but you're not here for technicalities.
848
+
849
+ ```ruby
850
+ File.open("bin/yarn", "w") do |f|
851
+ f.write <<~EOM
852
+ #! /usr/bin/env bash
853
+
854
+ echo "Called bin/yarn binstub"
855
+ `yarn install`
856
+ EOM
391
857
  end
392
858
  ```
393
859
 
394
- ## Testing on Travis
860
+ This version of heredoc will strip out indentation:
395
861
 
396
- Once you've got your tests working locally, you'll likely want to get them running on Travis because a) CI is awesome, and b) you can use pull requests to run your all your tests in parallel without having to kill your network connection.
862
+ ```ruby
863
+ puts <<~EOM
864
+ # Notice that the spaces are stripped out of the front of this string
865
+ EOM
866
+ # => "# Notice that the spaces are stripped out of the front of this string"
867
+ ```
397
868
 
398
- Set the `HATCHET_DEPLOY_STRATEGY` to `git`.
869
+ The `~` Is usually the operator for a heredoc that you want, it's supported in Ruby 2.5+.
399
870
 
400
- To run on travis, you will need to configure your `.travis.yml` to run the appropriate commands and to set up encrypted data so you can run tests against a valid heroku user.
871
+ - **Hashes**
401
872
 
402
- For reference see the `.travis.yml` from [hatchet](https://github.com/heroku/hatchet/blob/master/.travis.yml) and the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby/blob/master/.travis.yml). To make running on travis easier, there is a rake task in Hatchet that can be run before your tests are executed
873
+ A hash is like a dict in python. Docs: https://ruby-doc.org/core-2.7.1/Hash.html
403
874
 
404
- ```yml
405
- before_script: bundle exec hatchet ci:setup
875
+ ```ruby
876
+ person_hash = { "name" => "schneems", "level" => 6 }
877
+ puts person_hash["name"]
878
+ # => "schneems"
406
879
  ```
407
880
 
408
- I recommend signing up for a new heroku account for running your tests on travis to prevent exceding your API limit. Once you have the new api token, you can use this technique to [securely send travis the data](http://docs.travis-ci.com/user/environment-variables/#Secure-Variables).
881
+ You can also mutate a hash:
409
882
 
410
- ```sh
411
- $ travis encrypt HEROKU_API_KEY=<token> --add
883
+ ```ruby
884
+ person_hash = { "name" => "schneems", "level" => 6 }
885
+ person_hash["name"] = "Richard"
886
+ puts person_hash["name"]
887
+ # => "Richard"
412
888
  ```
413
889
 
414
- If your Travis tests are containerized, you may need sudo to complete this successfully. In that case, add the following:
890
+ You can inspect full objects by calling `inspect` on them:
415
891
 
416
- ```yml
417
- before_script: bundle exec hatchet ci:setup
418
- sudo: required
892
+ ```ruby
893
+ puts person_hash.inspect
894
+ # => {"name"=>"schneems", "level"=>6}
895
+ ```
896
+
897
+ As an implementation detail note that hashes are ordered
898
+
899
+ - **ENV**
900
+
901
+ You can access the current processes' environment variables as a hash using the ENV object:
902
+
903
+ ```ruby
904
+ ENV["MY_CUSTOM_ENV_VAR"] = "blm"
905
+ puts `echo $MY_CUSTOM_ENV_VAR`.upcase
906
+ # => BLM
907
+ ```
908
+
909
+ All values in an env var must be a string. See the Hash docs for more information on manipulating hashes https://ruby-doc.org/core-2.7.1/Hash.html. Also see the current ENV docs https://ruby-doc.org/core-2.7.1/ENV.html.
910
+
911
+ - **Strings versus symbols**
912
+
913
+ In Ruby you can have a define a symbol `:thing` as well as a `"string"`. They look and behave very closely but are different. A symbol is a singleton object, while the string is unique object. One really confusing thing is you can have a hash with both string and symbol keys:
914
+
915
+ ```ruby
916
+ my_hash = {}
917
+ my_hash["dog"] = "cinco"
918
+ my_hash[:dog] = "river"
919
+ puts my_hash.inspect
920
+ # => {"dog"=>"cinco", :dog=>"river"}
921
+ ```
922
+
923
+ - **Blocks, procs, and lambdas**
924
+
925
+ Blocks are a concept in Ruby for closure. Depending on how it's used it can be an anonymous method. It's always a method for passing around code. When you see `do |app|` that's the beginning of an implicit block. In addition to an implicit block you can create an explicit block using lambdas and procs. In Hatchet, these are most likely to be used to update the app `before_deploy`. Here's an example of some syntax for creating various blocks.
926
+
927
+ ```ruby
928
+ before_deploy = -> { FileUtils.touch("foo.txt") } # This syntax is called a "stabby lambda"
929
+ before_deploy = lambda { FileUtils.touch("foo.txt") } # This is a more verbose lambda
930
+ before_deploy = lambda do
931
+ FileUtils.touch("foo.txt") # Multi-line lambda
932
+ end
933
+ before_deploy = Proc.new { FileUtils.touch("foo.txt") } # A proc and lambda are subtly different, it mostly won't matter to you though
934
+ before_deploy = Proc.new do
935
+ FileUtils.touch("foo.txt") # Multi-line proc
936
+ end
937
+ ```
938
+
939
+ All of these things do the same thing more-or-less. You can execute a block/proc/lambda by running:
940
+
941
+ ```ruby
942
+ before_deploy.call
943
+ ```
944
+
945
+ - **Parens**
946
+
947
+ You might have noticed that some ruby methods use parens and some don't. I.e. `puts "yo"` versus `puts("yo")`. If the parser can determine your intent then you don't have to use parens.
948
+
949
+ - **Debugging**
950
+
951
+ If you're not used to debugging Ruby you can reference the [Ruby debugging magic cheat sheet](https://www.schneems.com/2016/01/25/ruby-debugging-magic-cheat-sheet.html). The Ruby language is very powerful in it's ability to [reflect on itself](https://en.wikipedia.org/wiki/Reflection_%28computer_programming%29). Essentially the Ruby code is able to introspect itself to tell you what it's doing. If you're ever lost, ask your ruby code. It might confuse you, but it won't lie to you.
952
+
953
+ Another good debugging tool is the [Pry debugger and repl](https://github.com/pry/pry).
954
+
955
+ - **Common Ruby errors**
956
+
957
+ ```
958
+ SyntaxError ((irb):14: syntax error, unexpected `end')
419
959
  ```
420
960
 
421
- ## Extra App Commands
961
+ If you see this, it likely means you forgot a `do` on a block, for example `.deploy |app|` instead of `.deploy do |app|`.
422
962
 
423
963
  ```
424
- app.add_database # adds a database to specified app
425
- app.heroku # returns a Herou Api client https://github.com/heroku/heroku.rb
964
+ NoMethodError (undefined method `upcase' for nil:NilClass)
426
965
  ```
427
966
 
967
+ If you see this it means a variable you're using is `nil` unexpectedly. You'll need to use the [above debugging techniques](https://www.schneems.com/2016/01/25/ruby-debugging-magic-cheat-sheet.html) to figure out why.
968
+
969
+ - **More**
970
+
971
+ Ruby is full of multitudes, this isn't even close to being exhaustive, just enough to make you dangerous and write a few tests. It's infinitely useful for testing, writing CLIs and web apps.
972
+
428
973
  ## Hatchet CLI
429
974
 
430
975
  Hatchet has a CLI for installing and maintaining external repos you're
431
976
  using to test against. If you have Hatchet installed as a gem run
432
977
 
433
- $ hatchet --help
978
+ $ Hatchet --help
434
979
 
435
980
  For more info on commands. If you're using the source code you can run
436
981
  the command by going to the source code directory and running:
@@ -438,8 +983,30 @@ the command by going to the source code directory and running:
438
983
  $ ./bin/hatchet --help
439
984
 
440
985
 
441
- ## License
986
+ ## Developing Hatchet
442
987
 
443
- MIT
988
+ If you want to add a feature to Hatchet (this library) you'll need to install it locally and be able to run the tests:
989
+
990
+
991
+ ## Install locally
444
992
 
993
+ ```
994
+ $ git clone https://github.com/heroku/hatchet
995
+ $ cd hatchet
996
+ $ bundle install
997
+ ```
445
998
 
999
+ ### Run the Tests
1000
+
1001
+ ```
1002
+ $ PARALLEL_SPLIT_TEST_PROCESSES=10 bundle exec parallel_split_test spec/
1003
+ ```
1004
+ This will execute all tests, you can also run a single test by specifying a file and line number:
1005
+
1006
+ ```
1007
+ $ bundle exec rspec spec/hatchet/app_spec.rb:4
1008
+ ```
1009
+
1010
+ ## License
1011
+
1012
+ MIT