257 lines
8.1 KiB
Markdown
257 lines
8.1 KiB
Markdown
# gh-repo-bootstrap
|
|
|
|
A [`gh` CLI extension](https://docs.github.com/en/github-cli/github-cli/using-github-cli-extensions)
|
|
for applying a standard set of guard-rails to a GitHub repository, powered by
|
|
[Pulumi](https://www.pulumi.com/):
|
|
|
|
- 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
|
|
|
|
```sh
|
|
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`](https://www.pulumi.com/docs/iac/download-install/) 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
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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**:
|
|
|
|
```toml
|
|
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:
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
go build ./...
|
|
go test ./...
|
|
```
|
|
|
|
The CLI is a single Go binary that uses the Pulumi
|
|
[Automation API](https://www.pulumi.com/docs/iac/automation-api/) to
|
|
run an inline program against the
|
|
[`pulumi-github`](https://www.pulumi.com/registry/packages/github/) provider.
|
|
|
|
## License
|
|
|
|
MIT
|