Skip to content

JMR-dev/gh-repo-bootstrap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gh-repo-bootstrap

A gh CLI extension for applying a standard set of guard-rails to a GitHub repository, powered by Pulumi:

  • A branch protection ruleset on the default branch (no force-push, no deletion, required PRs with N approvals, resolved review threads, optional signed commits)
  • Optional repository creation (--create) or adoption of an existing repo (--manage-repo) so the tool also manages repo-level settings: visibility, description, default branch, topics, merge buttons, delete-branch-on-merge, etc.
  • A configurable set of deployment environments with optional protection rules (required reviewers, wait timer, prevent-self-review, admin bypass, deployment branch policies including custom branch/tag patterns)
  • Optional repository- and environment-level GitHub Actions secrets, sourced from KEY = "value" files
  • A single TOML file (--config FILE) can describe everything above

Install

gh extension install JMR-dev/gh-repo-bootstrap

gh will fetch the precompiled binary for your OS/arch from the latest release. You also need:

  • pulumi on PATH
  • gh already authenticated (gh auth login), or a GITHUB_TOKEN exported in the environment — the extension uses GITHUB_TOKEN when it is set and otherwise falls back to gh auth token

Use

# Apply defaults (1 review, production env) to a repo:
gh repo-bootstrap JMR-dev/my-app

# Custom: 2 reviews, signed commits, multiple environments:
gh repo-bootstrap JMR-dev/api \
  --reviews 2 --signed \
  --env production --env staging --env preview

# Solo maintainer: allow the Admin role (you) to bypass the ruleset
# so you can merge your own PRs without a second approver:
gh repo-bootstrap JMR-dev/solo-project --solo

# Preview without applying:
gh repo-bootstrap JMR-dev/my-app --plan

# Tear down what this tool manages:
gh repo-bootstrap JMR-dev/my-app --destroy

Creating a new repo

--create registers the repo as a Pulumi resource. It prompts for visibility and description if those flags are not supplied; everything else uses defaults or flag/config values:

gh repo-bootstrap JMR-dev/my-new-app --create \
  --visibility private \
  --description "Service for X" \
  --topic go --topic service \
  --no-allow-merge-commit --allow-squash-merge \
  --delete-branch-on-merge \
  --auto-init

Managing an existing repo's settings

--manage-repo imports the existing GitHub repository into Pulumi state on the first apply and manages it from then on. Always run --plan first — the first apply imports the repo and reconciles any drift between your flags/config and the live settings in a single operation:

gh repo-bootstrap JMR-dev/api --manage-repo \
  --visibility private \
  --description "API service" \
  --default-repo-branch main \
  --no-allow-merge-commit --allow-squash-merge \
  --plan

Environment protection rules

gh repo-bootstrap JMR-dev/api \
  --env production \
  --env-reviewer production:user:octocat \
  --env-reviewer production:team:JMR-dev/release-managers \
  --env-wait-timer production:5 \
  --env-prevent-self-review production \
  --env-no-admin-bypass production \
  --env-branch-policy production:custom \
  --env-branch-pattern production:'release/*' \
  --env-branch-pattern production:'hotfix/*'

Reviewer specs accept numeric IDs or string identifiers (user:octocat, team:JMR-dev/release-managers). Strings are resolved to numeric IDs via gh api before Pulumi runs. Team specs must include the org (org/team-slug).

TOML configuration

A single --config FILE can describe everything. When --config is used, no other flags are allowed:

owner     = "JMR-dev"
name      = "my-new-app"
mode      = "create"   # or "manage", or "data" (default)
action    = "apply"    # or "plan", or "destroy" (default: apply)
state_dir = "./state"  # optional; overrides the default per-repo state dir

[repo]
visibility             = "private"
description            = "Service for X"
default_branch         = "main"
topics                 = ["go", "service"]
allow_merge_commit     = false
allow_squash_merge     = true
allow_rebase_merge     = false
delete_branch_on_merge = true
auto_init              = true

