How to build Gradle projects with GitHub Actions

If you have a Gradle project in GitHub, then GitHub Actions can build it automatically on push with minimal setup. And it won’t cost you a penny. While other CI solutions involve the headache of managing servers, GitHub Actions is entirely hosted on their infrastructure. In this article you’ll learn how to easily build Gradle projects with GitHub Actions, optimise build performance, and use the event driven approach to handle common scenarios like PR creation.

1. Why use GitHub Actions with Gradle?

GitHub Actions was recently released in November 2019, offering a continuous integration (CI) solution to take on the big boys like Jenkins and GitLab. If you haven’t tried it yet, here are the main advantages of using it to build Gradle projects.

  • 🤝 tight integration between version control management (VCM) & CI systems
    if you already store source code in GitHub, then GitHub Actions seamlessly integrates with it to react to repository events (e.g. push, create pull request), check out your code without worrying about authentication, and show build results all within a single unified GitHub UI.
  • 🆓 it’s free
    yes, I already mentioned this but it’s worth repeating. For public repositories you get unlimited minutes of build time, while within private repositories you get 2,000 free minutes per month.
  • 🔒 not just for open source projects
    while GitHub is hugely popular with open source projects like Spring Boot & Kubernetes, you can also setup private repositories and organizations. This allows teams of any size to securely develop applications. And of course GitHub Actions works here too.
  • designed with Java & Gradle in mind
    if you thought there’d be lots of setup to get GitHub Actions building Gradle, think again! Everything’s been considered, including installing the required Java version on the GitHub Actions runner (where your project gets built). As you’ll see in the step-by-step example later, running Gradle is a breeze and it even has caching for fast performance.
  • 🐘 leverage the full power of Gradle within GitHub
    while GitHub Actions is great (did I mention that already?), let’s not forget that a lot of the magic happens within the Gradle build process itself. And since Gradle is a Groovy/Kotlin based build tool, it can do whatever you need, including building, testing, and publishing your application.
  • 👪 community driven
    the active GitHub community publish helpful actions to a marketplace for you to include in your build workflow. These actions do useful work in your CI process like downloading the right Java version for Gradle to use. This likely means anything you need to do will already have an action.

Perspective of a long-term Jenkins user

Since I’ve used Jenkins heavily in the last few years, I was excited to get into GitHub Actions. Anyone that’s read my Jenkins tutorials knows there’s a lot of work not only to manage Jenkins but also to setup scalable infrastructure. GitHub Actions is a huge time-saver for anyone willing to jump into that ecosystem. At the end of the article you’ll learn some downsides of GitHub Actions and see a direct comparison with Jenkins.

2. How does GitHub Actions work?

Hopefully now you’re convinced there are at least some advantages to using GitHub Actions. Before you learn how to build a real Gradle project in GitHub, let’s explore some of the core concepts.

Imagine a scenario where your enthusiastic colleague pushes code changes to your Gradle project repository hosted on GitHub. We’ll explore step-by-step what happens to achieve the goal of building the project, resulting in either success or failure. As we go, you’ll learn the relevant GitHub specific terminology.

  1. Mr. Developer pushes the code which triggers an event in GitHub. push is one of many events.
  2. push triggers a workflow, which is your CI pipeline defined in a YAML file in your repository.
  3. The workflow has a job which runs in its own virtual machine, called a GitHub Action runner.
  4. In the runner the job can access a workspace directory, where you check out the source code.
  5. A job has multiple steps which get run in sequence. There are two types, actions and commands.
    • action steps are reusable components available from the GitHub Marketplace. Two useful actions are actions/checkout and actions/setup-java, which we’ll use later.
    • command steps are instructions like ./gradlew build or anything supported by the operating system.
  6. Steps run against the workspace. In this example, the actions/checkout action puts the source code in the workspace, then the ./gradlew build command builds the application.
  7. The workflow succeeds or fails. This time it fails, automatically emailing our developer. His enthusiasm quickly disappears. 😠
GitHub Actions overview

If that seems like a lot to remember, then don’t worry because to implement GitHub Actions in a Gradle project is actually very straightforward. Let’s jump into an example and it will start to make sense.

3. Create a GitHub workflow in a Gradle project

If you came to me and asked how to setup GitHub Actions in your Gradle project, I could tell you to a) click the Actions tab in the GitHub UI then b) click Set up this workflow on the suggested Gradle workflow.

But I’d be doing you a disservice because you’d learn nothing about how workflows work or how to customise them to your requirements.

You could click this button. But if you want to learn something, then read on.

Instead we’re going to create a workflow file by hand, understanding what each part does.

