Experimentations on Bazel: github-actions
Tags: bazel, github, githubactions
Published: 2021-03-14 18:05:26

In previous article [Experimentations on Bazel: intro](https://dev.to/davidb31/experimentations-on-bazel-intro-5d48), we created a workspace/project. In this article, we'll setup github-actions to `build` the workspace.

## Minimal config

First step, create the repository on github: https://github.com/new and push the content of the local repository into github by following instruction on the web, something like (name of the remote repository and the default branch depends of your setup):

```shell
git remote add origin git@github.com:davidB/sandbox_bazel.git 
git branch -M development
git push -u origin development
```

Then to enable github-action for CI, you need to define job in the file `.github/worflows/ci.yml` with the following content:

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    # virtual environments: https://github.com/actions/virtual-environments
    runs-on: ubuntu-20.04

    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for
      # the rest of the steps
      - uses: actions/checkout@v2

      # build
      - name: Build the code
        run: bazel build //...
```

`bazel` and `bazelisk` are included into the provided virtual-environments (see [virtual-environments/Ubuntu2004-README.md at main · actions/virtual-environments](https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md)), so no need to install it. The job just need to checkout the code and run bazel.

```shell
git add .github/
git commit -m ":construction_worker: add ci.yml config for github-actions"
git push
```

Check the result on your actions page by example the on https://github.com/davidB/sandbox_bazel/actions this job took 23s.

You can notice in the log that bazel for the version defined into `.bazelversion` is downloaded.

```shell
2021/03/14 15:53:06 Downloading https://releases.bazel.build/4.0.0/release/bazel-4.0.0-linux-x86_64...
Extracting Bazel installation...
Starting local Bazel server and connecting to it...
Loading: 
Loading: 0 packages loaded
Analyzing: 0 targets (1 packages loaded, 0 targets configured)
INFO: Analyzed 0 targets (1 packages loaded, 0 targets configured).
INFO: Found 0 targets...
[0 / 1] [Prepa] BazelWorkspaceStatusAction stable-status.txt
INFO: Elapsed time: 6.708s, Critical Path: 0.02s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
```

## Enable Cache

If you re-run the job, bazel will be downloaded again. You can improve a little the build by using [cache](https://github.com/actions/cache).

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    # virtual environments: https://github.com/actions/virtual-environments
    runs-on: ubuntu-20.04

    steps:
      # Caches and restores the bazelisk download directory.
      - name: Cache bazelisk download
        uses: actions/cache@v2
        env:
          cache-name: bazel-cache
        with:
          path: ~/.cache/bazelisk
          key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.ref }}
          restore-keys: |
            ${{ runner.os }}-${{ env.cache-name }}-development


      # Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for
      # the rest of the steps
      - uses: actions/checkout@v2

      # build
      - name: Build the code
        run: bazel build //...

```

```shell
git add .github/
git commit -m ":construction_worker: enable bazelisk cache on github-action"
git push
```

When you re-run the job (use the button on top-left in the page of CI jobs), no longer "Downloading https://releases.bazel.build/4.0.0/release/bazel-4.0.0-linux-x86_64..." in the output of the job.

The duration of the run increase to 26s, but the re-run now take 23s. Not a big gain (vs no-cache) because managing the cache (pre and post steps) or downloading bazel each time is equivalent. The gain is on the host of bazel archives, less load on its side (better metrics) and download of cache stay inside the github'server infrastructure.

But the using cache becomes more interesting to speed up CI by avoiding rebuild of unchanged packages. To simulate a long build, we'll create a fake long build, I'll explain it in the next article. Add the following code inside `./BUILD.bazel`

```python
genrule(
    name = "hello20s",
    outs = ["hello20s.txt"],
    cmd_bash = "sleep 20 && echo 'hello20s' >$@",
)
```

Then build it twice locally, the first time it will take at least 20s, and less than 1s the second time

```shell
❯ bazel build //...                                                                                                                18:07:00
INFO: Analyzed target //:hello20s (1 packages loaded, 1 target configured).
INFO: Found 1 target...
Target //:hello20s up-to-date:
  bazel-bin/hello20s.txt
INFO: Elapsed time: 20.067s, Critical Path: 20.01s
INFO: 2 processes: 1 internal, 1 linux-sandbox.
INFO: Build completed successfully, 2 total actions

❯ bazel build //...                                                                                                                18:10:45
INFO: Analyzed target //:hello20s (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello20s up-to-date:
  bazel-bin/hello20s.txt
INFO: Elapsed time: 0.049s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
```

Now we'll enable the cache on github-actions an check that re-run will take care of the cache. So update `.github/workflow/ci.yml` with:

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    # virtual environments: https://github.com/actions/virtual-environments
    runs-on: ubuntu-20.04

    steps:
      # Caches and restores the bazelisk download directory, the bazel build directory.
      - name: Cache bazel
        uses: actions/cache@v2
        env:
          cache-name: bazel-cache
        with:
          path: |
            ~/.cache/bazelisk
            ~/.cache/bazel
          key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.ref }}
          restore-keys: |
            ${{ runner.os }}-${{ env.cache-name }}-development

      # Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for
      # the rest of the steps
      - uses: actions/checkout@v2

      # build
      - name: Build the code
        run: bazel build //...
