Create and manage GitHub resources from
swamp using the GitHub REST API
(Octokit). Where read-only GitHub extensions stop at listing, this model owns the
mutations GoodCraft needs to publish its swamp extensions: it creates
repositories, cuts releases, and opens pull requests. Every mutation is
idempotent — it detects existing state first and defaults to dryRun: true,
so a run plans before it writes.
The API token is supplied through globalArguments.token, wired to a vault
expression at model-creation time — never a literal token. Set isOrg: true when
owner is an organization.
swamp extension pull @goodcraft/githubThis model authenticates with a GitHub personal access token (PAT). Create one, store it in a vault, and reference it from there — never paste the token into a command or a model file.
Fine-grained token (recommended — least privilege):
-
Go to https://github.com/settings/personal-access-tokens/new (GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token).
-
Token name: e.g.
swamp-github. Resource owner: your account. Expiration: your choice. -
Repository access → All repositories. Required: creating a brand-new repository can't be scoped to a repo that doesn't exist yet.
-
Permissions → Repository permissions — set each of these (leave everything else at "No access"):
Permission Access Used by Administration Read and write ensureRepo— create repo + set topicsContents Read and write ensureRelease— releases & tagsPull requests Read and write openPr— create pull requestsMetadata Read (auto-selected) sync+ repo existence checks -
Generate token and copy it (GitHub shows it once).
Classic token (simpler alternative):
- Go to https://github.com/settings/tokens → Generate new token (classic).
- Select the
reposcope — it covers creating repos, releases, and PRs for both public and private repos. (For public repos only,public_repois enough.) - Generate token and copy it.
Keep the token in a vault and reference it live, so it never lands in a file or a
log. Example with the @swamp/1password vault:
# One-time: create the vault (points at a 1Password vault)
swamp vault create @swamp/1password op-secrets --config '{"op_vault":"Personal"}'
# Store the token (or add it to the 1Password item directly)
swamp vault put op-secrets GitHub/PAT "<paste-your-token>"You then reference it as ${{ vault.get(op-secrets, "GitHub/PAT") }} when
creating the model (next section).
# Create the model with the token wired to a vault (never inline).
# owner = your GitHub user or org; omit isOrg for a personal account (set isOrg=true for an org).
swamp model create @goodcraft/github gh \
--global-arg token='${{ vault.get(op-secrets, "GitHub/PAT") }}' \
--global-arg owner=your-github-user
# Read-only: list the owner's repos (proves the token)
swamp model method run gh sync
# Idempotently ensure a public repo exists — dryRun defaults true, so plan first…
swamp model method run gh ensureRepo \
--input name=swamp-forge-provision \
--input description='Laravel Forge provisioning model for swamp' \
--input private=false
# …then create it for real
swamp model method run gh ensureRepo --input name=swamp-forge-provision --input dryRun=false
# Cut a release (tag) — idempotent on tag name
swamp model method run gh ensureRelease \
--input repo=swamp-forge-provision --input tagName=2026.06.14.1 --input dryRun=false
# Open a pull request — idempotent on head→base
swamp model method run gh openPr \
--input repo=swamp-forge-provision --input head=changes --input base=main \
--input title='Update provisioning' --input dryRun=falsesync— list the repositories owned byowner(read-only).ensureRepo— create a repository if it doesn't exist (name, description, visibility, auto-init, license/gitignore templates, homepage, topics).ensureRelease— create a release + tag if one with the tag isn't present.openPr— open a pull request if no open PR for the same head→base exists.
Each method builds an Octokit client from the token and calls the REST API
directly. The idempotency and payload-shaping logic (mapping arguments to the
REST create payload, matching existing releases by tag, matching open PRs by
head+base) lives in _lib/github_plan.ts as pure, zod-free functions with
colocated unit tests, so the methods stay thin. ensureRepo distinguishes a real
404 (repo absent → create) from other API failures, and the mutating methods
default to dryRun: true so you always see the plan before anything is written.
MIT — see LICENSE.txt.