version Bash GitHub CLI jq gum bats-core shellcheck
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 symlink
Verify:
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-proj
Note
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.json
Issues 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.json
setup-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-run
gh-toolkit apply-labels lipex360/myproj dev/fixtures/relabel.json
Fixture 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á existir
Template 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 remote
UPDATED: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 criar
Tip
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-project
Warning
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-mvp
Destroys 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-core
Tip
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/*.bats
Install via Homebrew:
brew install shellcheck
The -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.