When using Github Actions, Github supports skipping workflow runs by adding [no ci] to your Git commits.

Conversely, to help reduce your Github bill, this post shares a way to make workflow jobs opt-in by adding [ci] to your Git commit.

Github Actions can get expensive

Github Actions are awesome and allow automating software workflows to streamline development. At Hub3, we use Github Actions to automate the below jobs for our workflow named ci (short for continuous integration):

  1. lint: check the app’s code for consistent style and best practices
    • e.g. yarn lint
  2. test: run automated tests to verify that the app’s working correctly
    • e.g. bin/rspec
  3. deploy: push the app’s code to the live production server

By default (when using on: push), the lint and test jobs are automatically run for each new Git commit. This is uber convenient and allows our developers to receive fast feedback about their code’s style and functionality. Here’s what that looks like at the bottom of a pull request:

All jobs are passing. 🙂

Sadly, Github Actions are not free for private repositories, and the cost is based on how long your jobs take.

As your project grows, your jobs will likely take longer due to more code to lint, more tests to run, etc. This trend compounds as you add developers to the project.

Eventually you might ask: How can we use Github Actions more efficiently and reduce our Github bill? Make jobs opt-in instead?

Conditionally running Github Actions

Well, I asked that question!

When working on a large feature, it’s rare to want to run Github Actions for EVERY commit. For instance, some commits act as checkpoints, others as quick fixes, etc. In those cases, it’s often premature to run the test and lint jobs because the code is in a WIP state.

To run Github Actions only when we want to, we add the [ci] flag to our Git commits. For example, a pull request’s WIP commits might look like this:

* 2015073 - add payment migrations
* a778af1 - add payment controllers
* beecca1 - add payment model and controller specs [ci]
* 3c46eca - add simple payment frontend
* 216f3c1 - ui/ux improvements
* 78165cd - add feature specs [ci]
* a7a5f37 - address feedback
* e0fec97 - address more feedback
* 1b7442e - fix linting issues [ci]

In that example, we only run our ci workflow 3 times instead of 9, saving 66% on Github Actions costs for that pull request.

To add support for the [ci] flag, we added the if: statement under each of our workflow’s jobs:

# .github/workflows/ci.yml

lint:
  if: "contains(toJSON(github.event.commits.*.message), '[ci]') || github.ref == 'refs/heads/main'"
  ...

test:
  if: "contains(toJSON(github.event.commits.*.message), '[ci]') || github.ref == 'refs/heads/main'"
  ...

That’s it! Feel free to rename the [ci] flag to whatever you want. Also, notice that the jobs will always run if the branch is main, which is usually what you want.

Alternatively, you could introduce [ci-test] and [ci-lint] flags for more granular control over when jobs run. At Hub3, given the speed of our lint job and the overhead of two flags to remember, we didn’t.

If you find a simpler way to accomplish opt-in Github Actions, shoot me a note.