DEV Community

Eric Helgeson
Eric Helgeson

Posted on

Grails CI with Github Actions

I recently was playing with Github 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 - 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 - there's an entire marketplace of 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

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

// 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))
            }
        }
    }
}

Top comments (0)