Stacking and CI
Common optimizations and best practices for your CI and stacking

Before an organization begins stacking, two questions typically arise:

  1. Do I need to update my CI configuration first?

  2. How is stacking going to impact my CI bill?

The first bit of good news is, the following Graphite features all work with your CI configuration out of the box:

  • Stacking

  • Merge all

  • Merge when ready

  • The Graphite Merge Queue

The second bit of good news is stacking itself does not change your CI bill. Organizations with fewer than 10 stackers are unlikely to see any difference.

However, stacking does make your developers more productive: leading them to both make more changes and break up large changes into more PRs; which ultimately means an increased number of CI runs.


Note

Organizations that are still early in stacking are unlikely to need to do any of this.


One of the levers for optimizing CI on stacks is to decide whether you want to run CI on upstack PRs (PRs with a dependency). These PRs may be updated as their dependencies change. Because of that, some organizations wait until all dependencies are merged before running CI.

Configuring upstack CI is dependent on your specific CI provider. For GitHub actions, this is determined by setting the branch target filter for pull request actions:

yaml
on:
pull_request:
types: [
# Default pull_request trigger types.
opened,
synchronize,
reopened,
]
# This clause will prevent upstack PRs from running CI, because
# upstack branches aren't directly based on main.
#
# To run CI on upstack branches, remove this clause.
branches: [
main
]

Choosing whether to run CI upstack is based on the priorities of the organization. Not running CI upstack can save money. However, running CI upstack allows developers to respond to errors more quickly, and can also speed up your engineering organization.

Some organizations believe that this should be left to a developer. You can support this preference by defaulting to not running CI on upstack branches, unless the developer applies a specific label.

In GitHub actions, this looks like:

yaml
jobs:
test:
runs-on: ...
if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, '<label_name>')
steps:
- ...

If your organization has branch protection rules turned on, and you are not running CI on upstack PRs, you may see upstack PR as “missing required CI”. This is because the CI job that is required has not yet run on that PR (because it’s waiting for dependencies to be merged).

If you want to run CI upstack, but also want to save on CI cost, there are some general CI optimizations to make running CI cheaper.

Google recommends breaking up your tests from one CI job into many which run at different points. One recommended split is:

  • CI that runs on all PRs

  • CI that runs on PRs, excluding upstack

  • CI that runs after PRs merge to main

In GitHub actions, and other providers, you can do this by creating multiple workflows and setting different triggers for them.

If your organization doesn’t already have one set up, a dependency management tool (such as Bazel or Buck) can be really helpful. These tools look at what code changed, and determine which tests need to run as a result. This prevents unnecessary CI runs with stacking by skipping any tests that were unaffected.

In a similar vein, workflow orchestration tools with caching can create a similar effect. For example, Turborepo caches CI results based on the hash of the project’s files. Unlike Bazel and Buck, all tests will still run, but if the input files were unchanged across a stack, the test will hit the cache and make the cost negligible.

Lastly, the Graphite Merge Queue can help you save on CI cost when merging. When merging a stack, the Merge Queue runs CI once on every PR, and if all pass, they all get merged at once.

This is helpful for organizations that merge a lot of stacks.