Scala CI-CD with Github Actions and Heroku
( 10 min read )
We’ll here’s a quick guide on how to continuously run sbt test
on every pull request, and after merging deploy it to Heroku.
And we’ do this all within the comfort of GitHub, using their automation tool called GitHub Actions.
TL;DR: If you want to skip everything, here’s the git repo. 👍️
Requirements
Things you’ll need:
What you DON’T need:
- Heroku CLI …We’ll use a sbt plugin instead.
You can easily swap out Heroku for your favorite platform-as-a-service. But I like to use Heroku for doing rapid prototyping, testing, or live demos.
Init Sbt
Let’s setup a simple Scala app with some tests using Scalatest. If you have a Scala app already, skip to the next step.
$ sbt new akka/akka-http-quickstart-scala.g8
This command helps you start a new sbt project from a few simple questions. Just name your project
and press Enter
several times to accept the defaults.
$ sbt new akka/akka-http-quickstart-scala.g8
[info] Set current project to test (in build file:/home/amdelamar/workspace/)
[info] Set current project to test (in build file:/home/amdelamar/workspace/)
This is a seed project which creates a basic build for an Akka HTTP
application using Scala.
name [My Akka HTTP Project]: my-scala-app
scala_version [2.13.1]:
akka_http_version [10.1.10]:
akka_version [2.6.0]:
organization [com.example]:
package [com.example]:
Template applied in /home/amdelamar/workspace/./my-scala-app
Init Repo
Open the app up and start a new git repo like so. Or using GitHub to create it and clone it locally works fine too.
# initalize a new repo
$ mkdir my-scala-app # or whatever you called it
$ cd my-scala-app
$ git init
Edit the Project
We’ll need to modify this project to support Heroku deployments and GitHub Action workflows.
First, we’ll need two more plugins:
// add these to project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.25")
addSbtPlugin("com.heroku" % "sbt-heroku" % "2.1.2")
One plugin is for packaging your Scala app into a native package format, like a
.tar
, .zip
, or .jar
file, depending on your OS you wish to target. The
other plugin is for publishing your packaged Scala app to Heroku using their APIs.
This allows us to easily deploy in one command which you will see in a bit.
Ok, now add these at the bottom of the build.sbt
file.
// build.sbt
// ...
enablePlugins(JavaAppPackaging)
// heroku deployment configs
herokuAppName in Compile := "my-scala-app" // unique Heroku app name
herokuJdkVersion in Compile := "1.8"
herokuConfigVars in Compile := Map(
"HOST" -> "0.0.0.0"
)
herokuProcessTypes in Compile := Map(
"web" -> "target/universal/stage/bin/my-scala-app" // project name
)
If you’ve used Heroku before, you might decide to add a Procfile to your repo. This file is normally required for Heroku apps, but we use the sbt-heroku plugin to generate one for us during deployment.
Now the app needs to support reading from two environment variables to override the host and port options for our Akka Http server. This lets Heroku bind the application container correctly so it can be reachable via their generated url they use. “0.0.0.0” is required for the host, and typically the next random available port is chosen, which means we can’t always use port 8080 for deployment.
Next up, The main class QuickstartApp.scala
has these lines but…
//#main-class
object QuickstartApp {
//#start-http-server
private def startHttpServer(routes: Route, system: ActorSystem[_]): Unit = {
// Akka HTTP still needs a classic ActorSystem to start
implicit val classicSystem: akka.actor.ActorSystem = system.toClassic
import system.executionContext
val futureBinding = Http().bindAndHandle(routes, "localhost", 8080)
}
…we need to modify it with the two environment variables:
//#main-class
object QuickstartApp {
//#start-http-server
private def startHttpServer(routes: Route, system: ActorSystem[_]): Unit = {
// Akka HTTP still needs a classic ActorSystem to start
implicit val classicSystem: akka.actor.ActorSystem = system.toClassic
import system.executionContext
// Check env vars
val HOST = sys.env.getOrElse("HOST", "localhost")
val PORT = scala.util.Try(sys.env("PORT").toInt).getOrElse(8080)
val futureBinding = Http().bindAndHandle(routes, HOST, PORT)
}
Instead of hard-coding “0.0.0.0”, we’ll leave that to the build.sbt
config.
That way, we can still sbt run
this app for local development.
Now the final edit, before we’re ready to push to GitHub. We need to define our GitHub Actions, or workflows. These will let us control the commands run, and what they’re run on, during Pull Requests and merging.
Create these two files under this directory:
$ mkdir -p .github/workflows
$ touch .github/workflows/master-pull-request.yml
$ touch .github/workflows/master-push.yml
And edit them like so:
# master-pull-request.yml
name: master-pull-request
on:
pull_request:
branches:
- master
jobs:
test:
name: sbt test
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: Run sbt test
run: |
sbt compile
sbt test
# master-push.yml
name: master-push
on:
push:
branches:
- master
jobs:
deploy:
name: heroku deploy
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: Run sbt stage deployHeroku
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
run: |
sbt stage deployHeroku
The yamls are self-describing, but you can read the GitHub docs for more info. Here, we are simply enabling
two workflows. One triggers on every pull request opened to master
branch. The
the other triggers on every git push
or merge
to master
branch. The jobs
defined will run once using the defined OS (Ubuntu 18), and execute the given commands.
Feel free to edit these to do more than just the above!
Git Push
If you’ve followed along correctly, you should be ready to stage, commit, and push these files.
$ git add .
$ git commit -m "init"
$ git remote add origin [email protected]:amdelamar/my-scala-app.git
$ git push -u origin master
And that’s it!
Heroku App & API Key
Err, actually there is one last thing.
If you go to the GitHub project’s Actions tab, you might notice that first commit produced a failing job:
The deployment command failed. I forgot to mention that you’ll need to create the app placeholder in Heroku, as well as give GitHub your Heroku API secret key, so it can run push to Heroku on your behalf.
Open the Heroku Dashboard and click the “New” button to create a new app. Give it a unique name.
Note: The heroku app name has to be unique, but it doesn’t have to be the same as the project name itself. Go ahead and name it anything. But be sure to edit your build.sbt with the actual value once you’re done.
Now you have a placeholder to deploy to. But we need that API key for letting GitHub
Actions deploy on our behalf. Go find your API key and copy it.
In Heroku, its under Account settings
-> API key
-> and click “Regenerate API key”.
You’ll have to save this key in GitHub’s secrets. In the GitHub repo, go to Settings
-> Secrets
-> “Add new secret”.
Enter it as HEROKU_API_KEY
and paste in the secret key.
Done!
Testing a PR
Alright, now we’re ready to open a pull request and run some automated tests!
Make a new branch, push a change, and open a PR. Watch the automated checks kickoff and update your PR with a ✔️ pass or ❌️ fail depending on the results.
After the checks pass, (assuming they pass!), merge your PR. Watch the automated deployment trigger next. You can click on “Actions” tab and monitor it there, or open Heroku dashboard and view the activity and logs of your app.
After it deploys successfully, you can view it at: https://<your-app-name>.herokuapp.com
e.g. https://my-scala-app.herokuapp.com/users
(The /users is part of the default akka g8 template we used.)
Future TODOs
Pretty much any command that you would run on your Scala project,
you could add to a job
in a workflow file.
Things like, linting, code coverage, packaging, publishing to Maven Central or Bintray, publishing Docker images, and more, could easily be added here. Go wild!
Published: Nov 19, 2019
Category: code