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.
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.
-
Mr. Developer pushes the code which triggers an event in GitHub.
push
is one of many events. -
push
triggers a workflow, which is your CI pipeline defined in a YAML file in your repository. -
The workflow has a job which runs in its own virtual machine, called a GitHub Action runner.
-
In the runner the job can access a workspace directory, where you check out the source code.
-
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
andactions/setup-java
, which we’ll use later. -
command steps are instructions like
./gradlew build
or anything supported by the operating system.
-
-
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. -
The workflow succeeds or fails. This time it fails, automatically emailing our developer. His enthusiasm quickly disappears. ๐
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
-
you need a GitHub account
-
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 casepush
. 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 actionactions/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 aname
for the action which helps identify what it’s doing in the GitHub UI. Usingwith
we can pass input parameters to the action, which in this case is required to specifyjava-version
anddistribution
. -
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!
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.
-
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. -
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. -
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.
-
running the Gradle command directly, and making use of the setup-java action cache
-
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. ๐
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:
-
kicks off a workflow run when a PR is opened, reopened, or synchronized (i.e. feature branch is updated)
-
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 version.
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.
Stop reading Gradle articles like these
This article helps you fix a specific problem, but it doesn't teach you the Gradle fundamentals you need to actually help your team succeed.
Instead, follow a step-by-step process that makes getting started with Gradle easy.