Pre-requisites

  1. you need a GitHub account
  2. you need a Gradle project in GitHub which you can run a ./gradlew build command against. If not, feel free to clone one of my sample projects like spring-boot-api-example.

Create the workflow file

The workflow file defines what you want to happen when certain events are triggered against your repository. It lives in your repository within a .github/workflows directory structure.

Create a file .github/workflows/gradle.yml file in your project. The file name is not important. Remember this is YAML syntax, so open it up in your IDE for easy editing.

Add the following configuration.

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
          distribution: 'adopt'
      - name: Build with Gradle
        run: ./gradlew build

Let’s unpack this line by line.

  • name gives the workflow name of CI. This is the name that appears in the GitHub UI.
  • on define a list of events that trigger this workflow, in this case push. The workflow will run whenever code is pushed to this repository on any branch.
  • jobs in our case is a single job called build.
  • runs-on specifies the GitHub Actions runner type to use. We’ll use Ubuntu, but you can also pick Windows and MacOS VMs.
  • steps defines the work we want to be run inside our runner. In our case it needs to checkout the code, install Java, then build the Gradle project.
    • the first step is an action type step which uses a GitHub provided action actions/checkout@v2. Note that the action contains a version number to ensure consistent behaviour every time.
    • the second step is another action type step, using actions/setup-java@v2 also provided by GitHub. We provide a name for the action which helps identify what it’s doing in the GitHub UI. Using with we can pass input parameters to the action, which in this case is required to specify java-version and distribution.
    • the final step is a command type step, which runs the build task using the Gradle wrapper.

That’s all there is to it! At this point, you can commit and push the file and GitHub will take care of the rest.

git commit -am "Setup workflow file for GitHub actions."
git push

Oh, and don’t forget to make your gradlew file executable with git update-index --chmod=+x gradlew or you won’t get very far.

4. View build results in GitHub actions UI

Log into GitHub, navigate to your repository, then click the Actions tab. You’ll see your new workflow, with a queued or in progress workflow run.

If it says queued, that means that GitHub is provisioning resources to run your job. Normally it moves to in progress after a few seconds.

Click on the workflow run (the text that says Setup workflow file for GitHub actions) and you’ll see details of the job that’s running.

We only have a single build job, so there’s not too much to see here. This page refreshes dynamically, so once the workflow completes you can see things like the status and total duration.

Click on the job name on the left, to see its full details. At this point my workflow has successfully completed, which is why its now marked in green.

Note how we can see an entry for each step in our workflow job. Clicking on a step shows more details.

For example, selecting Build with Gradle gives us the console output showing Gradle starting up.

Once your workflow has completed, click Actions again and you’ll see the workflow run marked as success in green.

Awesome! You just successfully executed your first workflow run using GitHub Actions! 💪

Pretty simple don’t you think? This is the most straightforward setup for building Gradle projects, but we’ll look at some other scenarios later. First though, let’s consider performance.

5. Enable Gradle caching

Let’s trigger another workflow run by committing and pushing a whitespace change. Once the workflow run completes, you can see that the run durations are about the same.

Sound sensible? Well, no actually. Our friends at Gradle HQ have done a lot of work on caching to make subsequent builds quicker than the first.

For example, if we look into the Build with Gradle step for our latest workflow run, we’ll see Gradle downloading the wrapper again.

That’s bad! It should be cached in ~/.gradle/wrapper.

Gradle also caches dependencies, so you don’t need to download them every time. That saves A LOT of time!

This problem is happening because our GitHub Actions runner is created fresh for every workflow run, so Gradle’s local caches are forgotten. The next time the build runs, the cache has to be regenerated.

action/setup-java caching

Fortunately action/setup-java@v2 already has Gradle caching built in and we just need to enable it! It’s literally a one line change to pass the cache: gradle input parameter.

      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
          distribution: 'adopt'
          cache: gradle

Commit and push the above change. The workflow run that executes now will save Gradle’s cache on GitHub’s servers.

Let’s make one more whitespace change to start another workflow run to see the cache being used.

The performance improvements will depend on your specific application, but for gradle-github-actions-example it’s around 15s faster with the cache enabled.

If you look at the Build with Gradle step you’ll see that the wrapper is no longer being downloaded. Less stuff is better!

--no-daemon

The GitHub documentation says Ensure no Gradle daemons are running anymore when your workflow completes. Creating the cache package might fail due to locks being held by Gradle. For this reason, it’s a good idea to run your build with the --no-daemon flag i.e. ./gradlew build --no-daemon.

6. Use gradle-build-action

