In a modern, well-maintained repository, each pull request is automatically processed by a set of workflows triggered on every commit. This practice is called Continuous Integration (CI) and is one of the most fundamental best practices in software engineering.
Such workflows can run unit tests, static code analysis, various linters, end-to-end tests, integration tests, spin up the dedicated test environment, etc. This type of automation improves the confidence of the change implemented, verifying whether it is safe to integrate and release.
Each workflow run consumes “minutes,” as much as it takes to complete the task. It is not rare to use 20, 30, or more minutes collectively on every push to the repo.
In the GitHub Free plan, we get 2,000 minutes for free. GitHub Pro allows us to use only 1,000 minutes more (source). A simple calculation shows that you’ll “eat up” all available minutes after 100 commits are pushed to the repo in a given month – which, in projects involving multiple engineers, might be consumed within just a few days. While you can purchase additional minutes to avoid blocking your team of engineers, this is an extra bill to pay.
How can we optimize the “minutes usage” in GitHub Actions?
As you may already guessed from the title, the idea is to run the workflows only on non-draft pull requests. Draft PRs are often a “work in progress” – early explorations, not-necessarily-following-best-practices draft code that tests the approach and proves the assumptions. In most cases, running all the automated workflows for such PRs is unnecessary.
Draft PRs can not be merged, so there’s no risk that untested changes will get merged into the target branch. Once PR is marked as “ready for review” and becomes non-draft, all the workflow automation should run at that time and for all following pushes.
An engineer should create the new Pull Request as early as possible but keep it as a draft until all the code changes are fully implemented and ready for review. Then, based on the CI workflow results, the code should be improved/updated/adjusted to match the integration requirements.
Implementation
Due to various issues with how GitHub Actions interprets the if
statements defined in a workflow (see this discussion), I decided to implement a custom job that runs on every commit and checks the PR status. It outputs the is-eligible
variable, which the following jobs can use to determine whether they can run.
Our workflow must run on following pull_request
workflow triggers:
opened
– when a new PR is opened,reopened
– when previously closed PR is reopened,synchronize
– when there’s a push with commit(s) to the branch used by this PR,ready_for_review
– when existing draft PR is marked as “ready for review” – this is when the automation will run for the first time. Thanks to this trigger, we won’t need to push to the repo to start the automation,converted_to_draft
– when existing PR is converted back to draft.
Following the docs, workflow only runs when a pull_request
event’s activity type is opened
, synchronize
, or reopened
. In our implementation we added two more types (ready_for_review
and converted_to_draft
) to ensure our pipelines are triggered when these important actions occur.
The get-draft-prs
step uses GitHub CLI to get the JSON-formatted list of draft PR numbers in a given repository. The eligibility
step processes this JSON, verifying whether the current PR is in that list – if it is, that means it’s a draft PR.
And that’s how the result looks like for a draft PR:
Thanks to this small change, the “minutes usage” in my Teydea Studio project dropped by ~70%.