This Monorepo plugin will assist you in triggering pipelines, as well as run commands in your CI by watching folders in your monorepo.
Check out this post to learn more about How to set up Continuous Integration for monorepo using Buildkite.
A monorepo is a single, version-controlled code repository that houses multiple independent projects, offering benefits such as flexibility, streamlined management, and reduced tracking of changes and dependencies across multiple repositories.
This approach allows teams to:
- Reduce overhead associated with duplicating code for microservices.
- Easily maintain and monitor the entire codebase.
Check out the example monorepo source code.
If the version number is not provided then the most recent version of the plugin will be used. Do not use version number as master or any branch names.
It defines a list of paths or path to monitor for changes in the monorepo. It checks to see if there is a change to the subfolders specified in the path
A path or a list of paths to be watched, This part specifies which directory should be monitored. It can also be a glob pattern. For example specify path: "**/*.md" to match all markdown files. A list of paths can be provided to trigger the desired pipeline or run command or even do a pipeline upload.
When regex_paths: true is set on the watch block, paths are treated as regular expressions instead of globs.
A path or a list of paths to be ignored, which can be an exact path, or a glob.
This is intended to be used in conjunction with path, and allows omitting specific paths from being matched.
When regex_paths: true is set, skip paths are also treated as regular expressions.
A path or a list of paths to prevent the paths listed to be matched, which can be an exact path, or a glob.
This is intended to be used in conjunction with path, and allows for creating exclusive matches with simpler rules when several files are modified in the same execution.
When regex_paths: true is set, except paths are also treated as regular expressions.
Set to true to treat path, skip_path, and except_path as regular expressions instead of globs. Uses regexp2 which supports full PCRE syntax including lookaheads and lookbehinds.
Regex matching is unanchored: a pattern matches if it occurs anywhere in the file path, not only at the start. For example, path: "src/.*" matches vendor/src/main.go as well as src/main.go. Anchor with ^ (and $ where needed) to match the full path, as in the example above.
This is useful when the paths you want to match would require many glob patterns to express. For example, to match all TypeScript/JavaScript source files under src/ while excluding test files, snapshots, and specific directories:
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "^src/(?!pulumi|ci-generators|desktop|mobile|test)(?!.*\\.test\\.)(?!.*\\.snap$)(?!.*/__test__/)(?!.*/__mocks__/)(?!.*/__snapshots__/).*\\.[tj]sx?"
regex_paths: true
config:
trigger: "frontend-pipeline"Note: When
regex_paths: true, all paths in that watch block must be valid regular expressions. Glob syntax (e.g.**) is not supported in regex mode.
For example, in the following configuration:
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "**/*"
skip_path: "folder/file"
config:
trigger: "pipeline-1"
- path: "**/*"
except_path: "folder/file"
config:
trigger: "pipeline-2"
- path: "folder/file"
config:
trigger: "pipeline-3"If a single execution modified folder/file only pipeline-3 will be triggered. But if any other file is modified as well (thus matching **/*), pipeline-1 will also be triggered, but not pipeline-2.
This is a sub-section that provides configuration for running commands or triggering another pipeline when changes occur in the specified path. Configuration supports 3 different step types.
The plugin validates all step configurations before uploading the pipeline. Invalid steps are automatically skipped with a warning logged to the build output.
A valid step must have:
- A
commandorcommandsfield (for command steps), OR - A
triggerfield (for trigger steps), OR - A
groupfield with either:- An action (
command,commands, ortrigger) directly on the group, OR - Valid nested
steps
- An action (
Invalid configurations that will be skipped:
# ❌ Empty step - no action defined
- path: "app/"
config:
label: "Deploy app" # Only has a label, no command/trigger
# ❌ Empty group - no action and no nested steps
- path: "services/"
config:
group: "Deploy"
# Missing: steps array or actionValid configurations:
# ✅ Valid - has command
- path: "app/"
config:
label: "Deploy app"
command: "echo deploying"
# ✅ Valid - group with nested steps
- path: "services/"
config:
group: "Deploy"
steps:
- command: "deploy.sh"config can also be a list of step configs instead of a single object. Each entry becomes an independent generated step (they are not nested under an implicit group) — the same path/skip_path/except_path matching rules that apply to a single-object config apply equally to every entry in the list.
- path: services/api/
config:
- command: "npm test"
label: "Test API"
- trigger: "api-deploy"
label: "Deploy API"When a file under services/api/ changes, both the npm test command step and the api-deploy trigger step are generated as separate top-level steps.
A step's config can include a build matrix via the matrix attribute. It is passed through untouched to the generated pipeline step.
- path: services/api/
config:
command: "test.sh"
matrix:
setup:
os: ["linux", "windows"]The plugin preserves plugins: blocks when specified in command step configurations. This allows you to use Buildkite plugins within your monorepo-watched steps.
Example
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
watch:
- path: services/api/
config:
command: "npm test"
plugins:
- artifacts#v1.9.4:
upload: "coverage/**/*"
- docker-compose#v5.12.1:
run: api
- path: services/web/
config:
command: "yarn build"
plugins:
- docker#v5.13.0:
image: "node:20"
workdir: /appWhen changes are detected in the watched paths, the plugin generates steps that include the specified plugins. The plugins: blocks are preserved exactly as configured.
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
watch:
- path: app/
config:
trigger: "app-deploy"
- path: test/bin/
config:
command: "echo Make Changes to Bin"
- path: docker/
config:
group: docker/**
steps: # Required: groups must have either 'steps' or an action
- plugins:
- docker#v5.13.0:
build: service
push: service
- command: docker/run-e2e-tests.sh- Changes to the path
app/triggers the pipelineapp-deploy - Changes to the path
test/binwill run the respective configuration command - Changes to any file in the docker folder will run the steps in the group
app/ changes made to app/bin will not trigger the configuration. This is because the subfolder /bin was not specified.
Example
steps:
- label: "Triggering pipelines with plugin"
plugins:
- monorepo-diff#v1.11.2:
watch:
- path: test/.buildkite/
config: # Required [trigger step configuration]
trigger: test-pipeline # Required [trigger pipeline slug]
- path:
- app/
- app/bin/service/
config:
trigger: "data-generator"
label: ":package: Generate data"
build:
meta_data:
release-version: "1.1"- When changes are detected in the path
test/.buildkite/it triggers the pipelinetest-pipeline - If the changes are made to either
app/orapp/bin/service/it triggers the pipelinedata-generator
Conditional Step Execution (if):
The plugin supports conditional execution of pipeline steps using the if key, matching Buildkite’s pipeline conditional syntax. The if key allows you to control when a step runs, based on branch names, environment variables, build metadata, or custom expressions.
Example
steps:
- label: "Triggering pipelines with plugin"
plugins:
- monorepo-diff#v1.11.2:
diff: git diff --name-only HEAD~1
watch:
- path: services/api
config:
trigger: deploy-api
if: build.branch == 'main' || build.branch =~ /^release\//
- path: services/web
config:
command: echo "Deploy Web"
if: build.tag != nullIn the example above,
- The
deploy-apitrigger will only run on the main branch or branches matchingrelease/*. - The
web deploymentcommand will run only if the build has a tag.
if also works at the group step level, controlling whether the entire group runs:
steps:
- label: "Triggering pipelines with plugin"
plugins:
- monorepo-diff#v1.11.2:
diff: git diff --name-only HEAD~1
watch:
- path: services/
config:
group: "Deploy Services"
if: build.branch == 'main'
steps:
- command: deploy-uat.sh
label: Deploy UAT
- command: deploy-prod.sh
label: Deploy ProdThis will run the script provided to determine the folder changes. Depending on your use case, you may want to determine the point where the branch occurs https://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git and perform a diff against the branch point.
Default: git diff --name-only HEAD~1
The diff command must produce newline-delimited output with one file path per line. This is the format that git diff --name-only produces by default. Newline-delimited output is required for filenames containing spaces to be parsed correctly.
Custom diff scripts should follow the same convention — print one path per line to standard output.
README.md
lib/trigger.bash
directory/File Name With Spaces.md
diff: ./diff-against-last-successful-build.sh
#!/bin/bash
set -ueo pipefail
LAST_SUCCESSFUL_BUILD_COMMIT="$(aws s3 cp "${S3_LAST_SUCCESSFUL_BUILD_COMMIT_PATH}" - | head -n 1)"
git diff --name-only "$LAST_SUCCESSFUL_BUILD_COMMIT"diff: ./diff-against-last-built-tag.sh
#!/bin/bash
set -ueo pipefail
LATEST_BUILT_TAG=$(git describe --tags --match foo-service-* --abbrev=0)
git diff --name-only "$LATEST_TAG"Example
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
command: "echo deploy-bar"
- path: "foo-service/"
config:
trigger: "deploy-foo-service"This controls the pipeline interpolation on upload, and defaults to true.
If set to false it adds --no-interpolation to the buildkite pipeline upload,
to avoid trying to interpolate the commit message, which can cause failures.
A default config to run if no paths are matched, the config key is not required, so a default can be written with a config attribute or simple just a command or trigger.
Example
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
command: "echo deploy-bar"
- path: "foo-service/"
config:
trigger: "deploy-foo-service"
- default:
config: ## <-- Optional
command: echo "Hello, world!"The object values provided in this configuration will be appended to env property of all steps or commands.
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "foo-service/"
config:
trigger: "deploy-foo-service"
label: "Triggered deploy"
build:
message: "Deploying foo service"
env:
HELLO: 123
AWS_REGION: ~ # Null literal reads from $AWS_REGIONEnvironment variables can be specified in two formats. Both formats are fully supported.
The map format provides clean, readable syntax:
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
env:
NODE_ENV: production
API_URL: https://api.example.com
PORT: 8080
DEBUG: false
AWS_REGION: ~ # Null literal reads from $AWS_REGION
EMPTY_STRING: "" # Empty string sets to literal ""
watch:
- path: "services/"
config:
command: "npm test"
env:
TEST_ENV: integration
MAX_WORKERS: 4Map format features:
- Clean YAML syntax using key-value pairs
- Supports non-string values (numbers, booleans) which are converted to strings automatically
- Null values read from OS environment: use
KEY: ~(recommended YAML null literal) - The explicit
~ensures nothing is accidentally added during pipeline processing - Note: Unlike array format, you cannot use just
KEYalone - you must use a null value - Empty string (
"") is treated as a literal empty string value - Whitespace in values is preserved
- Recommended for new configurations
The array format uses key=value syntax:
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
env:
- NODE_ENV=production
- API_URL=https://api.example.com
- AWS_REGION # Key-only reads from $AWS_REGION
watch:
- path: "services/"
config:
command: "npm test"
env:
- TEST_ENV=integrationArray format features:
- Key-only entries (e.g.,
AWS_REGION) read from OS environment variables - Supports values with equals signs:
BUILD_ARGS=--arg1=val1 - Whitespace trimmed from keys and values automatically
- Fully supported alongside map format
| Feature | Map Format | Array Format |
|---|---|---|
| Syntax | KEY: value |
KEY=value |
| OS env reading | Null literal (KEY: ~) |
Key-only entries (KEY) |
| Empty string | KEY: "" sets to "" |
KEY= sets to "" |
| Type support | Numbers, booleans | Strings only |
| Whitespace | Preserved in values | Trimmed |
| Readability | High | Medium |
Note: The format is determined by YAML structure - you cannot mix array and map syntax at the same level. However, you can use different formats at different levels (e.g., map format at plugin level, array format at step level).
Add log_level property to set the log level. Supported log levels are debug and info. Defaults to info.
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
log_level: "debug" # defaults to "info"
watch:
- path: "foo-service/"
config:
trigger: "deploy-foo-service"Default: true
By setting download to false, the plugin will use a pre-installed binary instead of downloading it on each run. The binary monorepo-diff-buildkite-plugin must be available in your PATH (typically /usr/bin).
This option is useful for:
- Reducing build time by avoiding repeated downloads
- Improving security by using pre-vetted binaries
- Organizations with policies against runtime binary downloads
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
download: false
diff: "git diff --name-only HEAD~1"
watch:
- path: "foo-service/"
config:
trigger: "deploy-foo-service"The plugin automatically retries binary downloads up to 3 times with a 5-second delay between attempts. This handles transient network issues when downloading from GitHub.
Default: false
Enable SHA256 checksum verification for downloaded binaries to enhance security. When enabled, the plugin verifies checksums against those published in the GitHub release, providing protection against compromised artifacts, network attacks, and binary tampering.
Checksum verification is performed for:
- Newly downloaded binaries (fails and deletes binary on mismatch)
- Cached binaries before reuse (automatically re-downloads on mismatch)
- Pre-installed binaries when
download: false(best-effort, non-blocking)
To enable checksum verification:
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
verify_checksum: true # Recommended for enhanced security
diff: "git diff --name-only HEAD~1"
watch:
- path: "foo-service/"
config:
trigger: "deploy-foo-service"If checksums are unavailable for a release or the SHA256 command is not found on the system, the plugin will warn but continue execution (graceful degradation).
Currently supports a list of commands you wish to execute after the watched pipelines have been triggered
hooks:
- command: upload unit tests reports
- command: echo successDefault: true
By setting wait to true, the build will wait until the triggered pipeline builds are successful before proceeding
Add key to set the step or group key.
steps:
- label: "Setting Key"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
key: echo-step
command: "echo deploy-bar"Add depends_on to declare step or group dependencies. Accepts a single key string or a list of keys.
steps:
- label: "Deploy"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
group: "Deploy Bar"
depends_on: "build-bar"
steps:
- command: "echo deploy-bar"
label: "Deploy Bar"Set allow_dependency_failure: true to allow a step or group to run even if the steps it depends_on have failed.
steps:
- label: "Deploy"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
group: "Deploy Bar"
depends_on: "build-bar"
allow_dependency_failure: true
steps:
- command: "echo deploy-bar"
label: "Deploy Bar"By default, when a watch's path doesn't match any changed file, its step is omitted from the generated pipeline entirely. This can break a depends_on reference: if a downstream step depends on a step that was omitted, the reference never resolves and the build stalls or fails waiting on a step that was never created. The same applies when a watch is excluded via except_path, or every matching file is excluded via skip_path.
Set skip_on_no_changes: true at the plugin level to keep those steps in the pipeline instead of omitting them. Unmatched steps are still emitted, but marked with a skip reason instead. Buildkite creates a step carrying skip as a broken job, and treats a broken job as satisfying any depends_on reference to it — the same way it treats a step skipped by an if condition — so the reference still resolves.
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
skip_on_no_changes: true
watch:
- path: "services/"
config:
group: "CI/CD Infrastructure"
key: "group:cicd"
steps:
- command: "echo deploy"
- path: "app/"
config:
command: "echo build-app"
depends_on: "group:cicd"If services/ doesn't match any changed file, CI/CD Infrastructure is still emitted with skip: "No changes detected" instead of vanishing, and build-app's depends_on: "group:cicd" resolves correctly.
The skip reason reflects why the step was excluded:
No changes detected— the watch'spathdidn't match any changed file.Matched changes were excluded by skip_path— a file matchedpath, but every match was also excluded byskip_path.Excluded by except_path— the watch was excluded entirely viaexcept_path.
Note on groups: the plugin sets skip: on the group container itself in the generated YAML (as in the example above). Buildkite then applies that skip to every job nested inside the group, rather than treating the group as one single skipped unit — so an all-skipped group still renders as a visible group in the pipeline view, just with every job inside it shown as skipped, rather than the group disappearing.
Note on notify: enabling skip_on_no_changes can cause a plugin-level notify to fire on builds where nothing actually matched. Without the flag, a build where no watch matches and there's no default step uploads nothing, so notify never fires. With the flag on, that same build now uploads a pipeline containing only skip: placeholders, which is enough content for the upload to proceed and notify to trigger. If you rely on notify (for example a Slack webhook) to only fire on real activity, keep this in mind before enabling the flag.
Add notify to send notifications when a group step completes. Accepts the same notification types as Buildkite step-level notify.
steps:
- label: "Deploy"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "bar-service/"
config:
group: "Deploy Bar"
notify:
- slack: "#deployments"
if: "build.state == 'passed'"
steps:
- command: "echo deploy-bar"
label: "Deploy Bar"Add secrets to inject Buildkite Secrets into your command steps. Secrets can be specified in two formats:
Array format - secret names are used as environment variable names:
steps:
- label: "Deploy with secrets"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "service/"
config:
command: "deploy.sh"
secrets:
- API_ACCESS_TOKEN
- DATABASE_PASSWORDMap format - specify custom environment variable names:
steps:
- label: "Deploy with secrets"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "service/"
config:
command: "deploy.sh"
secrets:
MY_API_KEY: api_access_token_secret
DB_PASS: database_password_secretSecrets also work within grouped steps:
steps:
- label: "Deploy services"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only HEAD~1"
watch:
- path: "services/"
config:
group: "Deploy"
steps:
- command: "deploy-uat.sh"
label: "Deploy UAT"
secrets:
- UAT_DB_HOST
- command: "deploy-prod.sh"
label: "Deploy Prod"
secrets:
DB_HOST: prod_db_host_secretsteps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
diff: "git diff --name-only $(head -n 1 last_successful_build)"
interpolation: false
env:
env1: env-1 # this will be appended to all env configuration
hooks:
- command: "echo $(git rev-parse HEAD) > last_successful_build"
watch:
- path:
- "ops/terraform/"
- "ops/templates/terraform/"
config:
command: "buildkite-agent pipeline upload ops/.buildkite/pipeline.yml"
label: "Upload pipeline"
key: pipeline-upload
# following configs are available in command. notify is not available in trigger step
notify:
- basecamp_campfire: https://basecamp-url
- github_commit_status:
context: my-custom-status
- slack: "@someuser"
if: build.state === "passed"
# soft_fail: true
soft_fail:
- exit_status: 1
- exit_status: "255"
retry:
automatic:
- limit: 2
exit_status: -1
agents:
queue: performance
artifacts:
- "logs/*"
env:
FOO: bar
wait: trueNote: This plugin accepts both artifact_paths and artifacts field names for backward compatibility:
# Preferred - use "artifact_paths":
- path: "app/"
config:
command: "npm test"
artifact_paths:
- "logs/**/*"
# Also supported - "artifacts":
- path: "app/"
config:
command: "npm test"
artifacts:
- "logs/**/*"Both field names are supported by Buildkite. The generated pipeline YAML will use artifact_paths.
Default: BUILDKITE_PLUGINS_PATH
This is the filesystem folder where the Go binary will be kept.
steps:
- label: "Triggering pipelines"
plugins:
- monorepo-diff#v1.11.2:
binary_folder: "/var/buildkite-agent"
watch:
- path: "bar-service/"
config:
key: echo-step
command: "echo deploy-bar"If you see warnings like Skipping invalid step: empty step configuration, check that your step configuration includes:
- For command steps:
commandorcommandsfield - For trigger steps:
triggerfield - For group steps:
groupfield with eitherstepsarray or an action
Common issues:
- Forgetting to add
command:ortrigger:inside theconfigblock - Creating empty groups without nested steps
- Using only metadata fields like
label,key, orenvwithout an action
Example of fixing an invalid configuration:
# ❌ Invalid - missing action
- path: "app/"
config:
label: "Deploy app"
env:
- ENV=production
# ✅ Fixed - added command
- path: "app/"
config:
label: "Deploy app"
command: "deploy.sh"
env:
- ENV=production| Elastic Stack | Agent Stack K8s | Hosted (Mac) | Hosted (Linux) | Notes |
|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | N/A |
- ✅ Fully supported (all combinations of attributes have been tested to pass)
Thanks to @chronotc and @adikari for authoring the original Buildkite Monorepo Plugin.
MIT (see LICENSE)
Please read contributing guide.