Surfacing Gradle Build Scans Within GitHub Actions Workflows

This post demonstrates several approaches for surfacing Gradle build scans within GitHub Actions workflows; thereby making them more discoverable and hopefully more useful to a team.

Recently, our Android team at Premise Data has worked with the Gradle Enterprise team to optimize our Gradle builds.  Gradle build scans have been a key tool in this work.  Build scans have helped optimize our build, and have helped detect regressions and other build trends for both CI, and local developers’, builds.

Because of the value we’ve seen in regularly reviewing these build scans, I wanted a way to make these scans easier to find, and to use, for everyone on our team.

Thanks to Tony Robalik for pointing me in the direction of adding build scan urls as PR comments.

Working With Gradle Build Scans?

Before we dive in, let’s quickly review Gradle build scans; what are they, and how to enable them for your project?

Find the Code

If you want to check out these full examples, you can find them on GitHub.

What are Gradle build scans?

Gradle build scans provide a detailed overview of your Gradle build.  These details include a myriad of information including tasks executed, build cache performance, test results, environment details, and much more.

What does a build scan look like?

Build scans are presented as interactive webpages hosted at either `scans.gradle.com` or within your own Gradle Enterprise instance.

Sample build scan

When scans are generated for a build, a link to the build scan will be included at the end of the console build output.

How to generate a Gradle build scan?

Generating a Gradle build scan for a single Gradle task execution is simple.  We only need to add the `–scan` option to our task invocation.

./gradlew assembleDebug --scan

At the end of this execution, we see the following in our console window:

Publishing build scan...
https://gradle.com/s/<scan id here>

It’s this url that I wanted to surface to our team.

How to enable build scans for your Android project?

If your team is leveraging Gradle Enterprise, or you just want to generate build scans for all builds, you can integrate the Gradle Enterprise plugin into your project.

First, you’ll want to add the Gradle Enterprise plugin to your `settings.gradle` file. 

plugins {
  id "com.gradle.enterprise" version "3.4.1"
}

Next, you can configure build scan behavior in your `settings.gradle` file.

gradleEnterprise {
  buildScan {
    termsOfServiceUrl = "https://gradle.com/terms-of-service"
    termsOfServiceAgree = "yes"
    publishAlways()
  }
}

With this setup in place, a build scan will be generated for each build of your project. We took the time to review this setup because we’ll make use of this later on.

Surfacing Gradle Build Scan URLs as Pull Request Comments

Now that we’re familiar with Gradle build scans, let’s return to the idea of surfacing these build scans in our GitHub Actions workflows.

We’ve now seen that urls for these build scans are logged at the end of our Gradle task execution. 

If running on a continuous integration (CI) server, this often means having to scroll, and parse, through hundreds or even thousands of lines of build output to find this url.  And that’s only useful if developers even know the url exists in the first place.

One way to help with this problem is to add these urls as comments on an open PR. In this section, we’re going to examine several variations of this idea. In each variant, we’re going to make use of the following GitHub Actions:

These actions will do most of the heavy lifting for us.

Adding a Single Job’s Gradle Build Scan URL as a Pull Request Comment

Code Sample Here

Let’s first imagine we have a GitHub Actions workflow running a single job. That job is configured to assemble our app.

We can use the gradle-command-action action to run our desired Gradle task.

- id: gradle
  uses: eskatos/gradle-command-action@v1
  with:
    arguments: assembleDebug

This action will automatically parse our build output, find the build scan url, and add it as an output that can be accessed from other steps in the job.

steps.gradle.outputs.build-scan-url

Notice the structure of this output. steps.gradle is referencing the job build step with id gradle. Then, outputs.build-scan-url is referencing a specific output for that build step.

Now that we have access to the build scan url, we can use the add-pr-comment action in add the url as a pull request comment.

- name: Add Build Scan URLs to Pull Request
  uses: mshick/add-pr-comment@v1
  with:
    repo-token: ${{ secrets.GITHUB_TOKEN }}
    message: Buildscan url ${{ steps.gradle.outputs.build-scan-url }}

We could go one step further, and customize the PR comment a bit more.

In this example, we’ll add the workflow run id for this specific build scan so we can more closely tie the url to the workflow.

