Skip to content

amenophis1er/gh-setup

Repository files navigation

gh-setup

CI codecov Release License: MIT

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.

Features

  • Interactive wizard — generates a complete gh-setup.yaml through 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-interactive mode 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

Why not Terraform?

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.

Installation

As a gh CLI extension (recommended)

gh extension install amenophis1er/gh-setup

Then use it as gh setup <command>.

Homebrew

brew tap amenophis1er/tap
brew install gh-setup

Requires a tagged release. See Releases.

From source

go install github.com/amenophis1er/gh-setup@latest

Build locally

git clone https://github.com/amenophis1er/gh-setup.git
cd gh-setup
make build

Examples

Ready-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

Quick Start

# 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 apply

Enrolling a local repo

If 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 main

Authentication

gh-setup resolves your GitHub token in this order:

  1. GITHUB_TOKEN environment variable
  2. GH_TOKEN environment variable
  3. gh auth token (automatic when running as a gh extension)

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

Commands

gh setup import

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 file

When 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.

gh setup init

Interactive wizard that walks you through every configuration option and writes gh-setup.yaml.

gh setup init
gh setup init -c custom-config.yaml

gh setup apply

Reads 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)

gh setup diff

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 processing

Example 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 version

gh setup version

Config Reference

The 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:

Repo Scope

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: none

How it works:

  1. All repos are discovered from the GitHub account via API
  2. defaults (labels, branch protection, security, etc.) are applied to every discovered repo
  3. Repos listed in repos: override those defaults for that specific repo
  4. 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 diff to preview what would change before running gh setup apply
  • Consider using gh setup import myorg --stdout first to see the current state of all repos

Branch Protection Presets

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.

CI Workflow Templates

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.

Apply Behavior

Each resource follows the same pattern:

  1. Fetch current state from the GitHub API
  2. Compare with the desired config
  3. Skip if already matching (idempotent)
  4. Mutate if different (create or update)
  5. 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.

CI/CD Usage

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-interactive

Detect 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
    fi

Config Validation

Before 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)

Project Structure

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

Development

make vet      # run go vet
make test     # run tests
make build    # build binary
make all      # vet + test + build
make clean    # remove binary

License

MIT

About

Declaratively configure GitHub accounts, repos, and settings from a YAML file

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors