Grails CI with Github Actions
Tags: grails, github, java, groovy
Published: 2019-11-14 00:56:58


I recently was playing with [Github Actions](https://github.com/features/actions) and a Grails 3 app. Was a bit of trial and error though I finally got a working CI workflow.

Github recently added build caching [with limitations](https://github.com/actions/cache#cache-limits) - this dramatically speeds up your CI builds.

A nice aspect about Actions is you can have actions based on not only pushes/PRs/tags but also things like Github comments. For example [`/rebase`](https://github.com/cirrus-actions/rebase) - there's an entire [marketplace of actions](https://github.com/marketplace?type=actions) you can try out.

I've commented the `yml` file to help you get an understanding of whats going on and how it works.

Full syntax for this yml file can be found here:

https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions

I've also included customized test reporting below - since there is no "results" step to see a junit type report. You can find that below the ream of yml.

`.github/workflows/grails.yml`
```yml
name: Grails CI

# When this workflow will run - here it will run on all pushes but 
# not on branch 'master
on:
  push:
    branches-ignore:
      - 'master'

# A list jobs to run when the condition is met - here we've parallelized
# the build to run test, integrationTest, and assemble.
jobs:
  test:
    runs-on: ubuntu-latest
    # A list of Docker containers you require to run the test
    services:
      postgres:
        image: postgres:latest
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: postgres
        ports:
        - 5432:5432

    # Steps in this build 
    steps:
    - uses: actions/checkout@v1
    # We are caching the node_modules, ~/.gradle and app/.gradle   
    # to speed up our build. Caches are automatically stored and restored
    # based on a file hash - in this case yarn.lock and build.gradle
    - name: Cache node modules
      uses: actions/cache@v1
      with:
        path: src/main/vue/node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-node-
    - name: Cache .gradle
      uses: actions/cache@v1
      with:
        path: .gradle
        key: ${{ runner.os }}-dotgradle-${{ hashFiles('**/build.gradle') }}
        restore-keys: |
          ${{ runner.os }}-dotgradle-
    - name: Cache gradle
      uses: actions/cache@v1
      with:
        path: /home/runner/.gradle
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}
        restore-keys: |
          ${{ runner.os }}-gradle-
    # Setup JDK 8
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    # Finally run the test.
    - name: test
      run: ./gradlew test --no-daemon

  # Integration and assemble are much the same
  # though it's yml so not DRY - omitted the copy/paste
  integrationTest:
    steps:
      - name: integrationTest
        run: ./gradlew integrationTest --no-daemon
  assemble:
    steps:
      - name: assemble
        run: ./gradlew assemble --no-daemon
```

And here is a test formatter that gives you the Spock errors to stdout nicely for debugging as well as a summary:

`.gradle/testFormatter.gradle`
```groovy
// From https://stackoverflow.com/a/40656862
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent

tasks.withType(Test) {
    testLogging {
        // set options for log level LIFECYCLE
        events TestLogEvent.FAILED,
                TestLogEvent.PASSED,
                TestLogEvent.SKIPPED
        exceptionFormat TestExceptionFormat.SHORT
        showExceptions true
        showCauses true
        showStackTraces true

        // set options for log level DEBUG and INFO
        debug {
            events TestLogEvent.STARTED,
                    TestLogEvent.FAILED,
                    TestLogEvent.PASSED,
                    TestLogEvent.SKIPPED
            exceptionFormat TestExceptionFormat.SHORT
        }
        info.events = debug.events
        info.exceptionFormat = debug.exceptionFormat

        afterSuite { desc, result ->
            if (!desc.parent) { // will match the outermost suite
                def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
                def startItem = '|  ', endItem = '  |'
                def repeatLength = startItem.length() + output.length() + endItem.length()
                println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength))
            }
        }
    }
}
```