- name: Add Build Scan URLs to Pull Request
  uses: mshick/add-pr-comment@v1
  with:
    repo-token: ${{ secrets.GITHUB_TOKEN }}
    message: |
      **Buildscan url for run [${{ github.run_id }}](https://github.com/n8ebel/GitHubActionsAutomationSandbox/actions/runs/${{ github.run_id }})**
      ${{ steps.gradle.outputs.build-scan-url }}

Once this workflow is run, we’ll now get PR comments for each successful run of our workflow.

Example pull request comment including a Gradle build scan url

Adding Build Scan URL Comments for Multiple Jobs

Code Sample Here

In many cases, our workflows will have more than one job. If we’re running Gradle tasks for each job, this will result in multiple build scans.

So how can we surface each build scan for the workflow?

The easiest approach, is to extend our previous example and add PR comments for each job.

jobs:
  test_job:
    name: Test
    runs-on: ubuntu-latest
    steps:
      ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        with:
          arguments: test
      - name: Add Build Scan URL to Pull Request
        uses: mshick/add-pr-comment@v1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          message: |
            **Test job buildscan url for run [${{ github.run_id }}](https://github.com/n8ebel/GitHubActionsAutomationSandbox/actions/runs/${{ github.run_id }})**
            ${{ steps.gradle.outputs.build-scan-url }}
  assemble:
    name: Assemble Debug
    runs-on: ubuntu-latest
    steps:
      ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        with:
          arguments: assembleDebug
      - name: Add Build Scan URL to Pull Request
        uses: mshick/add-pr-comment@v1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          message: |
            **Assemble job buildscan url for run [${{ github.run_id }}](https://github.com/n8ebel/GitHubActionsAutomationSandbox/actions/runs/${{ github.run_id }})**
            ${{ steps.gradle.outputs.build-scan-url }}

Here, we’ve added a comment for each of the jobs run. In this case, we’ve customized the comment text to indicate which job the build scan is associated with; Test or Assemble.

The resulting PR looks something like this:

Multiple build scan PR comments; one for each job in the workflow

This approach succeeds in surfacing the build scan urls. However, it also results in a lot of PR noise as each workflow run may result in multiple PR comments.

Consolidating Multiple Job’s Build Scan URLs Into a Single Pull Request Comment

Code Sample Here

To improve upon the previous example, we’ll consolidate all the build scan urls for a workflow into a single PR comment. That way, we’ll have a 1:1 mapping between a workflow run and build scan PR comments.

The general idea of this approach is this:

  • Expose each build scan url as a job output
  • Delay adding PR comments until all relevant jobs have completed
  • Add a new job that waits for relevant jobs, and adds a single PR comment including all the build scan urls
jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    outputs:
      buildScanUrl: ${{ steps.gradle.outputs.build-scan-url }}
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        with:
          arguments: test
  assemble:
    name: Assemble Debug
    runs-on: ubuntu-latest
    outputs:
      buildScanUrl: ${{ steps.gradle.outputs.build-scan-url }}
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        with:
          arguments: assembleDebug
  notification_job:
    needs: [test, assemble]
    name: Add Build Scan URLs
    runs-on: ubuntu-latest
    steps:
      - uses: mshick/add-pr-comment@v1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          message: |
            **Build scans for run: [${{ github.run_id }}](https://github.com/premisedata/mobile-android/actions/runs/${{ github.run_id }})**
            * Test Build Scan ${{ needs.test.outputs.buildScanUrl }}
            * Assemble Build Scan ${{ needs.assemble.outputs.buildScanUrl }}

With all build scan urls consolidated into a single comment, our PR output might look something like this:

A single pull request comment that contains multiple Gradle build scan urls
A single build scan PR comment for all jobs in the workflow

Now, we will see a single comment for each workflow run.

If any job fails for a workflow run, we can find the associated build scan url for that job, and start investigating the issue.

Storing Gradle Build Scan URLs In GitHub Actions Workflow Artifacts

Surfacing build scan urls as PR comments might be a useful improvement to your pull request workflows.

However, not all workflows have an associated pull request. You may have a nightly build. Or a scheduled release build. Or any number of other automated workflows that don’t have an associated pull request.

If something goes wrong for those builds, we still want to quickly find the associated build scan and start diagnosing the problem.

How then can we improve discoverability of build scans for non-pull-request workflows?

One solution is to write the build scan urls to a file stored as a build artifact.

Within our GitHub Actions workflows, we can store any number of different build artifacts. These could be things like testing or linting reports, or maybe your .apk file.

Storing artifacts is quite straightforward using the upload-artifact action.

- uses: actions/upload-artifact@v2
  with:
    name: some-artifact
    path: path/to/artifact/some_artifact.txt

This sample will store the file some_artifact.txt in a workflow artifact named some-artifact.

We will use this approach to save a set of files that contain build scan urls for workflows that don’t include a PR.

Writing Gradle Build Scan URLs to a File

To write our build scan URL to a file, we can make use of the buildScanPublished {} callback of the Gradle Enterprise plugin.

We see in this example that we’ve added the buildScanPublished block, which is called with the current PublishedBuildScan object, once the scan is published.

gradleEnterprise {
    buildScan {
        termsOfServiceUrl = "https://gradle.com/terms-of-service"
        termsOfServiceAgree = "yes"
        publishAlways()
        buildScanPublished { PublishedBuildScan scan ->
            file("buildscan.log") << "${new Date()} - ${scan.buildScanUri}\n"
        }
    }
}

Once we have the scan, we can use it however we want.

In this case, we’re creating a new file named buildscan.log and writing the build scan uri to the file, along with the current timestamp.

Locally, as multiple build scans are generated, this file will continue to populate that list.

On CI, where builds are run in a clean environment, this file will include only the most recent build scan.

Storing Build Scan Files as Workflow Artifacts

Code Sample Here

Now that our builds will generate these build scan files, we need to store them as a workflow artifact so we can go back and view them for any arbitrary workflow.

For each job, we’ll add a build step that stores the buildscan.log file as a build artifact.

jobs:
  test_job:
    name: Test
    runs-on: ubuntu-latest
    steps:
     ...
      - name: Save Build Scan Log
        uses: actions/upload-artifact@v2
        with:
          name: buildscan-test
          path: 'buildscan.log'
  assemble_job:
    name: Assemble
    runs-on: ubuntu-latest
    steps:
     ...
      - name: Save Build Scan Log
        uses: actions/upload-artifact@v2
        with:
          name: buildscan-assemble
          path: 'buildscan.log'

To avoid overwriting the artifact, each job writes the buildscan.log file to a different artifact.

The result is something like this:

Multiple GitHub Actions workflow artifacts that each contain a Gradle build scan url
Multiple workflow artifacts storing build scan files

This allows us to find the build scan urls for a non-PR workflow without browsing the build logs.

Unfortuneately, this approach requires us to download multiple artifacts to view all of the workflow’s build scan urls.

So for one final improvement, we’ll look at how to consolidate all the buildscan.log files into a single file artifact.

Storing Multiple Build Scan Files as a Single Workflow Artifact

Code Sample Here

To store our build scan files in a single artifact, we need to be able to customize the file name of the buildscan.log files.

There’s probably many different ways to go about this, and I’ll cover just one here.

We’ll add an environment variable to the Gradle task build step. This variable will be used as a filename prefix for our buildscan.log file.

jobs:
  test_job:
    name: Test
    runs-on: ubuntu-latest
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        env:
          BUILDSCAN_FILE_PREFIX: test
        with:
          arguments: test
      ...
  assemble_job:
    name: Assemble
    runs-on: ubuntu-latest
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        env:
          BUILDSCAN_FILE_PREFIX: assemble-debug
        with:
          arguments: assembleDebug
      ...

Now, we can update our buildScanPublished{} configuration to use the prefix when naming the buildscan.log file.

buildScanPublished { PublishedBuildScan scan ->
        def scanNamePrefix = System.getenv("BUILDSCAN_FILE_PREFIX") ? "${System.getenv("BUILDSCAN_FILE_PREFIX")}-" : ""
        def filename =  scanNamePrefix + "buildscan.log"
        file(filename) << "${new Date()} - ${scan.buildScanUri}\n"
}

Now, each job’s buildscan.log file should have a unique name corresponding to the environment variable adding to the Gradle execution build step.

So our last step, is to update the workflow so that each job uploads the unique buildscan.log files to the same artifact.

jobs:
  test_job:
    name: Test
    runs-on: ubuntu-latest
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        env:
          BUILDSCAN_FILE_PREFIX: test
        with:
          arguments: test
      - name: Save Build Scan Log
        if: ${{ always() }}
        uses: actions/upload-artifact@v2
        with:
          name: buildscans
          path: 'test-buildscan.log'
  assemble_job:
    name: Assemble
    runs-on: ubuntu-latest
    steps:
     ...
      - id: gradle
        uses: eskatos/gradle-command-action@v1
        env:
          BUILDSCAN_FILE_PREFIX: assemble-debug
        with:
          arguments: assembleDebug
      - name: Save Build Scan Log
        if: ${{ always() }}
        uses: actions/upload-artifact@v2
        with:
          name: buildscans
          path: 'assemble-debug-buildscan.log'

Now, when our workflow is completed, we’ll have a single artifact called buildscans.

A single GitHub Actions workflow artifact that contains multiple build scan urls
A single workflow artifact for all build scan files

Once downloaded, we have access to each buildscan.log file for the workflow.

Build scan files stored within a GitHub Actions workflow artifact
The single workflow artifact will contain all the buildscan.log files once unzipped

Now, for any workflow that’s run, we can download the buildscans artifact and immediately find the build scan urls for that workflow.

Wrapping Up

In this post, we’ve explored several different ways of making Gradle build scan urls easier to find within our GitHub Actions workflows.

The effectiveness of these approaches likely depends upon your team. If your team is using Gradle Enterprise and everyone is comfortable using that tool to find build scans, then surfacing them in workflows may not be that helpful.

However, if not everyone on your team is familiar with Gradle Enterprise, or you’re not using Gradle Enterprise, then surfacing the build scan urls in a more visible location may in fact help your team quite a bit.

Find the Code

If you want to check out these full examples, you can find them on GitHub.