Configuration
A config repository is the input to ghfanout: a directory (usually its own Git repository) containing everything the tool needs — which files to distribute, to which repositories, on which branches.
Concepts
ghfanout borrows Kustomize's base + overlay model:
- base — the content. Directories under
base/hold the actual files to distribute:base/common/is distributed to every repository, unconditionally- Every other directory under
base/is a profile: a named file set (e.g.java-service) that a repository receives only when its overlay opts in
- overlay — the selection. Each directory under
overlays/corresponds to one target repository (the directory name is the destination repository name), and itsmanifest.yamlchooses profiles, branches, and template values - ghfanout.yaml — the root config at the top of the config repository: the destination host / org, and tool-wide defaults
Typical tasks map onto this directly:
- Distribute a file to every repository → put it under
base/common/ - Share a
pom.xmlamong only the Java services → create ajava-serviceprofile and list it in thebases:of each Java repository's manifest - Give one repository a slightly different value → set it in that overlay's
values:and reference it from a template (Templates)
Directory layout
ghfanout.yaml # Declares the destination host / org (one per config repository)
base/
common/ # Common to all repositories, distributed unconditionally
.gitignore
java-service/ # A profile (distributed only when selected by an overlay)
pom.xml.tmpl # .tmpl is a template. Variables are expanded and distributed as pom.xml
node-service/
package.json
overlays/
user-service/ # Directory name = destination repository name
manifest.yaml
api-gateway/
manifest.yaml
ghfanout.yaml
The root configuration file:
host: github.example.com # Defaults to github.com (specify only for GitHub Enterprise Server)
org: myorg # Required. overlays/<name> corresponds to the myorg/<name> repository
deploy_mode: pr # Deployment method. pr: open a PR (default) / push: push directly to the target branch
output_dir: dist # Output directory for build (defaults to dist)
auth: # Non-secret GitHub App authentication settings (optional)
app_id: 123456
# installation_id: 789 # resolved automatically from org if omitted
private_key_file: ~/.config/ghfanout/app.pem
| Key | Required | Description |
|---|---|---|
org |
✓ | Destination GitHub org. overlays/<name> corresponds to the <org>/<name> repository |
host |
GitHub host. Defaults to github.com; specify only for GitHub Enterprise Server |
|
deploy_mode |
pr (default): open a pull request / push: push directly to the target branch. See CLI Reference for the trade-offs |
|
output_dir |
Output directory for build (defaults to dist). Relative paths resolve against the directory containing ghfanout.yaml |
|
auth |
Non-secret GitHub App authentication settings. See Authentication |
.ghfanoutignore
An optional file at the config repository root that excludes files under base/ from distribution. This lets a profile keep its own README.md or helper files alongside the content it ships, without those files being sent to the target repositories.
# Same syntax as .gitignore ('#' starts a comment only at the start of a line)
# A README.md at any depth in any profile
README.md
# All markdown files
*.md
# Everything under a docs/ directory — but keep docs/keep.md (negation)
docs/
!docs/keep.md
- Syntax is
.gitignorecompatible (including!negation,**, and trailing-slash directory matches). One difference from git itself:!can re-include a file whose parent directory is excluded, so thedocs/+!docs/keep.mdexample above works - Each pattern is matched against a file's path relative to its profile directory — e.g.
base/java-service/docs/README.mdis matched asdocs/README.md, so one rule applies acrosscommon/and every profile - Matching happens on the source name, before the
.tmplsuffix is stripped. To excludepom.xml.tmpl, writepom.xml.tmpl(or*.tmpl), notpom.xml - Only the single file at the config repository root is read; a
.ghfanoutignoreplaced inside a profile has no effect (it is treated like any other file and distributed)
manifest.yaml
One per overlay, at overlays/<repo>/manifest.yaml:
bases: # Profiles to distribute. common/ is always included
- java-service
branches: # Destination branches. Defaults to the target repository's default branch only
- main
- release-1.x
deploy_mode: push # Override the deployment method for only this repository (defaults to ghfanout.yaml's value)
values: # Values referenced from templates (*.tmpl) (optional, can be nested)
version: "1.2.3"
| Key | Required | Description |
|---|---|---|
bases |
Profiles (directory names under base/) to distribute. common/ is always included |
|
branches |
Destination branches. Defaults to the target repository's default branch only. Elements can be strings or objects (see Per-branch overrides) | |
deploy_mode |
Overrides ghfanout.yaml's deploy_mode for this repository only |
|
values |
Values referenced from templates; can be nested. See Templates |
Typos are caught early: unknown keys and duplicate branch names are rejected when the manifest is loaded, and a profile listed in bases that has no directory under base/ fails the build.
File precedence
When files with the same relative path exist in multiple profiles, common/ has the lowest priority, and profiles listed later in bases: take precedence. The later one wins and fully overwrites the file — this is reported via an info log when it occurs.
Per-branch overrides
By writing an element of branches: in object form, you can override the profiles to distribute (bases) or the template values (values) for just that branch. Strings and objects can be mixed:
bases:
- java-service
values:
version: "1.0"
branches:
- main # String = use the top-level bases / values as-is
- name: release-1.x # Object = override bases or values for just this branch
bases:
- java-service-legacy
values:
version: "0.9"
The override semantics differ between bases and values:
basesis a replacement: the top-levelbasesis not inherited (explicitly specifyingbases: []distributes onlycommon/)valuesis a deep merge (similar to Helm): it is recursively merged key by key into the top-levelvalues, inheriting common values while overriding only the differences. Lists are not concatenated — they are replaced wholesale
Note: building an overlay that has a per-branch bases or values override writes output per branch to <output>/<branch name>/. See CLI Reference for details.