From code to deploy
The exact path a change takes from your editor to production — branch, PR, CI, merge, and the three things that ship on merge (console + control plane + docs) versus the agent, which ships on a tag. Plus how to run the whole thing locally.
Setu is a Go monorepo (a go.work workspace of five modules) plus a Next.js console. A code change flows through GitHub, gets verified by CI, and — on merge to main — auto-deploys the web + control plane (and docs, when they change). The edge setu runtime is different: it ships from a version tag, not from main.
The pipeline at a glance
Two triggers, two destinations. Push/PR drives verification and the provider-side app deploys· a tag drives the agent release. There is no deploy workflow in the repo for the app — Render and Vercel watch main directly.
Branch → PR → main
Standard trunk flow off main: branch, commit, push, open a PR· CI runs on the PR and again on the merge commit.
git checkout -b feat/incremental-resume
# … edit code …
git add -A && git commit -m "worker: resume staged loads from the last part"
git push -u origin feat/incremental-resume
gh pr create --fill
# CI (ci.yml) runs three jobs — see below.
gh pr merge --squash # once checks are green + reviewedCI — what ci.yml actually gates on
ci.yml runs on every PR and on push to main, with concurrency: cancel-in-progress per ref (a new push cancels the stale run) and least-privilege contents: read. It’s three jobs, not one:
go | Matrix over [engine, apiserver, worker, agent, packs]: go build ./..., go vet ./..., go test ./... -race -count=1. fail-fast: false, so one module failing still reports the rest. |
|---|---|
golangci | A separate matrix running golangci-lint v2.1.6 via install-mode: goinstall — built from source with the job’s Go 1.26 toolchain (the prebuilt binary refuses to lint go 1.26 modules). |
web | Detects web/package.json, then pnpm 11 / Node 24 with a frozen lockfile: pnpm lint, typecheck, test, build. |
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
go:
strategy:
fail-fast: false
matrix: { module: [engine, apiserver, worker, agent, packs] }
steps:
- uses: actions/setup-go@v5
with: { go-version: "1.26" }
- run: cd ${{ matrix.module }} && go build ./...
- run: cd ${{ matrix.module }} && go vet ./...
- run: cd ${{ matrix.module }} && go test ./... -race -count=1
golangci:
strategy:
matrix: { module: [engine, apiserver, worker, agent, packs] }
steps:
- uses: golangci/golangci-lint-action@v7
with: { version: v2.1.6, install-mode: goinstall, working-directory: "${{ matrix.module }}" }main (e.g. a flaky race) still surfaces — the same matrix runs on the merge commit.What runs on merge
App deploys are provider-driven — there is no deploy workflow in the repo for them:
- Control plane. Render watches
main(autoDeploy: trueinrender.yaml), rebuilds the Docker image’s defaultcombinedtarget (apiserver + worker), and rolls it out. The entrypoint starts the apiserver first — it applies the DB + River migrations — and only starts the worker once/healthzpasses·healthCheckPath: /healthzgates the rollout. - Console. Vercel builds and deploys the Next.js app on push to
main, bakingNEXT_PUBLIC_API_BASE_URLat build time. - Docs.
docs.ymlfires only whendocs-site/**(or the workflow itself) changes:next build→pagefind --site outfor search →touch out/.nojekyll→ upload the Pages artifact· a second job withpages: write+id-token: writedeploys it. Third-party actions are pinned to commit SHAs.
Agent releases — tag → GoReleaser → setu-dist
The edge runtime is versioned and released deliberately, not on every merge. Push a semver tag· release.yml runs GoReleaser (pinned to a commit SHA, since tags are mutable), which cross-compiles the setu binary and publishes assets to the public setu-dist repo. The source repo stays private, so curl | sh installs work for anyone.
git tag v0.2.0
git push origin v0.2.0
# release.yml → goreleaser/goreleaser-action (~> v2):
# builds ./agent/cmd/setu for {linux, darwin, windows} × {amd64, arm64}
# ldflags inject the version; windows archives as .zip, others tar
# uploads binaries + archives + checksums.txt to the GitHub Release
# on the PUBLIC Abhishekkumar2021/setu-dist repo| GOWORK=off | The agent is a go.work member but is built standalone against its own go.mod (it vendors the engine via replace ../engine) — so it compiles exactly as an external consumer would. |
|---|---|
| RELEASES_TOKEN | The built-in GITHUB_TOKEN is scoped to the source repo· a fine-grained PAT with contents:write on setu-dist is passed as GITHUB_TOKEN to publish cross-repo. |
| prerelease: auto | A tag ships as a prerelease until vetted — it isn’t marked “latest” automatically. |
| Homebrew | The cask block is present but disabled until the HOMEBREW_TAP_TOKEN secret exists· re-enabled later. |
builds:
- id: setu
dir: agent
main: ./cmd/setu
binary: setu
env: [CGO_ENABLED=0, GOWORK=off]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
ldflags:
- -s -w
- -X github.com/Abhishekkumar2021/setu/agent.Version={{ .Version }}
release:
github: { owner: Abhishekkumar2021, name: setu-dist } # PUBLIC dist repo
prerelease: autoLocal development
Everything runs on one machine. The Go modules are stitched by go.work· the console is pnpm.
go 1.26.4
use (
./agent
./apiserver
./engine
./packs
./worker
)The dev loop
# one-shot: Postgres + blob + apiserver + worker + web
./scripts/dev.sh
# or the self-contained compose box (Postgres + MinIO + everything)
cd deploy/compose
SETU_SECRET_KEY="$(openssl rand -base64 32)" \
docker compose -f docker-compose.selfhost.yml up --buildCodegen & the console
DB access is generated by sqlc from SQL queries — after editing a .sql query or a migration, regenerate the typed Go before building. The console is a normal pnpm app.
# regenerate typed queries after touching SQL (sqlc.yaml at repo root)
sqlc generate
# verify a module exactly like the ci.yml matrix
cd apiserver && go build ./... && go vet ./... && go test ./... -race -count=1
# the console
cd web
pnpm install --frozen-lockfile
pnpm dev # local console against NEXT_PUBLIC_API_BASE_URL
pnpm lint && pnpm typecheck && pnpm test && pnpm buildSETU_TEST_DATABASE_URL. Point it at a throwaway database — never your dev DB.main, PR, and let ci.yml run three jobs — the Go matrix, the golangci matrix, and the web build. Squash-merge to main and Render (control plane, apiserver-then-worker) and Vercel (console) auto-deploy· docs publish to Pages when docs-site/** changes. Ship the edge runtime separately by pushing a v* tag — GoReleaser builds it GOWORK=off and pushes binaries to the public setu-dist repo. Locally, go.work ties the modules together, sqlc generate regenerates DB code, and pnpm runs the console.