In the spirit of continuous improvement, Team Gradle have released their own GitHub action action/gradle-build-action. While the approach shown so far works great, this action does offer more functionality that might interest you.

  1. more sophisticated caching
    rather than caching the whole .gradle directory, gradle-build-action picks only the specific subdirectories that need caching. Caching is enabled by default.
  2. capturing a build scan link
    you can run a Gradle build with --scan to access to an in-depth report hosted on the Gradle servers. The gradle-build-action will expose the link in the GitHub UI for you, so you don’t have to go trawling through logs.
  3. running different Gradle versions
    by default gradle-build-action leaves the Gradle version selection to the Gradle wrapper. But you can specify a particular Gradle version you want to build with.

So let’s rewrite our workflow file using action/gradle-build-action@v2.

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
          distribution: 'adopt'
      - uses: gradle/gradle-build-action@v2
        with:
          arguments: build

You have to pass an input parameter of the Gradle task you want to run, in our case build. The action handles calling the wrapper for you.

If you commit and push the change you should see a very similar workflow run duration, showing that everything is working as expected.

build scan link

Just for fun, let’s update our workflow to include --scan on the Gradle command, to see how the action handles it.

      - uses: gradle/gradle-build-action@v2
        with:
          arguments: build --scan

You’ll also need to add this configuration to your Gradle build script to automatically accept the build scan terms of service.

buildScan {
    termsOfServiceUrl = 'https://gradle.com/terms-of-service'
    termsOfServiceAgree = 'yes'
}

Commit and push the change and go into the details of the workflow run. At the bottom it gives the link to the build scan. Nice!

If you haven’t tried build scan before, it gives you a whole load of detailed information you can use to fix slow or failing builds.

An example build scan

If you really want to go to town you can even add the build scan link as a pull request comment. The gradle-build-action docs have an example of this.

7. Which approach to use?

You’ve seen two different approaches to building a Gradle project with GitHub Actions.

  1. running the Gradle command directly, and making use of the setup-java action cache
  2. running the Gradle command via gradle-build-action, which also handles caching

If you want to just use the GitHub provided actions, use the first approach. If you want the functionality provided by gradle-build-action then use the second approach.

If you can’t decide, just pick one. You can easily switch later on. 😁

Example repository

All the examples from this article can be viewed in gradle-github-actions-example repository. Check out the README file to see how it’s all setup.

8. Build your Gradle project when a pull request is made

The simple workflow we’ve used so far only reacts to one event, push. That means the only time your workflow will run is when you push code. Nothing else.

on: [push]

Interestingly, given the configuration we’ve used so far, push will be triggered on any branch you push within your repository.

That means if you create a branch and push it, the workflow will run to ensure the build works on that branch too. Pretty cool!

pull_request event

However, a typical workflow in a GitHub repository involves creating a pull request to merge a feature branch into master. Much better would be to have a workflow run trigger when the pull request is created.

For that, we have the pull_request event. Two things happen when you use it as a trigger for your workflow:

  1. kicks off a workflow run when a PR is opened, reopened, or synchronized (i.e. feature branch is updated)
  2. includes the result of the workflow run in the PR itself

Let’s try it out by adding the pull_request event to our workflow file.

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
          distribution: 'adopt'
          cache: gradle
      - name: Build with Gradle
        run: ./gradlew build

Create a branch, make a minor change, then commit and push it.

git checkout -b pr-test
echo "Minor change" >> README.md
git commit -am "Minor change"
git push --set-upstream origin pr-test

This will immediately kick off a new workflow run, triggered by the push event

Note that the branch name is correctly shown as pr-test.

Now let’s create a new PR to test the pull_request event. Under Pull requests select New pull request. Choose the pr-test branch, then select Create pull request.

You’ll see this box showing that a workflow run is now in progress for this pull request

Once the workflow passes, everything will be marked as green, including the Merge pull request button.

Notice above that the PR shows results from two workflow runs. One triggered by the push event and one by the pull_request event. Fortunately there’s a way to make this a bit cleaner, so we only have the one workflow run showing up.

Filtering by branch

By default events will trigger on any branch. We can tell GitHub Actions to only respond to events on specific branches, like master.

Let’s do that for both push and pull_request with this workflow change, remembering to make the change on the master branch and merge it into any other branches.

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

This means that:

  • any pushes to the master branch will trigger the workflow
  • any pull requests raised to merge into the master branch will trigger the workflow

Now our PR shows only a single successful check. Much cleaner!

Note that the pull_request event triggers even if the pull request is from an external repository, which is ideal for validating potential project contributions.

9. What else can GitHub Actions do for Gradle projects?

What we’ve covered so far is a good starting point for building Gradle projects with GitHub Actions, but of course it can do a lot more. Let’s run through three of the most interesting features.

Add status badge

Ever wondered how those GitHub status badges work, normally shown on the repository README page?

You can add your very own for a specific workflow, by selecting the workflow, then selecting the three dots menu, then choosing Create status badge.

