gh-settings
ActionsAbout
Tags
Β (2)Declarative GitHub repository settings for the GitHub CLI.
Make repository configuration behave like infrastructure as code. One
.github/settings.yml describes the desired state; gh settings computes the
difference and applies it.
No GitHub App. No central service. No webhook. Just the GitHub CLI you already have.
$ gh settings plan
Plan for noirbizarre/gh-settings
Repository
~ update repository description
Topics
+ add topic rust
- remove topic archived
Labels
+ create label enhancement
~ update label bug
Autolinks
~ recreate autolink OPS- (no update endpoint)
2 to create, 2 to update, 1 to recreate, 1 to delete.
! this plan deletes existing configuration.$ gh settings sync --yes
β update repository description
β add topic rust
β remove topic archived
β create label enhancement
β update label bug
β recreate autolink OPS- (no update endpoint)
β applied 6 changes.Run it twice and the second run reports nothing to do. That is the point.
safe-settings is the incumbent, and
it is a fine tool β if you can run a GitHub App.
| safe-settings | gh-settings | |
|---|---|---|
| GitHub App required | yes | no |
| Central service | yes | no |
| Runs locally | no | yes |
| Preview before applying | no | gh settings plan |
| Deletes things you did not list | by default | only when you ask |
| Formal, versioned schema | no | yes |
| Editor completion | no | yes |
gh-settings uses the same spelling as safe-settings for the sections it
supports, so those need no rewriting. Sections it does not support yet β like
branches or collaborators β have to be removed first: unknown keys are a
parse error, which is what lets a typo be caught and suggested against. See
ADR-006 for exactly how far
compatibility goes.
gh extension install noirbizarre/gh-settingsThat is the only supported install path, deliberately β see
ADR-014. Building from source with cargo build
produces a standalone gh-settings binary rather than a gh subcommand, so the
gh settings β¦ examples below would not apply to it.
# Generate a configuration file from a repository you already have
gh settings export
# Check it, without touching the network β unless it uses `extends:`
gh settings validate
# See what would change
gh settings plan
# Apply it
gh settings sync
# Find out what your token can actually manage
gh settings doctorThe repository is inferred from your git remote, exactly as gh does. Override
it with -R owner/repo.
| Flag | Effect |
|---|---|
--only labels,topics |
Restrict the run to specific resources |
--prune / --no-prune |
Force deletion of unmanaged items on or off |
--dry-run |
Show what sync would do, change nothing |
--format json |
Machine-readable output |
--verbose |
Field-level detail in the plan |
--plan plan.json |
Apply a plan saved by plan --out |
| Code | Meaning |
|---|---|
0 |
Success, nothing to do |
1 |
Failure |
2 |
plan found pending changes |
The distinct code for pending changes lets CI detect drift without treating it as a build failure.
# $schema: https://noirbizarre.github.io/gh-settings/schema/v1/settings.json
version: 1
repository:
description: Declarative GitHub repository settings
homepage: https://noirbizarre.github.io/gh-settings
has_issues: true
has_wiki: false
allow_squash_merge: true
allow_merge_commit: false
delete_branch_on_merge: true
web_commit_signoff_required: true
security:
secret_scanning: true
secret_scanning_push_protection: true
topics:
- rust
- github-cli
- gh-extension
labels:
prune: true
items:
- name: bug
color: d73a4a
description: Something isn't working
- name: enhancement
color: a2eeef
autolinks:
- key_prefix: OPS-
url_template: https://jira.company.com/browse/<num>
is_alphanumeric: false
rulesets:
- name: main-protection
target: branch
enforcement: active
conditions:
ref_name:
include: ["~DEFAULT_BRANCH"]
bypass_actors:
- team: engineering
bypass_mode: pull_request
rules:
- type: pull_request
parameters:
required_approving_review_count: 1
- type: non_fast_forward
environments:
- name: production
wait_timer: 15
reviewers:
- team: engineering
deployment_branch_policy:
branches: ["main"]
variables:
- name: DEPLOY_URL
value: https://example.com
variables:
- name: DEFAULT_REGION
value: eu-west-1
actions:
enabled: true
allowed_actions: selected
selected_actions:
github_owned_allowed: true
verified_allowed: false
patterns_allowed:
- docker/*
default_workflow_permissions: read
can_approve_pull_request_reviews: false
artifact_and_log_retention_days: 30
fork_pr_contributor_approval: first_time_contributors
pages:
build_type: legacy
source:
branch: gh-pages
path: /
cname: docs.example.com
https_enforced: trueEvery section is optional, and an absent section is unmanaged β nothing is read, diffed or written for it. You can start by managing labels alone and nothing else will move.
By default the configuration is additive. A label that exists on GitHub but is absent from your file is left alone.
To make a section authoritative, opt in:
labels:
prune: true
items:
- name: bug
color: d73a4aDeletions always appear in the plan as - delete lines, and sync asks before
performing them. --no-prune overrides the file, so you can always force
safety. See ADR-005.
Add the schema annotation and get completion, validation and hover documentation:
# $schema: https://noirbizarre.github.io/gh-settings/schema/v1/settings.jsongh settings export writes it for you.
version: 1
extends: acme/.github@v1 # reads .github/settings.yml from acme/.github at v1
labels:
- name: bug
color: ff0000 # replaces the inherited `bug` outrightAnything the local file declares wins. Collections merge by item identity β a label of the same name replaces the inherited one as a whole, so change one field and you restate the item.
The ref is required, so a shared file cannot move underneath a plan you already
reviewed. A base may not itself use extends:.
prune is never inherited. Editing a shared file cannot start deleting
things in the repositories that extend it. Note that sync --prune on the
command line does apply to inherited items β that is a local, explicit
instruction.
Reading a base needs Contents: read on that repository, which the Actions
GITHUB_TOKEN does not have. See
authentication.
Important: secrets.GITHUB_TOKEN cannot manage repository settings.
A workflow's permissions: block has no administration key, so repository
metadata, topics, autolinks, rulesets, environments and Actions general settings
cannot be granted to it β
this is not a permission you forgot to enable, it cannot be requested at all.
Variables are blocked the same way, by the missing variables key. Labels and
Pages are the exceptions, since they live under Issues: write and
Pages: write, both of which the permissions: block does have keys for.
Use a personal access token or a GitHub App installation token:
- uses: actions/checkout@v5
- run: gh extension install noirbizarre/gh-settings
- run: gh settings sync --yes
env:
GH_TOKEN: ${{ secrets.GH_SETTINGS_TOKEN }} # NOT secrets.GITHUB_TOKENIn a workflow, use the action rather than wiring up the CLI by hand:
- uses: actions/checkout@v5
- uses: noirbizarre/gh-settings@main
with:
token: ${{ secrets.GH_SETTINGS_TOKEN }} # NOT secrets.GITHUB_TOKENcommand: plan reports drift through a changed output instead of failing the
job, and writes the plan to the job summary. See
docs/actions.md.
Run gh settings doctor to see what your current credential can manage:
$ gh settings doctor
Environment
β gh CLI gh version 2.62.0
β Authentication github.com as noirbizarre
β Token type classic personal access token
Scopes repo, read:org
Resources
β repository
β topics
β labels
β autolinks
β rulesets
β environments
β actions
β variables
β pagesFull details, including the exact scopes each resource needs, are in docs/authentication.md.
sync checks this before its first request and refuses to start when a change
is certain to be rejected, naming the permission instead of letting you find
out through a failed write. It refuses only when it can prove the problem β a
credential it cannot introspect is allowed through, so you get GitHub's error
rather than a guess.
| Resource | Status |
|---|---|
| Repository metadata, merge & security settings | β |
| Topics | β |
| Labels (including renames) | β |
| Autolinks | β |
| Rulesets | β |
| Environments (protection rules, reviewers, branch policies) | β |
| Actions variables, at repository and environment scope | β |
| Actions general settings (permissions, retention, fork PR approval) | β |
| GitHub Pages (source, custom domain, HTTPS) | β |
Inheritance from a shared repository (extends:) |
β |
Declaring a pages: section enables Pages; it is never turned back off, because
an absent section means unmanaged and a published site should not come down over
a missing key.
Two toggles on the General settings page are knowingly not supported:
Allow users to comment on individual commits and pull request diffs and
Automatically close issues with merged linked pull requests. Neither has a
field on PATCH /repos, and accepting one in the configuration would publish a
setting that is silently ignored.
Custom properties, webhooks and collaborators are planned; secrets are deliberately out of scope.
The guiding ambition: if it is under the repository Settings page, it should eventually be manageable here.
See the roadmap for the full picture, including what will never be supported and why.
gh-settings is built around a synchronisation engine. Every GitHub feature is
an independent resource implementing one trait:
load desired state β validate β read current state β diff β plan β apply
Adding support for a new setting means writing one module and adding one line to the registry. The engine never changes.
Decisions, and what they cost, are recorded as Architecture Decision Records. The ones worth reading first:
- ADR-001 β the resource abstraction
- ADR-003 β why we shell out to
gh api - ADR-005 β why deletion is opt-in
- ADR-015 β how permissions are tracked
mise run # fmt, lint, lint:actions, build, test
mise run test # cargo nextest
mise run cover # coverage
mise run snapshots # review insta snapshots
mise run schema # regenerate the JSON Schema
mise run docs:reference # regenerate the configuration reference
mise run docs # serve the documentation locallySee CONTRIBUTING.md.
Releases are orchestrated by gh-ship:
pushing to main maintains a Release PR carrying the version bump and
changelog, and merging it tags, drafts and publishes. See
ADR-014.
MIT
gh-settings is not certified by GitHub. It is provided by a third-party and is governed by separate terms of service, privacy policy, and support documentation.