cmd / deploy

deploy is a Go CLI that deploys services to Render from the latest origin/main.

Run it:

go run ./cmd/deploy

It reads these environment variables from .env:

Flow

  1. Fetch origin/main, the latest commit merged by sockeye.
  2. Prompt to check Render status for incidents.
  3. For each service, compare the live commit to origin/main. If there are new commits, show the log and prompt to deploy.
  4. Deploy the selected services at origin/main.
  5. Wait for app-jobs to reach live so its pre-deploy migration finishes before other services deploy.
  6. Record each service in sockeye as it goes live.
  7. If anything deployed, create a Sentry release.

Example session:

$ go run ./cmd/deploy
From https://sockeye.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:

Render API client

The render package (render/client.go) wraps these endpoints:

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 sockeye as each service reaches live, so a failure partway through still records what shipped. The row stores Render's deploy id.

Render's service names include the environment (app-production). sockeye names each target with its environment too (app-web-prod), so the script maps Render's names to sockeye's at the call site.

The script holds no sockeye token. sockeye validates the same personal token for its API and its git transport. So the script asks git for the token that soc login stored for the server's host:

in := "protocol=https\nhost=sockeye.example.com\n\n"
c := exec.Command("git", "credential", "fill")
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")

One credential lives in ~/.config/sockeye/token, and sockeye 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:

  1. Create a release (version = short SHA, what the UI shows)
  2. Set release refs (repository + full SHA, so the GitHub integration can read commit metadata)
  3. 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 branch or git worktree and ignores unpushed local commits. origin is sockeye's self-hosted git, and Render builds from the GitHub mirror, so both read the same commit.

If I skip app-jobs, 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.

← All articles