GitHub Actions is a workflow system by GitHub that supports continuous integration (CI) and continuous deployment (CD). As CI/CD feature was introduced in 2019, it’s a newcomer in the CI/CD field, but it quickly rised to the de-facto standard CI solution for open source Scala projects.
project/build.properties Continuous integration is a great way of checking that your code works outside of your machine.
If you haven’t created one already, make sure to create project/build.properties and explicitly set the
sbt.version number:
sbt.version=1.10.10
Your build will now use 1.10.10.
A treasure trove of Github Actions tricks can be found in the Github Actions official documentation, including the Reference. Use this guide as an inspiration, but consult the official source for more details.
To build an sbt project on GitHub Actions you will need to config Java (using actions/setup-java) and an sbt launcher (using actions/setup-sbt). A minimal CI workflow for running tests would look something like:
name: CI
on:
  pull_request:
  push:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 11
      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1
      - name: Build and Test
        run: sbt +test
The default JVM options provided by the sbt runner installed by actions/setup-sbt should work for most cases. If you do decide to customize it,
add the -v parameter to your sbt call to enable verbose output:
    - name: Build and Test (with debug)
      run: sbt -v +test
This will cause the Java command line to be logged along with the JVM arguments:
# Executing command line:
java
-Dfile.encoding=UTF-8
-Xms1024m
-Xmx1024m
-Xss4M
-XX:ReservedCodeCacheSize=128m
-jar
/usr/share/sbt/bin/sbt-launch.jar
We can define JAVA_OPTS and JVM_OPTS environment variables to override this.
name: CI
on:
  pull_request:
  push:
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8
      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 11
      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1
      - name: Build and Test
        run: sbt -v +test
Again, let’s check the log to see if the flags are taking effect:
# Executing command line:
[process_args] java_version = '11'
java
-Xms2048M
-Xmx2048M
-Xss6M
-XX:ReservedCodeCacheSize=256M
-Dfile.encoding=UTF-8
-jar
/usr/share/sbt/bin/sbt-launch.jar
+test
You can speed up your sbt builds on GitHub Actions by caching various artifacts in-between the jobs.
The action setup-java has built-in support for caching artifacts downloaded by
sbt when loading the build or when building the project.
To use it, set the input parameter cache of the action setup-java to the value "sbt":
    - name: Setup JDK
      uses: actions/setup-java@v4
      with:
        distribution: temurin
        java-version: 8
        cache: sbt
    - name: Setup sbt launcher
      uses: sbt/setup-sbt@v1
    - name: Build and test
      run: sbt -v +test
Note the added line cache: sbt.
Overall, the use of caching should shave off a few minutes of build time per job.
When creating a continous integration job, it’s fairly common to split up the task into multiple jobs that runs in parallel. For example, we could:
Both use cases are possible using the build matrix. The point here is that we would like to mostly reuse the steps except for a few variance. For tasks that do not overlap in steps (like testing vs deployment), it might be better to just create a different job or a new workflow.
Here’s an example of forming a build matrix using JDK version and operating system.
name: CI
on:
  pull_request:
  push:
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            java: 8
          - os: ubuntu-latest
            java: 17
          - os: windows-latest
            java: 17
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ matrix.java }}
      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1
      - name: Build and test
        shell: bash
        run: sbt -v +test
Note that there’s nothing magical about the os or java keys in the build matrix.
The keys you define become properties in the
matrixcontext and you can reference the property in other areas of your workflow file.
You can create an arbitrary key to iterate over! We can use this and create a key named jobtype to split the work too.
name: CI
on:
  pull_request:
  push:
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            java: 17
            jobtype: 1
          - os: ubuntu-latest
            java: 17
            jobtype: 2
          - os: ubuntu-latest
            java: 17
            jobtype: 3
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ matrix.java }}
      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1
      - name: Build and test (1)
        if: ${{ matrix.jobtype == 1 }}
        shell: bash
        run: |
          sbt -v "mimaReportBinaryIssues; scalafmtCheckAll; +test;"
      - name: Build and test (2)
        if: ${{ matrix.jobtype == 2 }}
        shell: bash
        run: |
          sbt -v "scripted actions/*"
      - name: Build and test (3)
        if: ${{ matrix.jobtype == 3 }}
        shell: bash
        run: |
          sbt -v "dependency-management/*"
Here’s a sample that puts them all together. Remember, most of the sections are optional.
name: CI
on:
  pull_request:
  push:
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            java: 17
            jobtype: 1
          - os: ubuntu-latest
            java: 17
            jobtype: 2
          - os: windows-latest
            java: 17
            jobtype: 2
          - os: ubuntu-latest
            java: 17
            jobtype: 3
    runs-on: ${{ matrix.os }}
    env:
      JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8
      JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ matrix.java }}
          cache: sbt
      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1
      - name: Build and test (1)
        if: ${{ matrix.jobtype == 1 }}
        shell: bash
        run: |
          sbt -v "mimaReportBinaryIssues; scalafmtCheckAll; +test;"
      - name: Build and test (2)
        if: ${{ matrix.jobtype == 2 }}
        shell: bash
        run: |
          sbt -v "scripted actions/*"
      - name: Build and test (3)
        if: ${{ matrix.jobtype == 3 }}
        shell: bash
        run: |  
          sbt -v "dependency-management/*"
There’s also sbt-github-actions, an sbt plugin by Daniel Spiewak that can generate the workflow files, and keep the settings in build.sbt file.