[ruleset]
name                   = "default-branch-protection"
branch                 = "main"
required_reviews       = 1
require_signed_commits = false

[[ruleset.bypass]]
actor_type = "RepositoryRole"
actor_id   = 5
mode       = "always"

[[environments]]
name                = "production"
wait_timer          = 5
prevent_self_review = true
can_admins_bypass   = false
reviewers_users     = ["octocat", 12345]
reviewers_teams     = ["JMR-dev/release-managers"]
branch_policy       = "custom"
branch_patterns     = ["release/*", "hotfix/*"]

[[environments]]
name = "staging"

[secrets]
repo_file = "./repo.secrets.tfvars"
env_dir   = "./env-secrets"

When mode = "create" all [repo] keys listed above are required — the loader errors with a single line naming the missing field. When mode = "manage", the same keys are required except auto_init / license_template / gitignore_template, which apply only at creation time.

Uploading GitHub Actions secrets

The extension can also upload Actions secrets — both repository-level and per-environment — sourced from KEY = "value" files:

# Repo-level:
cat > repo.secrets.tfvars <<'EOF'
API_TOKEN      = "ghp_..."
WEBHOOK_SECRET = "s3kr3t"
EOF
gh repo-bootstrap JMR-dev/my-app --upload-repo-secrets ./repo.secrets.tfvars

# Per-environment: one <env>.tfvars per env in a directory.
mkdir env-secrets
cat > env-secrets/production.tfvars <<'EOF'
DB_PASSWORD = "prodpw"
EOF
cat > env-secrets/staging.tfvars <<'EOF'
DB_PASSWORD = "stagepw"
EOF
gh repo-bootstrap JMR-dev/my-app \
  --env production --env staging \
  --upload-env-secrets ./env-secrets

Each line in a secrets file must be NAME = "value". Names follow GitHub's rules (alphanumerics + underscore, no leading digit, no GITHUB_ prefix). # and // comments are supported. Values may be double-quoted (with \\ \" \n \r \t escapes) or single-quoted (raw).

Secret values are wrapped in Pulumi secret outputs, so they are encrypted at rest in the state file and elided from --plan output.

State and secret encryption

State is kept per-repo under $XDG_STATE_HOME/gh-repo-bootstrap/<owner>__<repo>/ (default ~/.local/state/gh-repo-bootstrap/...). Override with --state-dir.

Each per-repo directory contains:

  • A Pulumi project (Pulumi.yaml, Pulumi.bootstrap.yaml)
  • The local-backend stack state (encrypted JSON)
  • .passphrase — a chmod 600 file holding an auto-generated passphrase used to encrypt secrets in the state file

Back up the whole state directory, not just the state JSON. If .passphrase is lost, the stack's encrypted secrets cannot be decrypted and the stack will be unusable. You can also override the passphrase by exporting PULUMI_CONFIG_PASSPHRASE before running the command.

Migrating from the OpenTofu-based versions

Previous versions of this extension used OpenTofu. There is no automatic migration: if you previously ran gh repo-bootstrap against a repo, the GitHub ruleset / environments / secrets already exist on GitHub and Pulumi will try to create them again on first run, which can fail or conflict.

To migrate a repo:

  1. Either tear down the previously-managed resources (e.g. delete the ruleset and environments via the GitHub UI or gh api -X DELETE ...) and let Pulumi re-create them, or
  2. Use pulumi import against the local stack to adopt the existing resources without recreating them.

The old OpenTofu state directory ($XDG_STATE_HOME/gh-repo-bootstrap/<owner>__<repo>/terraform.tfstate) is safe to delete once the Pulumi stack is in place.

Hacking

go build ./...
go test ./...

The CLI is a single Go binary that uses the Pulumi Automation API to run an inline program against the pulumi-github provider.

License

MIT

About

`gh` CLI extension to apply standard branch protection and environments to a GitHub repository

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages