Determines which build targets need rebuilding after a code change.
Given a base ref and a head ref, should-build walks the changes in between,
projects them through a declarative config (should-build.yaml) and a
language-specific dependency-graph analyzer, and answers a single question per
target: rebuild, or skip.
The easiest way to use should-build in CI is the composite action hosted in
this repo. It downloads a prebuilt binary from the matching GitHub release (with
SHA-256 checksum verification), falling back to building from source if the
binary isn't available for the runner's platform. Outputs are matrix-friendly.
Note: The checkout step must use
fetch-depth: 0becauseshould-buildrunsgit diffbetween the base and head commits — a shallow clone won't have the base commit.
jobs:
changes:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.sb.outputs.targets }}
any: ${{ steps.sb.outputs.any }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v4
id: shas
- uses: prassoai/should-build@v0
id: sb
with:
base: ${{ steps.shas.outputs.base }}
head: ${{ steps.shas.outputs.head }}
# config: should-build.yaml (default)
build:
needs: changes
if: needs.changes.outputs.any == 'true'
strategy:
matrix:
target: ${{ fromJSON(needs.changes.outputs.targets) }}
runs-on: ubuntu-latest
steps:
- run: echo "Building ${{ matrix.target }}"The if: ... any == 'true' guard matters: when no targets need rebuilding,
targets is [] and fromJSON produces a zero-iteration matrix, which
GitHub treats as a workflow error unless the job is skipped.
| Input | Required | Default | Description |
|---|---|---|---|
base |
yes | Base commit SHA | |
head |
yes | Head commit SHA | |
config |
no | should-build.yaml |
Path to config file, relative to repo root |
only |
no | Comma-separated target names to evaluate, no whitespace (empty = all) | |
repo |
no | . |
Repository root path |
verbose |
no | false |
Include per-file match rules in JSON output |
| Output | Description |
|---|---|
targets |
JSON array of target names that need rebuilding, e.g. ["api","web"] |
any |
"true" if any target needs rebuilding, "false" otherwise |
json |
Full JSON output from should-build |
If you currently invoke should-build via a docker image, replace the
docker run step with the composite action:
# Before (docker):
- name: Determine targets
run: |
json=$(docker run --rm -v .:/workspace \
ghcr.io/your-org/should-build:latest \
--base $NX_BASE --head $NX_HEAD --repo /workspace)
echo "matrix=$(echo "$json" | jq -cr '[.[] | split("/")[-1]]')" >> "$GITHUB_OUTPUT"
# After (composite action):
- uses: prassoai/should-build@v0
id: sb
with:
base: ${{ env.NX_BASE }}
head: ${{ env.NX_HEAD }}
# only: api,web (optional — omit to evaluate all targets in config)The action handles JSON parsing internally — outputs.targets is already the
array of names that need rebuilding.
go install github.com/prassoai/should-build/cmd/should-build@latest
should-build [flags] <base-ref> <head-ref>
Flags:
| Flag | Description |
|---|---|
--config <path> |
Path to config file (default: should-build.yaml, relative to --repo) |
--target <name> |
Evaluate only this target (repeatable) |
--json |
Output JSON |
--quiet |
Exit 0 = nothing to rebuild, exit 1 = rebuild needed. No stdout. |
--verbose |
Show per-file match rules |
--repo <path> |
Repository root (default: .) |
--version |
Print version and exit |
Examples:
# PR CI: which targets changed between base and head?
should-build $BASE_SHA $HEAD_SHA
# Human-readable table
should-build main HEAD
# JSON for scripting
should-build --json main HEAD
# CI gate: does this target need a rebuild?
if ! should-build --quiet --target api main HEAD; then
echo "api needs rebuilding"
fiCreate should-build.yaml at your repo root:
global:
ignore:
- ".github/**"
- "docs/**"
- "**/*.md"
trigger_all:
- "go.mod"
- "go.sum"
unknown_file: trigger_all # or "ignore"
targets:
api:
path: ./cmd/api # Go dep-graph root
include:
- "k8s/api.yaml"
exclude: []
web:
lang: none # no dep graph; patterns only
include:
- "web/**"A target can declare triggers: — a list of other targets that must also
build whenever it builds. Use this when deploying target A requires target B
to exist at the same version (e.g. a control plane and its matching VM image):
targets:
murmur-control:
path: ./cmd/murmur-control
triggers:
- murmur-vm
- murmur
murmur-vm:
path: ./cmd/murmur-vm
murmur:
path: ./cmd/murmurTriggers propagate transitively: if A triggers B and B triggers C, building A
also builds B and C. Cycles are a configuration error — should-build rejects
them at parse time. Targets that already build from their own rules are not
given a redundant triggered-by entry.
In the JSON output, triggered targets appear with reason "triggered-by" and
rule set to the source target name:
{
"target": "murmur-vm",
"build": true,
"files": [{ "reason": "triggered-by", "rule": "murmur-control" }]
}Evaluation precedence (per file, per target):
global.ignore— file invisible to all targetstarget.exclude— target opts outtarget.include— triggers target- Dependency graph — file is in a package the target imports
global.trigger_all— triggers all non-excluded targetsunknown_file— fallback policy for orphan files onlytarget.triggers— after per-target evaluation, propagate builds transitively
A file is an orphan only if no target accounts for it — it matches no
target's include, is in no target's dependency graph, and matches no
global.trigger_all pattern. The unknown_file: trigger_all safety net fires
for orphans alone. A file that some target depends on is not an orphan, so it
rebuilds only the targets that actually reference it (plus their triggers) —
a change scoped to one service's files does not rebuild every target.
Glob syntax. Patterns use doublestar
globs, not gitignore semantics. * matches within a single path segment;
use ** to cross directory boundaries. *.md matches README.md but NOT
docs/README.md — write **/*.md for that.
The {target} template variable in include/exclude patterns expands to
the target's key name: targets/{target}/conf/*.yaml becomes
targets/api/conf/*.yaml for the api target.
Apache-2.0.