Copy the resulting code and add it to your README file.

Now your repo’s looking a bit more professional! 👨‍💼

Validate the Gradle wrapper

The Gradle wrapper is a script you use to interact with a Gradle project to ensure you’re using the correct Gradle version. The script actually calls a gradle-wrapper.jar file, which sometimes gets updated when you update the Gradle verison.

This is a security vulnerability, since when a PR is created for this kind of change the reviewer cannot see what’s changed within the jar file. That’s because it’s a binary file so GitHub can’t show the differences. The change could contain malicious code to do anything the attacker wants when the wrapper executes. Pretty scary!

Fortunately, Gradle have created the gradle/wrapper-validation-action action to ensure the jar file is legitimate. You can run it immediately after checkout.

      - uses: actions/checkout@v2
      - uses: gradle/wrapper-validation-action@v1

Now when your workflow runs it shows that your Gradle wrapper is valid.

Save build artifacts

You can save any files or directories in your workspace to help diagnose build issues using the actions/upload-artifact action. These files are stored along with your workflow run for easy access.

Let’s setup our project to save the Gradle test report directory, generated by the test task.

name: CI
on:
  push:
    branches: [master]
  pull_request:
    branches: [master]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
          distribution: 'adopt'
          cache: gradle
      - name: Build with Gradle
        run: ./gradlew build
      - name: Archive test report
        uses: actions/upload-artifact@v2
        with:
          name: Test report
          path: build/reports/tests/test

Selecting the individual test run in the UI, you’ll see the artifact at the bottom of the page. You can click on it to download the zip file containing the test reports. Nice!

10. How does GitHub Actions compare to Jenkins

To explore some of the downsides of using GitHub actions to build Gradle projects (yes there are some!), let’s compare it to a CI tool I’ve used extensively, Jenkins.

If you’re not familiar with Jenkins, it’s a self-hosted CI tool that’s highly configurable through various plugins. It also allows you to create a workflow (which it calls a pipeline), although this time written in Groovy.

Let’s compare the two tools across several different aspects.

Maintenance

Since GitHub Actions is a hosted solution, it’s a lot easier to get started. Jenkins requires deployment to your own server, including setting up all the networking, storage, and backups. Throw in setting up Jenkins agents for scalability (the equivalent of GitHub runners), then the complexity really starts to add up.

Note that some businesses may have a requirement not to use hosted services like GitHub to store their code. Unlike other VCM tools like GitLab, GitHub doesn’t have a self-hosted option.

✅ Winner: GitHub

Scalability

With Jenkins you can scale both vertically (add more CPU & memory to the server) and horizontally (add more servers) since you’re in control of everything. The GitHub Actions runner for Ubuntu is fixed to a 2 core CPU and 7GB RAM, and you can’t go beyond that. This could be a limitation if you’re running heavy workloads, such as compiling large codebases.

To address this pitfall, GitHub allow you to run self-hosted runners. These are runners which you manage and pay for, but which hook into the GitHub ecosystem. Setting this up negates some of the maintenance benefits of using the fully hosted solution, but it’s at least no more management than you’d have with Jenkins.

✅ Winner: GitHub & Jenkins

Jenkins scales very well, especially combined with a cloud provider like AWS

Integration

While GitHub is a tool which now includes both VCM and CI, Jenkins is purely a CI tool. You can integrate Jenkins with VCM tools like GitHub, but it will likely require a plugin and additional setup. Also, nothing compares to being able to access everything through a single unified web UI.

✅ Winner: GitHub

Customisation

Jenkins is the daddy of customisation, with plugins to do all sorts of things from integrating with SonarQube code coverage checks to displaying pretty graphs of performance test results. Whilst you can achieve some of these with GitHub Actions, there’s less scope for customisation, especially when it comes to the user interface.

You can argue that this is a good thing for the sake of simplicity, but if you need very rich customisations Jenkins might still be your best option.

✅ Winner: Jenkins

11. Conclusion

GitHub Actions is a powerful GitHub feature to easily add CI to your Gradle projects. It’s relatively new, and will likely get better as GitHub release more core features and new actions become available in the marketplace.

If your code is already in GitHub then there’s a strong argument to start using GitHub Actions to benefit from an integrated solution. If you’re using another CI tool and would consider migrating, then check out these migration guides.

See the gradle-github-actions-example repository for examples of all the features discussed in this article.

Now that you’ve seen what GitHub Actions can do, what will be your preferred tool for building Gradle projects? Let me know in the comments below.

Get going with Gradle course
Gradle icon

Want to learn more about Gradle?
Check out the full selection of Gradle tutorials.

How to build Gradle projects with GitHub Actions

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website