cmd / deploy
deploy is a Go CLI that deploys services to Render
from the latest origin/main.
Run from any branch or git worktree:
go run ./cmd/deploy
It reads these environment variables from .env:
RENDER_API_KEYSENTRY_AUTH_TOKEN
The cibot token comes from git (see Recording the deploy).
Flow
- Fetch
origin/main, the latest commit merged by cibot. - Prompt to check Render status for incidents.
- For each service, compare the live commit to
origin/main. If there are new commits, show the log and prompt to deploy. - Deploy the selected services at
origin/main. - Wait for
app-jobsto reachliveso db migrations finish before other services deploy. - Record each service in cibot as it goes live.
- If anything deployed, create a Sentry release.
Example session:
$ go run ./cmd/deploy
From https://cibot.example.com/git/app
* branch main -> FETCH_HEAD
Check https://status.render.com/ for incidents before continuing.
Press any key to continue or ctrl+c to exit...
```
a1b2c3d4e fix session expiry on token refresh
f5e6d7c8b add retry logic to webhook delivery
```
deploy app-jobs? (y/n) y
deploying app-jobs...
waiting for app-jobs deploy dep-abc123 to go live...
app-jobs live
skipping app-web, already up to date...
Services
The script deploys these Render services:
app-jobs(migration gate)app-web
Render API client
The render package (render/client.go) wraps these endpoints:
GET /services/:id/deploys?limit=5: find the live deploy's commit SHAPOST /services/:id/deploys: trigger a deploy at a specific commitGET /services/:id/deploys/:deployID: poll deploy status untillive
WaitForDeploy polls every 10s with a 30 minute timeout. Any terminal
state other than live is an error. A failed migration appears as
pre_deploy_failed.
Recording the deploy
The script POSTs a row to cibot as each
service reaches live, so a failure partway through still records what
shipped. The row carries Render's deploy id.
Render's service names carry the environment (app-production), and
cibot keeps the two in separate columns, so the script translates at
the call site.
The script holds no cibot token. cibot validates the same personal
token for its API and its git transport, so the script asks git for the
one cibot git setup stored for the farmer's host:
in := "protocol=https\nhost=cibot.example.com\n\n"
c := exec.Command("git", "credential", "fill")
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
One credential lives in the Keychain, and cibot names the token holder as the deployer. That token also pushes and merges, so CI should hold a service token.
The script looks the token up first, so a missing token stops the deploy before a service ships unrecorded.
Sentry release
After the deploys, the script calls Sentry's API:
- Create a release (version = short SHA, what the UI shows)
- Set release refs (
repository+ full SHA, so the GitHub integration can read commit metadata) - Record a deploy for
production
This connects a Sentry error to the deploy that introduced it.
The three calls are idempotent and run after every deploy reaches
live. A failed release warns and prints the retry command.
Design
The script reads origin/main, so it runs from any git worktree and
ignores unpushed local commits. origin is cibot's self-hosted git,
and Render builds from the GitHub mirror, so both read the same commit.
app-jobs is the migration gate. The script waits for it to reach
live so its pre-deploy migration finishes
before other services deploy. If I skip it, the script warns that later
services may run against an old schema.
app-web waits too, so a failed web deploy aborts before the Sentry
release. Each service has its own y/n prompt, so I can skip one.