```

```shell
git add BUILD.bazel
git commit -m ":alembic: add target hello20s"
git add .github/
git commit -m ":construction_worker: enable bazel cache on github-action"
git push
```

:tada: first run in 45s, and re-run in 23s, the rule 'hello20s' was not executed the previous built output was used.

## Do not forgot test

Currently the project doesn't include test, so the CI will failed if we run `bazel test \\...` on it, but we can prepare CI to run the future test.

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    # virtual environments: https://github.com/actions/virtual-environments
    runs-on: ubuntu-20.04

    steps:
      # Caches and restores the bazelisk download directory, the bazel build directory.
      - name: Cache bazel
        uses: actions/cache@v2
        env:
          cache-name: bazel-cache
        with:
          path: |
            ~/.cache/bazelisk
            ~/.cache/bazel
          key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.ref }}
          restore-keys: |
            ${{ runner.os }}-${{ env.cache-name }}-development

      # Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for
      # the rest of the steps
      - uses: actions/checkout@v2

      # build
      - name: Build the code
        run: bazel build //...
      # - name: Run the test
      #   run: bazel test //...

```

```shell
git add .github/
git commit -m ":construction_worker: prepare to run test in CI"
git push
```

## share the cache

Currently the cache is setup per git ref, meaning that each branch, PR, tag will have its own cache. But bazel cache has a good handle to check if cached content should be rebuild or not based on inputs like a hash(command + files). So you can also avoid PR and tag to rebuild everything by removing `-${{ github.ref }}` from the cache name.

```yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    # virtual environments: https://github.com/actions/virtual-environments
    runs-on: ubuntu-20.04

    steps:
      # Caches and restores the bazelisk download directory, the bazel build directory.
      - name: Cache bazel
        uses: actions/cache@v2.1.4
        env:
          cache-name: bazel-cache
        with:
          path: |
            ~/.cache/bazelisk
            ~/.cache/bazel
          key: ${{ runner.os }}-${{ env.cache-name }}

      # Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for
      # the rest of the steps
      - uses: actions/checkout@v2

      # build
      - name: Build the code
        run: bazel build //...
      # - name: Run the test
      #   run: bazel test //...
```

```shell
git add .github/
git commit -m ":construction_worker: share the CI cache between gitref"
git push
```

## Keep the github-actions'version uptodate

Setup the dependabot service to create PR on project to keep uptodate the version of the action by adding the file `.github/dependabot.yml`

```yaml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
```

```shell
git add .github/
git commit -m ":construction_worker: setup dependabot to update version of github-actions"
git push
```

## Conclusion

We setuped github-actions to run bazel with cache to speed up result. We also added the first rule in `./BUILD.bazel`, next article will explain the rule and it will start digging into `genrule`.

The sandbox_bazel is hosted on github (not with the same history, due to errors), use tag to have the expected view at end of article: [`article/1_github-actions`](https://github.com/davidB/sandbox_bazel/tree/article/1_github-actions).