Demo: Heroku-style Review Apps

A coworker was telling me about Heroku’s review apps. I thought that was pretty cool and would be totally viable with Fly, so here’s how I set that up.

“5/5. Great PR”—satisfied code reviewers and developers

Stateless web app demo, using Ktor GitHub - technillogue/fly-review-demo: A demo for setting up Heroku-style review apps with Fly and GHA. It also uses Mustache for templating and Spotless for linting.

You’ll need to set FLY_API_TOKEN and REVIEW_APP_SECRETS with any secrets your app needs. When you create a pull request, this workflow runs: fly-review-demo/.github/workflows/create-app.yml at main · technillogue/fly-review-demo · GitHub. It does the straightforward process of creating an app, then deploying to it with a remote builder. The only detail is a bash replace pattern for stripping invalid characters in app names. Then, for every push, we check if there’s already a review app using the exit code of fly status –app $review_app_name; if so, we redeploy. Finally, merging or closing the PR destroys the app.

If you need a database, things are a little bit more complicated. Demo for a stateful gRPC server that uses Exposed to talk to a database: GitHub - technillogue/fly-review-db-demo: A demo for setting up Heroku-style review apps that use a database with Fly and GHA. It also features a neat database and gRPC mocking setup.

This time, you’ll need to additionally set REVIEW_DB_NAME. When a PR is opened, that postgres cluster is attached to the new review app. fly postgres attach does its usual thing: creating database and a user with access to it, then sets a DATABASE_URL app secret with the password and internal networking address. Attaching saves time over creating a fresh postgres cluster for each review app, while maintaining isolation. (Also, I found fly postgres create to be kind of flakey. Setting up postgres at the fly launch prompt worked more consistently.)

For database migrations, If your app image can run them on its own you can just use release_command in fly.toml. However, our project uses Liquibase’s Gradle plugin for database version control. Previously, we were using fly proxy, sleeping while the proxy connects, running ./gradlew liquibaseUpdate, and then removing the wireguard peer connection.

To run the migrations in CI, I wanted to avoid that sleep, so I figured out that Liquibase has an offline mode (with JDBC_DATABASE_URL=offline:postgresql) and can output the generated SQL it would run without actually doing it. I use this with a kind of hacky move, running fly postgres connect to get a psql prompt on the newly created database and piping in the generated SQL followed by \n\q to exit the psql shell.

Things to note there:

  • Dashes in the app name are converted to underscores for the database name.
  • This only runs all migrations once; subsequent database changes in the same PR would have to be migrated differently

If I could consistently generate the necessary setup SQL in advance, I wouldn’t even need to bring Gradle to the CI container.

In our case with Exposed, I also needed .replace("postgres://(\\w*):(\\w*)@(.*)".toRegex(), "jdbc:postgresql://$3?user=$1&password=$2")

I might post a similar setup for Python apps, but hopefully it’s clear enough how to set this up for your preferred stack.

Future directions:

  • Having three separate workflows isn’t ideal. When you first create a PR both the create-and-deploy and the deploy workflow are triggered; this can cause a race condition where the app is deployed twice unnecessarily (and makes the list of checks a little bit confusing). There might be a way to fix this with the workflow triggers. Alternatively, it could all be rolled into a single workflow that triggers on everything and checks github.ref_name or environmental variables to figure out if a PR was opened, otherwise if it was a push, otherwise if a PR was closed.
  • Better flags for fly postgres connect, namely --command or --file (see https://community.fly.io/t/feature-request-command-and-file-for-fly-pg-connect).
  • A global --yes flag to skip WARN app flag 'review-app-name' does not match app name in config file 'review-demo' (see Feature request: global --yes for "does not match app name in config").
  • Sometimes fly deploy exits with ==> Creating release Error An unknown error occurred and the run fails even though the deploy was successful.
  • Surfacing a message in the main PR view with a link to the created app like Heroku.
  • Potentially making this available as a ready-to-use Github Action.
5 Likes