Configuration Reference
This document covers all operators and options available in .common-repo.yaml.
- Configuration File
- Core Operators
- Structural Operators
- Merge Operators
- Operation Order
- Complete Example
Configuration File
The .common-repo.yaml file is a list of operations executed in order. Each operation is a YAML map with a single key indicating the operator type.
Operator Quick Reference
| Operator | Description |
|---|---|
repo | Inherit files from a remote Git repository |
include | Add files from the current repository |
exclude | Remove files from the in-memory filesystem |
rename | Transform file paths using regex patterns |
template | Mark files for variable substitution |
template-vars | Define variables for template substitution |
tools | Validate that required tools are installed |
yaml | Merge YAML configuration fragments |
json | Merge JSON configuration fragments |
toml | Merge TOML configuration fragments |
ini | Merge INI configuration fragments |
markdown | Merge markdown document fragments |
self | Run operations locally without exposing them to consumers |
Example configuration:
# .common-repo.yaml
- repo: { ... }
- include: [ ... ]
- exclude: [ ... ]
- rename: [ ... ]
- template: [ ... ]
- template-vars: { ... }
- tools: [ ... ]
- yaml: { ... }
- json: { ... }
- toml: { ... }
- ini: { ... }
- markdown: { ... }
- self: [ ... ]
Core Operators
repo - Inherit from a Repository
Pull files from a remote Git repository.
- repo:
url: https://github.com/owner/repo
ref: v1.0.0
Options
| Option | Required | Description |
|---|---|---|
url | Yes | Git repository URL |
ref | Yes | Git reference (tag, branch, or commit SHA) |
path | No | Sub-directory to use as root |
with | No | Inline operations to apply |
Examples
Basic inheritance:
- repo:
url: https://github.com/common-repo/rust-cli
ref: v1.2.0
Inherit a sub-directory:
# Only pull files from the 'templates/rust' directory
- repo:
url: https://github.com/common-repo/templates
ref: main
path: templates/rust
Inline filtering with with:
- repo:
url: https://github.com/common-repo/configs
ref: v2.0.0
with:
- include: [".github/**", ".pre-commit-config.yaml"]
- exclude: [".github/CODEOWNERS"]
- rename:
- ".github/workflows/ci-template.yml": ".github/workflows/ci.yml"
include - Add Files
Add files from the current repository to the output based on glob patterns.
- include:
- "**/*" # All files
- ".*" # Hidden files at root
- ".*/**/*" # All files in hidden directories
Patterns
**/*- All files recursively*.rs- All Rust files in current directorysrc/**/*.rs- All Rust files under src/.*- Hidden files (dotfiles) at root.*/**/*- All files in hidden directories
if-exists: — control over destination collisions
The optional if-exists: sibling key on an include: operator chooses what
happens when an included file’s destination already exists in the
consumer’s working tree at write time:
overwrite(default): the include result clobbers any local content at the destination. Equivalent to omittingif-exists:.preserve: skip the write so the consumer’s content survives. Useful when an upstream ships a sensible-default file the consumer is free to customise (for example, a stub CI workflow).error: fail propagation with a clear error naming the path. Useful when local content at the destination should be a hard signal that the consumer’s setup needs attention.
“Exists” means any filesystem entry at the destination path on disk — a regular file, a symlink (valid or broken), or a directory. The check uses on-disk metadata, so it detects entries that would be invisible to a plain file walk.
Example using preserve:
- include: [".github/workflows/ci.yaml"]
if-exists: preserve
Example using error — choose this when the destination holds content
that must not be silently replaced, such as production credentials
shipped via include: from a template repo:
- include: ["sensitive/config.yaml"]
if-exists: error
if-exists: works the same way inside a repo: block’s with: clause:
- repo:
url: https://github.com/common-repo/semantic-release
ref: v1.0.0
with:
- include: ["**"]
if-exists: preserve
Last-op-wins
Multiple operators on the same path follow last-op-wins by execution
order. A later non-deferred operator (including the Overwrite-tagged
output of a merge operator) replaces an earlier Preserve tag at the
same destination.
Interaction with upstream auto-merge:
if-exists: preserve does not opt out of an upstream auto-merge:
declaration. Auto-merge operations are deferred — they run at the
residual stage, after the consumer’s sequential pass. The merge produces
a fresh file with the default Overwrite tag at the destination,
clobbering any preceding Preserve tag.
A consumer-side exclude: likewise does not suppress an upstream
auto-merge:. The exclude removes the file from the composite, but the
deferred merge captures a snapshot of the upstream file at integration
time and merges that snapshot into the local file regardless.
The reliable way to avoid an upstream auto-merge: for a particular
file is to remove the merge declaration upstream — either by working
with the upstream maintainer, or by depending on a fork that does not
declare the merge.
Unknown sibling keys
A typo in an operator-level sibling key (for example, if-exits:
instead of if-exists:) does not abort propagation. Whether the typo
is caught depends on which YAML form the config uses:
- Legacy bare-mapping form (the original parser, where the operator
key and its siblings are direct children of a list-entry mapping):
emits a
log::warn!at default visibility naming the unknown key. The key is dropped and propagation continues. - Standard form (serde’s derived deserializer): silently ignores
unknown fields. A typo parses as if the key were absent, so
if-exits: preserveproduces no warning andif_existsfalls back to its defaultOverwrite.
Most consumer configs are written in the legacy bare-mapping form, so the warning fires for typos. Configs in standard form should be reviewed manually — serde will not flag unknown keys.
exclude - Remove Files
Remove files from the in-memory filesystem based on glob patterns.
- exclude:
- ".git/**"
- "target/**"
- "**/*.bak"
- "node_modules/**"
rename - Transform Paths
Transform file paths using regex patterns with capture group placeholders.
- rename:
- "old-name/(.*)": "new-name/$1"
- "^templates/(.*)": "$1"
- "(.+)\\.template$": "$1"
Placeholders
$1- First capture group$2- Second capture group- etc.
Examples
Strip a directory prefix:
- rename:
- "^files/(.*)": "$1"
Result: files/config.yaml becomes config.yaml
Move files to a subdirectory:
- rename:
- "^(.+\\.md)$": "docs/$1"
Result: README.md becomes docs/README.md
Rename file extensions:
- rename:
- "(.+)\\.template$": "$1"
Result: config.yaml.template becomes config.yaml
template - Mark Template Files
Mark files for variable substitution. Files matching these glob patterns are scanned for the __COMMON_REPO__ prefix — if found, the file is flagged as a template for processing during composite construction.
- template:
- "**/*.template"
- ".github/workflows/*.yml"
- "Cargo.toml"
Template files use the __COMMON_REPO__VARNAME__ sentinel pattern for variable placeholders:
# In a template file (e.g., Cargo.toml)
name = "__COMMON_REPO__PROJECT_NAME__"
version = "__COMMON_REPO__VERSION__"
Variable names must be valid identifiers ([A-Za-z_][A-Za-z0-9_]*) and must not contain double underscores (__).
template-vars - Define Variables
Define variables for template substitution. Values are literal strings.
- template-vars:
project_name: my-project
author: Jane Doe
rust_version: "1.75"
Variable Cascading
Variables cascade through the inheritance tree. Child repos can override ancestor variables:
# In parent repo
- template-vars:
log_level: info
# In child repo (overrides parent)
- template-vars:
log_level: debug
tools - Validate Required Tools
Check that required tools are installed with correct versions.
- tools:
- rustc: ">=1.70"
- cargo: "*"
- pre-commit: "^3.0"
- node: "~18.0"
Version Constraints
| Syntax | Meaning |
|---|---|
* | Any version |
1.70 | Exactly 1.70 |
>=1.70 | 1.70 or higher |
^1.70 | Compatible with 1.70 (>=1.70.0, <2.0.0) |
~1.70 | Approximately 1.70 (>=1.70.0, <1.71.0) |
This operator validates but does not install tools. Warnings are issued for missing or incompatible versions.
Structural Operators
self - Local-Only Operations
Run a sub-pipeline of operations that apply only to the local repository. Files produced by self: blocks are written to disk but never appear in the composite filesystem that downstream consumers see.
This is useful when a repository is both a source (providing shared configuration to consumers) and wants to consume from its own upstream repos.
- self:
- repo:
url: https://github.com/org/upstream-tooling
ref: v1.0.0
- exclude:
- ".releaserc.yaml"
How It Works
- The orchestrator partitions
self:blocks from the rest of the config - The source pipeline runs first (producing the composite filesystem for consumers)
- Each
self:block runs as an independent pipeline afterward - Output from
self:blocks is written to the working directory but excluded from what consumers inherit
Rules
self:blocks must contain at least one operationself:blocks cannot be nested (noself:insideself:)self:blocks are stripped when a repo is consumed as an upstream — consumers never see them- Any operation valid at the top level can appear inside
self:(repo, include, exclude, rename, template, merge operators, etc.) - A
self:block needs at least onerepo:to populate its composite filesystem. Without arepo:, the composite is empty and filtering operators likeinclude/exclude/renamehave nothing to operate on.
Example: Source Repo That Consumes Upstream Tooling
# .common-repo.yaml for a shared-config repo
# Self operations — local only, consumers don't see these
- self:
- repo:
url: https://github.com/org/ci-tooling
ref: v2.0.0
- exclude:
- ".releaserc.yaml"
- "commitlint.config.cjs"
# Source API — what consumers get
- include:
- "src/**"
- "src/.*"
- template:
- "src/.github/workflows/release.yaml"
- template-vars:
GH_APP_ID_VAR: CHRISTMAS_ISLAND_APP_ID
- rename:
- from: "^src/(.*)$"
to: "$1"
In this example, the repo pulls CI tooling for its own use via self:, while consumers only see the files exposed by include, template, template-vars, and rename.
For a detailed guide on using self: when authoring upstream repositories, see Authoring Upstream Repositories.
Merge Operators
Merge operators intelligently combine configuration fragments into destination files.
Upstream-Declared Merge (defer/auto-merge)
All merge operators support two additional options for upstream-declared merge behavior:
| Option | Type | Description |
|---|---|---|
auto-merge | string | Shorthand: sets source=dest to this value and implies defer=true |
defer | bool | When true, this operation only runs when repo is used as an upstream |
auto-merge is the preferred syntax when the source and destination filenames are the same:
# In upstream repo's .common-repo.yaml
- markdown:
auto-merge: CLAUDE.md # source=dest=CLAUDE.md, defer=true
section: "## Rules"
append: true
defer: true is used when source and destination paths differ:
- yaml:
source: config/base.yaml
dest: config.yaml
path: settings
defer: true # Only applies when repo is inherited
See Upstream-Declared Merge Behavior for detailed usage.
Path Syntax
The path option in merge operators supports multiple notations for navigating nested structures:
| Syntax | Example | Description |
|---|---|---|
| Dot notation | foo.bar.baz | Access nested keys |
| Bracket notation | foo["bar"] or foo['bar'] | Access keys with special characters |
| Array indices | items[0] or items[1].name | Access array elements by index |
| Escaped dots | foo\.bar | Literal dot in key name |
| Mixed | servers[0].config["special.key"] | Combine notations |
Examples:
# Navigate to nested object
- yaml:
source: labels.yml
dest: config.yml
path: metadata.labels
# Access array element
- json:
source: script.json
dest: package.json
path: scripts[0]
# Key with special characters
- toml:
source: deps.toml
dest: Cargo.toml
path: dependencies["my-crate"]
# Escaped dot in key name
- yaml:
source: fragment.yml
dest: config.yml
path: config\.v2.settings
yaml - Merge YAML Files
- yaml:
source: fragment.yml
dest: config.yml
Options
| Option | Required | Default | Description |
|---|---|---|---|
source | Yes* | - | Source fragment file |
dest | Yes* | - | Destination file |
auto-merge | No | - | Shorthand: sets source=dest, implies defer=true |
defer | No | false | Only apply when repo is used as an upstream |
path | No | root | Path to merge at (see Path Syntax) |
array_mode | No | replace | Array handling: replace, append, or append_unique |
append | No | false | Deprecated: use array_mode: append instead |
*Either source+dest or auto-merge is required
Array Merge Modes
| Mode | Description |
|---|---|
replace | Replace destination array with source array (default) |
append | Append source items to the end of destination array |
append_unique | Append only items not already in destination array |
Examples
Merge at root:
- yaml:
source: extra-config.yml
dest: config.yml
Merge at a specific path:
# Merge labels into metadata.labels
- yaml:
source: labels.yml
dest: kubernetes.yml
path: metadata.labels
Append to a list:
# Add items to an existing list
- yaml:
source: extra-items.yml
dest: config.yml
path: items
append: true
Add jobs to CI workflow:
# Merge shared CI jobs into existing workflow
- yaml:
source: ci-jobs.yml
dest: .github/workflows/ci.yml
path: jobs
append: true
json - Merge JSON Files
- json:
source: fragment.json
dest: package.json
path: dependencies
Options
| Option | Required | Default | Description |
|---|---|---|---|
source | Yes* | - | Source fragment file |
dest | Yes* | - | Destination file |
auto-merge | No | - | Shorthand: sets source=dest, implies defer=true |
defer | No | false | Only apply when repo is used as an upstream |
path | No | root | Dot-notation path to merge at |
append | No | false | Append to arrays instead of replace |
position | No | - | Where to append: start or end (only used when append: true) |
*Either source+dest or auto-merge is required
Examples
Add dependencies to package.json:
- json:
source: extra-deps.json
dest: package.json
path: dependencies
Append scripts:
- json:
source: scripts.json
dest: package.json
path: scripts
append: true
position: start
Merge TypeScript compiler options:
- json:
source: strict-options.json
dest: tsconfig.json
path: compilerOptions
toml - Merge TOML Files
- toml:
source: fragment.toml
dest: Cargo.toml
path: dependencies
Options
| Option | Required | Default | Description |
|---|---|---|---|
source | Yes* | - | Source fragment file |
dest | Yes* | - | Destination file |
auto-merge | No | - | Shorthand: sets source=dest, implies defer=true |
defer | No | false | Only apply when repo is used as an upstream |
path | No | root | Path to merge at (see Path Syntax) |
array_mode | No | replace | Array handling: replace, append, or append_unique |
append | No | false | Deprecated: use array_mode: append instead |
preserve-comments | No | false | Keep comments in output |
*Either source+dest or auto-merge is required
See Array Merge Modes for details on array handling options.
Examples
Add Cargo dependencies:
- toml:
source: common-deps.toml
dest: Cargo.toml
path: dependencies
Add dev-dependencies with comments preserved:
- toml:
source: test-deps.toml
dest: Cargo.toml
path: dev-dependencies
preserve-comments: true
Merge pyproject.toml settings:
- toml:
source: lint-settings.toml
dest: pyproject.toml
path: tool.ruff
ini - Merge INI Files
- ini:
source: fragment.ini
dest: config.ini
section: database
Options
| Option | Required | Default | Description |
|---|---|---|---|
source | Yes* | - | Source fragment file |
dest | Yes* | - | Destination file |
auto-merge | No | - | Shorthand: sets source=dest, implies defer=true |
defer | No | false | Only apply when repo is used as an upstream |
section | No | - | INI section to merge into |
append | No | false | Append values instead of replace |
allow-duplicates | No | false | Allow duplicate keys |
*Either source+dest or auto-merge is required
Examples
Merge database settings:
- ini:
source: db-settings.ini
dest: config.ini
section: database
Add editor configuration rules:
# Merge settings into the [*] section for all files
- ini:
source: editor-rules.ini
dest: .editorconfig
section: "*"
Configure git settings:
- ini:
source: git-aliases.ini
dest: .gitconfig
section: alias
markdown - Merge Markdown Files
- markdown:
source: installation.md
dest: README.md
section: "## Installation"
Options
| Option | Required | Default | Description |
|---|---|---|---|
source | Yes* | - | Source fragment file |
dest | Yes* | - | Destination file |
auto-merge | No | - | Shorthand: sets source=dest, implies defer=true |
defer | No | false | Only apply when repo is used as an upstream |
section | No | - | Heading to merge under |
level | No | 2 | Heading level (1-6) |
append | No | false | Append to section |
position | No | - | Where to insert: start or end (only used when append: true) |
create-section | No | false | Create section if missing |
*Either source+dest or auto-merge is required
Examples
Add installation instructions:
- markdown:
source: install-instructions.md
dest: README.md
section: "## Installation"
append: true
position: end
create-section: true
Add contributing section:
# Insert a standard Contributing section in the README
- markdown:
source: contributing-section.md
dest: README.md
section: "## Contributing"
create-section: true
Merge CLAUDE.md rules (upstream-declared):
# In upstream repo - will only apply when inherited
- markdown:
auto-merge: CLAUDE.md
section: "## Rules"
append: true
Operation Order
Operations execute in the order they appear in the configuration file. For inheritance:
- Ancestor repos are processed before parent repos
- Parent repos are processed before the local repo
- Siblings are processed in declaration order
This means later operations can override earlier ones, and child repos can customize what they inherit from ancestors.
self: blocks execute after the source pipeline completes, in declaration order. Each self: block runs as an independent sequential pipeline: operations fire in YAML declaration order, and repo: operations resolve inline at their declaration position. The pipeline starts with local files loaded from the working directory. Because local files are loaded first, filter operators (include, exclude, rename) can transform the local file set before a repo: integrates upstream content.
Example Order
# local .common-repo.yaml
- repo: {url: A, ref: v1} # A is processed first (including A's ancestors)
- repo: {url: B, ref: v2} # B is processed second (including B's ancestors)
- include: ["local/**"] # Local operations are processed last
If A inherits from C, and B inherits from D:
Processing order: C -> A -> D -> B -> local
Complete Example
Here’s a complete configuration showing multiple operators:
# .common-repo.yaml
# Inherit base Rust CLI configuration
- repo:
url: https://github.com/common-repo/rust-cli
ref: v2.0.0
with:
- include: ["**/*"]
- exclude: [".git/**", "target/**"]
# Inherit pre-commit configuration
- repo:
url: https://github.com/common-repo/pre-commit-rust
ref: v1.5.0
with:
- include: [".pre-commit-config.yaml"]
# Consume shared tooling for this repo only (not exposed to consumers)
- self:
- repo:
url: https://github.com/common-repo/shared-tooling
ref: v1.0.0
# Include local files
- include:
- src/**
- Cargo.toml
- README.md
# Exclude generated files
- exclude:
- "**/*.generated.rs"
# Rename template files
- rename:
- "(.+)\\.template$": "$1"
# Define template variables
- template-vars:
project_name: my-project
author: Your Name
rust_edition: "2021"
# Mark files as templates
- template:
- Cargo.toml
- README.md
# Require tools
- tools:
- rustc: ">=1.70"
- cargo: "*"
- pre-commit: ">=3.0"
# Merge additional dependencies into Cargo.toml
- toml:
source: extra-deps.toml
dest: Cargo.toml
path: dependencies
# Add CI workflow
- yaml:
source: ci-jobs.yml
dest: .github/workflows/ci.yml
path: jobs
append: true