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 requires these environment variables (loaded from .env):
RENDER_API_KEYSENTRY_AUTH_TOKEN
The cibot token comes from git instead, as Recording the deploy describes.
Flow
- Fetch
origin/mainto get the latest commit merged by cibot. - Prompt to check Render status for incidents.
- For each service, compare the live deploy commit to
origin/main. If there are new commits, show the log and prompt to deploy. - Deploy selected services at
origin/main. - Wait for
app-jobsto reachliveso db migrations complete before deploying other services. - Record each service in cibot as it goes live.
- If anything deployed, create and tag 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 multiple 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 (build_failed, update_failed,
pre_deploy_failed, deactivated, canceled) is an error. A failed
migration surfaces as pre_deploy_failed.
Recording the deploy
Each service POSTs a row to cibot as it
reaches live, inside the loop rather than once at the end, so a
failure partway through still records what shipped. The row carries
Render's deploy id, which is the way back to its console.
Render's service names carry the environment (app-production).
cibot keeps service and environment in separate columns, so the script
translates: (app-web, production), (app-jobs, production). Renaming
the Render services would mean moving DNS, since a custom domain is a
CNAME to <service>.onrender.com. Translating at the call site costs
one line and no downtime.
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 already 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, in the Keychain rather than a dotfile, rotated in one place. cibot attributes the deploy to the token holder, so the row names the person who ran it. That token also pushes and merges, which is the trade for reusing it: good for an operator at a terminal, wrong for CI, which should hold a service token instead.
The lookup happens up front, with the other credentials. A token git does not hold should stop the deploy before it starts, not after something has shipped unrecorded.
Sentry release (API, not CLI)
After deploying, the script calls Sentry's API:
- Create release (version = short SHA, what the UI shows)
- Set release refs (
repository+ full SHA, so the GitHub integration can pull commit metadata) - Record deploy for
production
This connects Sentry errors to the deploy that introduced them.
The three calls are idempotent, and the release runs last, after every
deploy is live. A failed release only warns and prints the retry
command: the deploys already succeeded.
Design
The script uses origin/main. This makes it work from any git worktree and
ignores local main commits that haven't been pushed yet.
origin is cibot's self-hosted git, which is where a change merges.
Render builds from the GitHub mirror the merge pushes to, so both read
the same commit.
app-jobs is the migration gate: waiting for it to reach live confirms
its pre-deploy db migration step finished before other services.
Skipping it prints a warning that later services may run against an old schema.
Service order matters, and app-web waits too, so a failed web deploy
aborts before the Sentry release is tagged.
Each service is deployed independently with a y/n prompt, so you can skip a service if it has unrelated changes or you want to deploy incrementally.