8c796f4087
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.
160 lines
5.1 KiB
Markdown
160 lines
5.1 KiB
Markdown
# forgebot
|
|
|
|
K8s operator + API for AI agent dispatch from git forges.
|
|
|
|
## Architecture
|
|
|
|
- **API server** (`cmd/api`) — REST API backed by PostgreSQL. Receives webhooks from Gitea, manages task lifecycle, and orchestrates the review workflow.
|
|
- **Operator** (`cmd/operator`) — Kubernetes controller that watches for pending tasks, creates Jobs via AgentPool/AgentTask CRDs, and reports completion back to the API.
|
|
- **TUI** (`cmd/tui`) — Terminal kanban board for viewing and managing tasks.
|
|
|
|
## Task Lifecycle
|
|
|
|
Tasks follow a kanban workflow with automated review:
|
|
|
|
```
|
|
+-----------+
|
|
| Todo |
|
|
+-----+-----+
|
|
|
|
|
agent picks up
|
|
|
|
|
+-----v-------+
|
|
| In Progress |
|
|
+-----+-------+
|
|
|
|
|
agent completes
|
|
|
|
|
+-----------+-----------+
|
|
| |
|
|
auto-create error?
|
|
review task back to Todo
|
|
|
|
|
+-----v------+
|
|
| In Review |<-----+
|
|
+-----+------+ |
|
|
| |
|
|
human decision reviewer
|
|
| suggests
|
|
+----+----+ changes
|
|
| | |
|
|
+----v--+ +---v----+ |
|
|
| Done | | Wontdo | |
|
|
+-------+ +--------+ |
|
|
|
|
|
(new fix task in Todo)
|
|
```
|
|
|
|
Only humans can move tasks from In Review to Done/Wontdo.
|
|
|
|
## Quick Start
|
|
|
|
### API Server
|
|
|
|
```bash
|
|
export DBHOST=localhost DBUSER=forgebot DBPASS=secret DBNAME=forgebot
|
|
export GITEA_URL=https://git.unkin.net GITEA_TOKEN=<token>
|
|
make build
|
|
./bin/forgebot-api
|
|
```
|
|
|
|
### Operator
|
|
|
|
```bash
|
|
./bin/forgebot-operator --api-url http://forgebot-api:8000
|
|
# or
|
|
FORGEBOT_API_URL=http://forgebot-api:8000 ./bin/forgebot-operator
|
|
```
|
|
|
|
### TUI
|
|
|
|
```bash
|
|
./bin/forgebot-tui -api http://localhost:8000
|
|
# or
|
|
FORGEBOT_API_URL=http://forgebot-api:8000 ./bin/forgebot-tui
|
|
```
|
|
|
|
#### TUI Key Bindings
|
|
|
|
| Key | Action |
|
|
|-----|--------|
|
|
| `h`/`l` or arrows | Move between columns |
|
|
| `j`/`k` or arrows | Move within column |
|
|
| `Enter` | Task detail view |
|
|
| `e` | Edit task in $EDITOR |
|
|
| `n` | Create new task |
|
|
| `d` | Mark done (in review only) |
|
|
| `w` | Mark wontdo (in review only) |
|
|
| `r` | Refresh |
|
|
| `/` | Filter by repository |
|
|
| `?` | Toggle help |
|
|
| `q` | Quit |
|
|
|
|
## API
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| `GET` | `/health` | Health check |
|
|
| `GET` | `/api/v1/tasks` | List tasks (`?status=`, `?repository=`) |
|
|
| `POST` | `/api/v1/tasks` | Create task |
|
|
| `GET` | `/api/v1/tasks/{id}` | Get task |
|
|
| `PATCH` | `/api/v1/tasks/{id}` | Update task status |
|
|
| `POST` | `/api/v1/tasks/{id}/complete` | Agent completion callback (triggers review workflow) |
|
|
| `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)
|
|
- **AgentTask** — A task dispatched to a pool for execution
|
|
- **ProviderQueue** — Polls the API for pending tasks and creates AgentTask CRs
|
|
- **RepositoryBinding** — Links a repository to a queue and pool with access controls
|
|
|
|
## Building
|
|
|
|
```bash
|
|
make build # all binaries to bin/
|
|
make test # run tests
|
|
make lint # go vet
|
|
make fmt # gofmt
|
|
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.
|