Stop clicking through GitHub settings tabs. Define your repos, branch rules, labels, teams, CI, and security in one YAML file — then apply it in seconds. Unlike Terraform (heavy, HCL, state files) or shell scripts (fragile, no drift detection), gh-setup is purpose-built for GitHub: zero state management, idempotent, and interactive when you want it.
Installable as a standalone binary or as a gh CLI extension.
- Interactive wizard — generates a complete
gh-setup.yamlthrough guided prompts - Idempotent apply — only mutates what has drifted, safe to run repeatedly
- Dry-run mode — preview every change before it happens
- Diff — compare your config against live GitHub state (text or JSON output)
- Branch protection presets — none, basic, standard, strict, or fully custom
- Import — reverse-engineer config from an existing GitHub account or repo
- CI workflow templates — Go, Rust, Node.js, Python, Docker, Terraform, Java, Ruby (embedded)
- Pipeline-friendly —
--non-interactivemode with secrets via env vars for CI/CD - Governance files — CONTRIBUTING.md, Code of Conduct, SECURITY.md, CODEOWNERS
- Security — Dependabot, secret scanning, code scanning, dependabot.yml generation
- Secrets management — org and repo-level secrets (values prompted securely at apply time)
- Config validation — catches errors before any API calls are made
| gh-setup | Terraform + GitHub provider | |
|---|---|---|
| Config language | YAML | HCL |
| State management | None — compares directly against the API | Remote state file required |
| Setup | Single binary, gh auth login |
Terraform + provider + backend config |
| Drift detection | gh setup diff |
terraform plan |
| Learning curve | Minimal — one YAML file | HCL syntax, state concepts, module system |
| Import existing repos | gh setup import generates full YAML |
terraform import per-resource, write HCL by hand |
| Interactive mode | Wizard, per-action confirm prompts | No |
| CI workflows | Embedded templates (8 languages) | Write separately |
| Governance files | Built-in (CONTRIBUTING, CoC, SECURITY, CODEOWNERS) | Write separately |
| Scope | GitHub only | Multi-cloud |
Terraform is the right choice if you manage GitHub alongside AWS/GCP/Azure infrastructure. gh-setup is for teams that just want their GitHub settings in version control without the overhead of a full IaC stack.
gh extension install amenophis1er/gh-setupThen use it as gh setup <command>.
brew tap amenophis1er/tap
brew install gh-setupRequires a tagged release. See Releases.
go install github.com/amenophis1er/gh-setup@latestgit clone https://github.com/amenophis1er/gh-setup.git
cd gh-setup
make buildReady-to-use configs for common setups — copy one and edit to fit your account:
| Example | Description |
|---|---|
| minimal.yaml | One repo, no frills |
| personal.yaml | Personal account with a few repos, labels, and security |
| startup.yaml | Small org — private repos, one team, secrets |
| open-source-org.yaml | Public org — repo_scope: all, strict protection, governance, teams |
# Try one out
cp examples/startup.yaml gh-setup.yaml
# Edit account.name, repo names, team members, etc.
gh setup apply --dry-run# 1. Generate a config interactively
gh setup init
# 2. Review the generated file
cat gh-setup.yaml
# 3. Preview what would change
gh setup apply --dry-run
# 4. Apply for real
gh setup applyIf you're in a local git repo with no remote, apply will automatically add the origin remote when it creates the GitHub repo (when the directory name matches the repo name in your config):
mkdir my-project && cd my-project
git init
gh setup init # configure your repo
gh setup apply # creates the repo on GitHub + adds origin
git push -u origin maingh-setup resolves your GitHub token in this order:
GITHUB_TOKENenvironment variableGH_TOKENenvironment variablegh auth token(automatic when running as aghextension)
If you're authenticated via gh auth login, no extra setup is needed. For CI or explicit control:
export GITHUB_TOKEN="ghp_..."GitHub Enterprise: set GITHUB_API_URL or GH_HOST to target a GHE instance.
The token needs the following scopes:
| Scope | Required for |
|---|---|
repo |
Repository creation, settings, branch protection, file commits |
admin:org |
Organization settings, team management (org accounts only) |
workflow |
CI workflow file commits |
Reverse-engineer an existing GitHub account or repo into a config file.
gh setup import # auto-detect from git remote
gh setup import myorg # import entire org
gh setup import myuser --repo my-repo # import a single repo
gh setup import --stdout -o json # auto-detect, JSON to stdout
gh setup import myorg -c existing.yaml # write to a custom fileWhen run inside a git repository with a GitHub remote, the account and repo are inferred automatically — no arguments needed.
This fetches repos, labels, branch protection, teams, governance files, and security settings from the live GitHub state and generates a complete gh-setup.yaml. Great for adopting gh-setup on existing projects.
Interactive wizard that walks you through every configuration option and writes gh-setup.yaml.
gh setup init
gh setup init -c custom-config.yamlReads the config and applies it to GitHub. Each resource is fetched, compared, and only mutated if different.
gh setup apply # apply all changes
gh setup apply --dry-run # preview changes without mutating
gh setup apply -i # confirm each change interactively
gh setup apply -c other.yaml # use a different config file
gh setup apply --non-interactive # no prompts (for CI/CD pipelines)Compares your config file against the actual GitHub state and prints the differences.
gh setup diff # styled text output
gh setup diff --output json # JSON output for CI pipelines
gh setup diff -o json | jq . # pipe to jq for processingExample text output:
repo x-phone/xphone-rust
visibility: private → public
branch_protection.require_pr: false → true
labels: + breaking (e11d48)
labels: - wontfix (ffffff)
repo x-phone/xphone-go
✓ up to date
team core
+ member: new-contributor
gh setup versionThe full config file with all available options:
# gh-setup.yaml
account:
type: organization # individual | organization
name: my-org
defaults:
visibility: public # public | private
default_branch: main
delete_branch_on_merge: true
allow_squash_merge: true # omit to leave unchanged
allow_merge_commit: false # omit to leave unchanged
allow_rebase_merge: true # omit to leave unchanged
allow_auto_merge: true # enable auto-merge
has_issues: true # omit to leave unchanged
has_wiki: false # omit to leave unchanged
has_discussions: false # omit to leave unchanged
branch_protection:
preset: standard # none | basic | standard | strict | custom
# Custom overrides (only when preset: custom):
# require_pr: true
# required_approvals: 1
# dismiss_stale_reviews: false
# require_status_checks: false
# status_checks: [] # e.g. ["ci", "lint"]
# require_up_to_date: false
# enforce_admins: false
# allow_force_push: false
# allow_deletions: false
labels:
replace_defaults: true # remove GitHub's default labels first
items:
- { name: "bug", color: "d73a4a", description: "Something isn't working" }
- { name: "enhancement", color: "a2eeef", description: "New feature or request" }
- { name: "breaking", color: "e11d48", description: "Breaking change" }
- { name: "docs", color: "0075ca", description: "Documentation" }
- { name: "ci", color: "e4e669", description: "CI/CD changes" }
- { name: "chore", color: "cfd3d7", description: "Maintenance" }
repo_scope: all # omit to manage only listed repos (see Repo Scope below)
repos:
- name: my-api
description: "REST API service"
topics: ["api", "rest", "go"]
visibility: private # overrides default
homepage: "https://example.com"
ci: go # go | rust | node | python | docker | terraform | java | ruby
extra_protection: # overrides defaults.branch_protection for this repo only
preset: strict
- name: my-frontend
description: "Web frontend"
topics: ["frontend", "react"]
ci: node
teams: # organization only
- name: core
description: "Core maintainers"
permission: admin # read | write | admin
members: ["user1", "user2"]
- name: contributors
description: "External contributors"
permission: write
members: []
governance:
contributing: true # generate CONTRIBUTING.md
code_of_conduct: true # Contributor Covenant
security_policy: true # SECURITY.md
codeowners: | # .github/CODEOWNERS
* @my-org/core
security:
dependabot: true # enable alerts + generate dependabot.yml
secret_scanning: true
code_scanning: false # requires GitHub Advanced Security on private repos
secrets: # names only — values prompted at apply time
- name: DEPLOY_TOKEN
scope: org # org-level secret (available to all repos)
- name: NPM_TOKEN
scope: repo # set on every repo listed in repos:By default, gh-setup only manages repos explicitly listed in repos:. Set repo_scope: all to manage every repo in the account:
repo_scope: all
defaults:
visibility: private
branch_protection:
preset: standard
repos:
# Only repos that need overrides — all others get the defaults above
- name: public-docs
visibility: public
extra_protection:
preset: noneHow it works:
- All repos are discovered from the GitHub account via API
defaults(labels, branch protection, security, etc.) are applied to every discovered repo- Repos listed in
repos:override those defaults for that specific repo - Repos in
repos:that don't exist yet are created
Caveats:
- Without
repo_scope: all, unlisted repos are completely untouched - With
repo_scope: all, be cautious — defaults apply to every repo, including ones you may not want to change - Start with
gh setup diffto preview what would change before runninggh setup apply - Consider using
gh setup import myorg --stdoutfirst to see the current state of all repos
| Rule | None | Basic | Standard | Strict |
|---|---|---|---|---|
| Require PR | 1 approval | 1 approval | ||
| Require status checks | yes | |||
| Require up-to-date | yes | |||
| Block force push | yes | yes | yes | |
| Block deletion | yes | yes | yes |
Choose custom to configure each rule individually in the wizard or YAML.
Built-in templates embedded in the binary:
| Template | Steps |
|---|---|
| go | go vet, golangci-lint, go test ./... |
| rust | cargo fmt --check, cargo clippy -- -D warnings, cargo test |
| node | npm ci, npm run lint, npm test |
| python | ruff check, mypy, pytest |
| docker | docker/build-push-action (build only) |
| terraform | terraform fmt -check, terraform validate, terraform plan |
| java | mvn verify (Temurin JDK 21, Maven cache) |
| ruby | bundle exec rubocop, bundle exec rspec |
Templates are written to .github/workflows/ci.yml in each repository.
Each resource follows the same pattern:
- Fetch current state from the GitHub API
- Compare with the desired config
- Skip if already matching (idempotent)
- Mutate if different (create or update)
- Report the action taken
In --dry-run mode, step 4 is replaced with a preview log.
In -i (interactive) mode, step 4 requires confirmation.
In --non-interactive mode, all confirmations are auto-approved.
Run gh-setup in pipelines with --non-interactive. Secrets are read from env vars named GH_SETUP_SECRET_<NAME>:
# .github/workflows/setup.yml
- name: Apply GitHub config
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_SETUP_SECRET_DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: gh-setup apply --non-interactiveDetect config drift in CI:
- name: Check for drift
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHANGES=$(gh-setup diff -o json | jq '.changes | length')
if [ "$CHANGES" -gt 0 ]; then
echo "Drift detected: $CHANGES change(s)"
exit 1
fiBefore any API calls, the config is validated for:
- Required fields (
account.name, repo names, team names, secret names) - Valid enum values (account type, visibility, preset, permissions, secret scope)
- Character restrictions (no spaces or special characters in names)
- Duplicate detection (repo names, team names)
- Logical checks (teams require organization account type)
gh-setup/
├── main.go # entry point
├── cmd/
│ ├── root.go # root command, --config flag, version
│ ├── init.go # init wizard command
│ ├── import.go # import from live GitHub state
│ ├── apply.go # apply command (--dry-run, -i, --non-interactive)
│ └── diff.go # diff command (--output text/json)
├── internal/
│ ├── config/config.go # YAML structs, Load/Save, Validate, presets
│ ├── wizard/wizard.go # interactive wizard (charmbracelet/huh)
│ ├── gitutil/remote.go # auto-detect owner/repo from git remote
│ ├── github/
│ │ ├── client.go # authenticated GitHub client
│ │ ├── retry.go # retry/backoff with rate-limit handling
│ │ ├── org.go # organization settings
│ │ ├── repo.go # repo CRUD, topics, file content
│ │ ├── protection.go # branch protection rules
│ │ ├── labels.go # label management
│ │ ├── teams.go # team and membership management
│ │ ├── security.go # Dependabot, secret/code scanning
│ │ └── secrets.go # encrypted secrets (NaCl box)
│ ├── templates/
│ │ ├── ci.go # embedded CI workflow loader
│ │ ├── governance.go # CONTRIBUTING, CoC, SECURITY templates
│ │ ├── dependabot.go # dependabot.yml generation
│ │ └── workflows/ # CI YAML templates (8 languages/tools)
│ ├── apply/
│ │ ├── apply.go # idempotent apply logic
│ │ └── output.go # styled terminal output
│ ├── diff/diff.go # config vs live state comparison (text + JSON)
│ └── importer/importer.go # reverse-engineer config from GitHub
├── Makefile
├── .goreleaser.yml
└── .github/workflows/
├── ci.yml # CI pipeline
└── release.yml # goreleaser on tag push
make vet # run go vet
make test # run tests
make build # build binary
make all # vet + test + build
make clean # remove binaryMIT