Adopt golib/pg for migrations and pool construction
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

forgebot applied its schema by executing an inline DDL string on every boot,
with no version tracking and no lock, so two API replicas starting together
raced and the SQL had nowhere to grow. golib/pg already owns that mechanic for
the estate; take it and keep owning the SQL.

- Move the schema into migrations/0001_init.sql, embedded via migrations.FS.
  The DDL is verbatim.
- The four legacy-status UPDATEs move into 0001 unchanged. Each reads only
  retired statuses (pending/failed/running/succeeded/cancelled) and writes only
  current ones, and no current status is a source, so replaying 0001 once
  against the live database is a no-op. A test pins that property.
- Build the pool with pg.NewMigrated, LockName "forgebot-migrations": the
  advisory lock serializes replicas, schema_migrations records what ran, and a
  migration failure fails startup instead of half-migrating. database.New and
  apiserver.New take a context and logger for it.
- Render the DSN with pg.DSN, which percent-escapes the credentials the
  fmt.Sprintf builder pasted in raw. LoadConfig still reads the environment
  itself: pg.DSNFromEnv has no defaults for user and database name, where
  forgebot defaults both to "forgebot", and would newly honour DATABASE_URL and
  PG*. The deployed DBHOST/DBPORT/DBUSER/DBPASS/DBNAME/DBSSL contract and its
  defaults are unchanged, and pinned by a test.
- Plumb GOPRIVATE=git.unkin.net for the first cross-repo Go dependency:
  exported by the Makefile, set in both Dockerfiles and the woodpecker Go
  steps, documented in the README.
- gofmt the four files that were already unformatted on main, so the
  pre-commit step can pass.
This commit is contained in:
2026-09-02 00:16:58 +10:00
parent 40d1a750a7
commit 8c796f4087
21 changed files with 518 additions and 107 deletions
+36
View File
@@ -103,6 +103,26 @@ FORGEBOT_API_URL=http://forgebot-api:8000 ./bin/forgebot-tui
| `POST` | `/api/v1/tasks/{id}/comment` | Post comment to forge |
| `POST` | `/api/v1/webhook/gitea` | Gitea webhook receiver |
## Schema
The SQL lives in `migrations/`, is embedded in the API binary and applied at
startup before the server listens, so there is no mirrored copy in the
deployment to drift out of sync. The runner is
[`golib/pg`](https://git.unkin.net/unkin/golib)'s `pg.NewMigrated` — forgebot
owns the SQL, the shared library owns the mechanics.
Each start takes `pg_advisory_lock` on a fixed key (FNV-1a/64 of the lock name
`forgebot-migrations`), creates `schema_migrations` (`version`, `applied_at`) if
missing, and applies every embedded file whose filename is not yet recorded — in
lexical (version) order, each file's SQL and its tracking row in one transaction
— then unlocks. Replicas starting together queue on the lock and then find
nothing to do.
A file absent from `schema_migrations` is re-run even where the live database
already has the schema, which is how a database created by the pre-`golib`
runner is adopted. Migrations are therefore `IF NOT EXISTS`-guarded and their
data fixups re-runnable.
## CRDs
- **AgentPool** — Configuration for a pool of AI agents (model, concurrency, image, resources)
@@ -121,3 +141,19 @@ make generate # regenerate CRDs and RBAC
make docker-api # build API container image
make docker-operator # build operator container image
```
### `GOPRIVATE`
forgebot depends on `git.unkin.net/unkin/golib`, which is served by Gitea and is
unknown to `proxy.golang.org` / `sum.golang.org`. Module resolution therefore
needs:
```
export GOPRIVATE=git.unkin.net
```
The `Makefile` exports it for every target, and both Dockerfiles and the
woodpecker Go steps set it themselves, so `make build|test|lint` and CI work on
a clean checkout. Only bare `go` commands run outside `make` need it in your
shell — set it there rather than with `go env -w`, which is machine state this
repo cannot carry.