Config reference
A config file names the repositories to manage and a shared block of sections applied to every one of them. Every key is validated, so a typo becomes a load error, not a silent no-op — unknown fields anywhere in the schema are rejected.
File anatomy
A config lists repos: — owner/repo strings, each validated to be
in that exact form — plus the shared sections below. All sections are optional, but the
repos: list must have at least one entry.
repos:
- octocat/hello-world
- octocat/spoon-knife
settings:
description: "Example repository"
private: false
has_issues: true
delete_branch_on_merge: true
actions:
enabled: true
allowed_actions: all
can_approve_pull_request_reviews: true
labels:
- name: bug
color: d73a4a
description: Something isn't working
- name: needs-triage
color: fbca04
collaborators:
- username: octocat
permission: admin
webhooks:
- url: https://example.com/hooks/ci
events: [push, pull_request]
secret_from_env: CI_WEBHOOK_SECRET
secrets:
- name: DEPLOY_TOKEN
value_from_env: DEPLOY_TOKEN
variables:
- name: ENVIRONMENT
value: production
rulesets:
- name: main-branch-protection
target: branch
rules:
- type: pull_request
required_approving_review_count: 1
Section authority
A declared section is the complete desired set. Anything present on the repo but absent from the section is removed on apply: labels, webhooks, secrets, and variables not listed are deleted; direct collaborators not listed are removed; repo rulesets not listed are deleted. There's no partial-update mode — declaring three labels when the repo has five means the other two go away.
An omitted section is the opposite: leave it out entirely and that whole domain is left unmanaged, untouched by apply. Whether you declare a section at all matters just as much as what you put in it.
rulesets
Repository rulesets are declared under rulesets:, one entry per ruleset,
matched to live rulesets by name and reconciled to exactly the declared spec.
The shape is large enough — target, enforcement, bypass actors, conditions, and a dozen rule
types — that it has its own page: see the rulesets reference.
settings
Repository-level settings. Every field is optional; a field left unset is left unmanaged on the repo (not reset to a default).
| Field | Type | Default | Notes |
|---|---|---|---|
description | string | unmanaged | |
homepage | string | unmanaged | |
private | bool | unmanaged | |
topics | list[string] | unmanaged | |
has_issues | bool | unmanaged | |
has_wiki | bool | unmanaged | |
has_projects | bool | unmanaged | |
has_discussions | bool | unmanaged | |
default_branch | string | unmanaged | |
allow_squash_merge | bool | unmanaged | |
allow_merge_commit | bool | unmanaged | |
allow_rebase_merge | bool | unmanaged | |
allow_auto_merge | bool | unmanaged | |
delete_branch_on_merge | bool | unmanaged | |
allow_update_branch | bool | unmanaged |
actions
Actions enablement/policy and workflow permissions — three separate GitHub API endpoints
that PyGithub doesn't model, driven directly. Every field is optional; a field left unset is
left unmanaged, and an unmanaged field of a pair (e.g. enabled vs.
allowed_actions) is written back with its live value so the PUT doesn't clear
it.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | unmanaged | Whether Actions is enabled on the repo |
allowed_actions | all | local_only | selected | unmanaged | |
selected_actions.github_owned_allowed | bool | true | Only meaningful when allowed_actions: selected |
selected_actions.verified_allowed | bool | false | |
selected_actions.patterns_allowed | list[string] | [] | owner/repo patterns |
default_workflow_permissions | read | write | unmanaged | |
can_approve_pull_request_reviews | bool | unmanaged | "Allow GitHub Actions to create and approve pull requests" |
actions:
enabled: true
allowed_actions: selected
selected_actions:
github_owned_allowed: true
verified_allowed: true
patterns_allowed: ["astral-sh/*"]
default_workflow_permissions: read
can_approve_pull_request_reviews: true
labels
Issue and PR labels, matched by name. Exact-set semantics apply: labels on
the repo that aren't in the list are deleted.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Match key |
color | string | ededed | A leading # is stripped and the value lowercased before it's sent |
description | string | unmanaged | Omitted means left as-is on GitHub, not cleared |
collaborators
Direct collaborators and their permission level, matched by username.
| Field | Type | Default | Notes |
|---|---|---|---|
username | string | required | Match key |
permission | pull | triage | push | maintain | admin | push |
Only direct collaborators (GitHub affiliation direct) are
managed. Access inherited from an organization or a team is never inspected or touched, so
this section can't accidentally revoke org-level access.
webhooks
Repository webhooks, matched by url.
| Field | Type | Default | Notes |
|---|---|---|---|
url | string | required | Match key |
events | list[string] | [push] | |
active | bool | true | |
content_type | json | form | json | |
insecure_ssl | bool | false | |
secret_from_env | string | unset | Env var holding the webhook secret |
A webhook with a secret is re-sent on every apply, whether or not anything else
changed — the GitHub API never returns a webhook's existing secret, so there's no value to
diff against. Plans show it as (set) rather than printing it.
secrets
Actions secrets. Each entry needs a name plus exactly one of
value or value_from_env — providing both, or neither, is a
validation error.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Match key |
value | string | unset | Literal value; exactly one of value/value_from_env |
value_from_env | string | unset | Env var to read the value from |
Secret values are write-only: GitHub never returns them, so apply never diffs the current
value against the declared one. By default a secret already present on the repo is left
alone; pass --force-secrets to re-push every declared secret, e.g. to rotate
one. Values are never printed in plans.
Literal value: is supported for convenience, but a secret's value in plain
YAML is a secret in your git history. Prefer value_from_env and keep the actual
value out of the repo entirely.
variables
Actions repository variables — same shape as secrets, but not sensitive.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Match key |
value | string | unset | Literal value; exactly one of value/value_from_env |
value_from_env | string | unset | Env var to read the value from |
Variable values are readable on GitHub and shown in plain text in plans. Unlike secrets, an existing variable is only pushed when its resolved value actually differs from what's on the repo.
Composing with extends
A config can extends: one or more base files:
extends: ../base/org-defaults.yaml
# or
extends:
- ../base/org-defaults.yaml
- ../base/security-baseline.yaml
Paths are relative to the file that declares them, and extends: is
recursive — a base can itself extend another base. Bases merge underneath the file that
extends them, in list order, and the extending file's own content merges on top of all of
them last.
Merge rules:
- Scalars (strings, bools, individual
settings/actionsfields): the override wins outright. - Keyed lists —
rulesets,labels,secrets, andvariablesbyname;collaboratorsbyusername;webhooksbyurl— merge item-by-item: an override item sharing a base item's key replaces that item in place (keeping its position), and an override item with a new key is appended.
A circular extends chain (A extends B extends A) is detected and rejected as
a config error rather than looping forever.
By convention, *.yml files in the config directory are the applied
configs — each with its own repos: list, picked up by the CLI's config-dir glob
— while *.yaml files are base layers that only exist to be pulled in via
extends:. Neither extension is enforced by the schema; it's a naming
convention this repo relies on to tell the two apart at a glance.
extends: reads local files by relative path with no sandboxing — treat a
base file with exactly the same scrutiny you'd give the config that includes it. It's a full
peer of the file that extends it, not an inert template.
Environment-sourced values
value_from_env (secrets, variables) and secret_from_env
(webhooks) are resolved when a plan or apply actually runs — not when the config is loaded
and validated, so schema checks don't require any secrets to be present in the
environment.
An env var that's unset or empty is a hard error at resolve time, and the
two are treated identically on purpose: in GitHub Actions, ${{ secrets.X }} for
an unset secret expands to an empty string rather than failing the expression, so a
presence-only check would silently propagate an empty value to every managed repo instead of
failing loudly.