Standalone GitHub project setup/teardown scripts. Create repos, apply standard labels and project boards, bootstrap milestones and issues from local JSON fixtures, and tear down or nuke the whole thing for re-testing — all from any working directory.
Extracted from pi-cli and adapted to run anywhere via a single dispatcher entrypoint.
- Layout
- Install
- Project root resolution
- Quick start — full setup flow
- Issue body — inline vs body-file
- Apply labels to existing issues
- Release — bump, tag, publish
- Dry-run support
- Reset for re-testing
- Nuke (full destruction)
- Output contract
- Testing
- Linting
- Prerequisites
- Standard labels
- Project board structure
- Source
github-toolkit/
├── install.sh ← installer (symlink into PATH dir)
├── bin/
│ └── gh-toolkit ← single entrypoint (dispatcher; resolves symlinks)
├── scripts/ ← script implementations (12 commands + 1 sourced lib)
│ ├── create-repo.sh
│ ├── setup-labels.sh
│ ├── setup-board.sh
│ ├── create-milestones.sh
│ ├── create-issues.sh
│ ├── bootstrap-issues.sh
│ ├── bootstrap-milestones.sh
│ ├── apply-labels.sh
│ ├── find-project.sh
│ ├── gh-preflight.sh ← sourced library, not standalone
│ ├── teardown-repo.sh
│ ├── nuke-project.sh
│ └── release.sh
├── lib/
│ └── log.sh ← logging library (shared)
├── tests/ ← bats-core test files (*.bats — 265 tests)
├── SKILL.md ← orchestration spec for AI agents
└── README.md
The toolkit ships with install.sh, which symlinks bin/gh-toolkit into a directory on your PATH. Default target is /usr/local/bin (in macOS default PATH — no shell config edit required).
cd ~/www/claude/projects/github-toolkit
./install.sh # → CREATED:link/usr/local/bin/gh-toolkit (sudo if needed)
./install.sh --dry-run # preview without changes
./install.sh --target-dir ~/bin # user-local target (no sudo, must be in PATH)
./install.sh --force # replace stale link pointing elsewhere
./install.sh --uninstall # remove the symlinkVerify:
gh-toolkit --list # shows 10 commandsinstall.sh is idempotent — safe to re-run. Output uses the same CREATED/EXISTS/REPLACED/DELETED/SKIPPED/DRY_RUN/FAILED contract as the rest of the toolkit.
Note
If you don't want a symlink, run via absolute path: ~/www/claude/projects/github-toolkit/bin/gh-toolkit <command>. Or add bin/ to PATH in your shell config — both work.
Optional alias for shorter typing:
alias ght='gh-toolkit'Scripts produce/consume local artifacts in <project-root>/:
dev/fixtures/milestones.json,dev/fixtures/bootstrap-issues.json— bootstrap outputdev/fixtures/<slug>/,dev/checklist-<slug>.md— created/destroyed bystart-new-projectflow.logs/scripts/<name>.log— script logs (stderr + file).config/settings.json— optional, read bygh-preflightfor rate-limit threshold
Resolution order:
$GH_PROJECT_ROOTif set$(pwd)(current working directory)
# Default — runs from inside your project directory
cd ~/www/myproj
gh-toolkit create-repo lipex360/myproj --private
# Override — operate on another project from anywhere
GH_PROJECT_ROOT=~/www/other-proj gh-toolkit setup-labels lipex360/other-projNote
GitHub-only operations (create-repo, setup-labels, setup-board, create-milestones, create-issues, teardown-repo) work regardless of cwd — <owner/repo> is always explicit and gh is global. Only the local-artifact steps care about PROJECT_ROOT.
cd ~/www/myproj # or set GH_PROJECT_ROOT
# Step 0 — build local fixtures
gh-toolkit bootstrap-milestones --init
gh-toolkit bootstrap-milestones "Backlog" --description "deferred work"
gh-toolkit bootstrap-milestones "MVP" --description "first release" --due "2026-08-01"
gh-toolkit bootstrap-issues --init
gh-toolkit bootstrap-issues "Setup auth" --labels "feat,P1,M" --body "implement OAuth login flow" --milestone "MVP"
gh-toolkit bootstrap-issues "Investigate idle bug" --labels "fix,P2,S" --body-file dev/issue-bodies/idle-investigation.md --milestone "Backlog"
# Step 1 — create repo
gh-toolkit create-repo lipex360/myproj --private --description "..."
# Step 2 — labels, board, milestones (any order)
gh-toolkit setup-labels lipex360/myproj
gh-toolkit setup-board lipex360/myproj
gh-toolkit create-milestones lipex360/myproj dev/fixtures/milestones.json
# Step 3 — issues (filename created by bootstrap-issues)
gh-toolkit create-issues lipex360/myproj dev/fixtures/bootstrap-issues.jsonIssues aceitam corpo (markdown) de duas formas, mutuamente exclusivas:
| Forma | Quando usar | Como passar |
|---|---|---|
Inline (--body "...") |
Issues curtas: uma frase, descrição simples, link único. | gh-toolkit bootstrap-issues "title" --labels "..." --body "implementar X" |
Body-file (--body-file <path>) |
Issues longas: checklists, code blocks, múltiplas seções, RFCs, especificações detalhadas. | gh-toolkit bootstrap-issues "title" --labels "..." --body-file dev/issue-bodies/foo.md |
Regras:
- Se ambos forem passados → falha com
FAILED:issue/<title>:--body and --body-file are mutually exclusive. - O caminho do body-file é registrado no JSON como
body_filee só lido no momento decreate-issues. Se o arquivo for renomeado/deletado entre o bootstrap e o create, a issue falha comFAILED:issue/<title>:body_file not found:<path>. - Caminhos relativos são resolvidos contra
PROJECT_ROOT. Use absolutos se preferir não depender decwd.
<project-root>/
dev/
issue-bodies/
auth-setup.md
db-migration.md
idle-investigation.md
fixtures/
bootstrap-issues.json ← referencia "body_file": "dev/issue-bodies/auth-setup.md"
milestones.json
Você pode também montar dev/fixtures/bootstrap-issues.json direto, sem usar bootstrap-issues:
[
{
"title": "Setup auth",
"body": "implement OAuth login flow",
"labels": ["feat", "P1", "M"],
"milestone": "MVP"
},
{
"title": "Migrate DB schema",
"body_file": "dev/issue-bodies/db-migration.md",
"labels": ["feat", "P1", "L"],
"milestone": "MVP"
}
]create-issues aceita esse JSON direto:
gh-toolkit create-issues lipex360/myproj dev/fixtures/bootstrap-issues.jsonsetup-labels cria as definições das 14 labels padrão no repo. create-issues aplica labels na criação. Para issues que já existem (criadas via UI, gh issue create, ou importadas), use apply-labels.
Dois modos:
# Adicionar
gh-toolkit apply-labels lipex360/myproj 12 --add "feat,P1"
# Remover
gh-toolkit apply-labels lipex360/myproj 12 --remove "chore"
# Combinar
gh-toolkit apply-labels lipex360/myproj 12 --add "feat,P1" --remove "chore"
# Preview
gh-toolkit apply-labels lipex360/myproj 12 --add "feat,P1" --dry-rungh-toolkit apply-labels lipex360/myproj dev/fixtures/relabel.jsonFixture format:
[
{"number": 12, "add": ["feat", "P1"], "remove": ["chore"]},
{"number": 15, "add": ["fix", "M"]},
{"number": 22, "remove": ["wontfix"]}
]UPDATED:issue/12:+feat,+P1,-chore # mudou
SKIPPED:issue/15:already applied # nada a fazer (idempotente)
DRY_RUN:issue/22:+fix,+M # com --dry-run
FAILED:issue/30:label not found in repo: ghost-label
- Idempotente: lê labels atuais via
gh issue view, calcula delta, só chamagh issue editquando há mudança real. Re-rodar é seguro. - Validação upfront: o script faz uma chamada
gh label listpara o repo e valida que todas as labels do--add/--removeexistem antes de tentar editar. Label não cadastrada → entrada falha sem chamarissue edit. - Continuação em erro (JSON mode): uma entrada inválida não impede as outras. Exit code reflete se houve qualquer falha (1) ou tudo OK (0).
Tip
Se você renomeou ou adicionou uma label nova depois que issues já existiam, apply-labels é o caminho. Combine com gh issue list --json number,labels + jq para gerar a fixture programaticamente.
Project-agnostic. Funciona em qualquer projeto que use CHANGELOG.md como source-of-truth da versão. Lê o topo do CHANGELOG (## [vX.Y.Z] ou ## [X.Y.Z]), atualiza manifests detectados automaticamente, commita, dá push, e opcionalmente cria tag + GitHub Release.
Para projetos novos sem CHANGELOG.md:
gh-toolkit release --init # cria CHANGELOG.md com template v0.1.0
gh-toolkit release --init --dry-run # preview do conteúdo
gh-toolkit release --init --force # sobrescreve se já existirTemplate gerado segue Keep a Changelog com entrada inicial ## [v0.1.0] — <today>. Após gerado, o arquivo já é parseable por release (sem --init) — o próximo gh-toolkit release lê v0.1.0 e bumpa manifests.
--init é mutuamente exclusivo com --publish (é bootstrap, não release).
# Bump + commit + push (sem tag, sem release)
gh-toolkit release
# Bump + commit + push + tag + GitHub Release
gh-toolkit release --publish
# Preview de tudo (zero changes)
gh-toolkit release --dry-run [--publish]O script atualiza estes arquivos quando existem (ignora silenciosamente se não):
| Arquivo | Pattern atualizado |
|---|---|
package.json |
"version": "..." no top-level |
pyproject.toml |
version = "..." no início (qualquer seção [project]/[tool.poetry]) |
Cargo.toml |
version = "..." apenas em [package] (deixa [dependencies] intactas) |
VERSION |
conteúdo do arquivo (single-line) |
README.md |
badge shields.io/badge/version-vX-<color> |
gh-toolkit release --changelog HISTORY.md # CHANGELOG em outro path
gh-toolkit release --no-readme-badge # pula update do badge
gh-toolkit release --no-push # commita mas não dá push
gh-toolkit release --prefix "release-" # tag = "release-1.2.3" em vez de "v1.2.3"
gh-toolkit release --remote upstream # push para outro remoteUPDATED:file/package.json:0.1.0→0.2.0
UPDATED:file/README.md:badge 0.1.0→0.2.0
SKIPPED:file/pyproject.toml:up to date (0.2.0)
CREATED:commit/abc1234
PUSHED:remote/origin/main
CREATED:tag/v0.2.0
PUSHED:tag/v0.2.0
CREATED:release/v0.2.0
# 1. Editar CHANGELOG.md adicionando ## [v0.2.0] no topo com as notes
$EDITOR CHANGELOG.md
git add CHANGELOG.md && git commit -m "docs: changelog v0.2.0"
# 2. Preview do release
gh-toolkit release --dry-run --publish
# 3. Se OK, executar
gh-toolkit release --publish- Idempotente: manifest já na versão correta →
SKIPPED:up to date. Re-rodar é seguro. - Sem mexer onde não deve:
Cargo.tomlsó atualiza[package].version;[dependencies]ficam intactas. - Short-circuit anti-erro: se a versão do topo do CHANGELOG já é uma tag git local → emite
EXISTS:tag/<tag>:already released — bump CHANGELOG to release a new versione sai com exit 0 antes de tocar em manifests, commit ou push. Pega o erro mais comum: rodarreleasedepois de commit docs-only sem ter atualizado o CHANGELOG. A correção é adicionar uma nova entrada no topo e rodar de novo. - Notes vêm do CHANGELOG: o conteúdo entre o header da versão e o próximo
## [é usado como release notes.
Warning
release faz git add -A + commit antes do tag/push. Garanta que o working tree esteja limpo do que você não quer comitar — em particular arquivos não relacionados ao bump. Use git status antes.
Todos os comandos com efeito remoto ou destrutivo aceitam --dry-run. O flag faz o script imprimir as linhas que seriam emitidas (DRY_RUN:<type>/<name>) sem executar nenhuma chamada gh ou alteração local.
| Command | --dry-run? |
Comentário |
|---|---|---|
install |
✓ | preview do symlink |
create-repo |
✓ | preview da criação remota |
setup-labels |
✓ | lista labels que seriam criadas |
setup-board |
✓ | preview do project board + colunas + fields |
create-milestones |
✓ | preview por milestone |
create-issues |
✓ | preview por issue (resolve body_file na hora) |
apply-labels |
✓ | preview do delta (+a,-b) por issue |
release |
✓ | preview de bump+commit+push (+ tag+release com --publish) |
find-project |
✓ | já é read-only; flag preserva contrato |
teardown-repo |
✓ | lista o que seria deletado |
nuke-project |
✓ | preview da destruição completa (recomendado antes do real) |
bootstrap-issues |
— | apenas escreve JSON local; sem efeito remoto |
bootstrap-milestones |
— | mesmo motivo |
Padrão para validar antes de aplicar:
gh-toolkit nuke-project lipex360/myproj --dry-run # verificar o que seria destruído
gh-toolkit nuke-project lipex360/myproj --no-confirm # então executar real
./install.sh --dry-run # preview do symlink
./install.sh # então criarTip
Em nuke-project, o --dry-run é especialmente importante porque o comando deleta repo + branches + arquivos locais em sequência. Sempre rode dry-run primeiro.
# Wipe issues/milestones/labels (keeps repo + project board link)
gh-toolkit teardown-repo lipex360/myproj
# Wipe everything including the project board itself
gh-toolkit teardown-repo lipex360/myproj --delete-projectWarning
nuke-project is destructive and irreversible. It deletes the GitHub repository, both branches, and local fixture/checklist files. Always start with --dry-run.
Use only when abandoning a project. Requires gum (brew install gum).
# Preview what would be destroyed (no changes)
gh-toolkit nuke-project lipex360/myproj --dry-run
# Destroy with interactive confirmation
gh-toolkit nuke-project lipex360/myproj
# Destroy without confirmation (CI / scripted)
gh-toolkit nuke-project lipex360/myproj --no-confirm
# Destroy remote only — keep local branch and dev/ files
gh-toolkit nuke-project lipex360/myproj --keep-local
# Slug differs from repo name
gh-toolkit nuke-project lipex360/myproj --slug myproject-mvpDestroys in this order:
- Issues, milestones, labels, project board (via
teardown-repo --delete-project) - GitHub repository (
gh repo delete) - Remote branch
feat/<slug> - Local branch
feat/<slug>(in currentcwd) dev/checklist-<slug>.mdanddev/fixtures/<slug>/(inPROJECT_ROOT)
All scripts emit machine-parseable lines on stdout:
| Line | Meaning |
|---|---|
CREATED:<type>/<name> |
resource created |
EXISTS:<type>/<name> |
already existed (idempotent) |
DELETED:<type>/<name> |
resource destroyed |
SKIPPED:<type>/<name>:<reason> |
not found, nothing to do |
DRY_RUN:<type>/<name> |
preview, no change |
FAILED:<type>/<name>:<msg> |
error |
GH_NOT_AUTH |
run gh auth login |
REPO_NOT_FOUND:<repo> |
repo missing — earlier step failed |
GH_RATE_LIMITED:remaining=N:threshold=N:resets_at=HH:MM:SS |
rate limit too low |
Logs (stderr + file) go to <project-root>/.logs/scripts/<name>.log.
Suite de testes em bats-core. 265 tests covering all scripts and install.sh.
# Run full suite
bats tests/
# Run single file
bats tests/create-repo.bats
# Parallel (requires GNU parallel)
bats --jobs 4 tests/Install bats via Homebrew:
brew install bats-coreTip
Tests use mocked gh binaries via PATH injection — no GitHub API calls happen during the suite. Integration tests against real gh are gated behind lipex360x/nonexistent-repo-* to verify error paths only.
Static analysis via shellcheck. 0 issues across scripts, lib, bin, and tests.
shellcheck -x install.sh scripts/*.sh lib/*.sh bin/gh-toolkit tests/*.batsInstall via Homebrew:
brew install shellcheckThe -x flag follows # shellcheck source=... directives so sourced files are analyzed in context.
| Tool | Used by | Install |
|---|---|---|
gh (authenticated) |
all scripts that touch GitHub | brew install gh && gh auth login |
jq |
bootstrap-, create-, gh-preflight | brew install jq |
gum |
nuke-project (interactive confirm) | brew install gum |
bash 4+ |
all | macOS default 3.2 works for these scripts |
bats-core (dev) |
running the test suite | brew install bats-core |
shellcheck (dev) |
linting | brew install shellcheck |
Applied by setup-labels:
- Priority:
P0,P1,P2 - Size:
XS,S,M,L,XL - Type:
feat,fix,chore,refactor,docs,test
- Board name:
Board - Columns (8):
Backlog,Ready,In Progress,In Review,Blocked,Done,Won't Do,Cancelled - Custom fields (2): see
setup-board.shfor current schema
Originally part of pi-cli. This is a copy adapted to be invoked from any working directory. Bug fixes should be ported back upstream.