AGENTS.md — Gordon
Guidelines for AI coding agents operating in this repository.
Project
Gordon is a self-hosted container deployment platform: Docker registry + reverse proxy + auto-deploy.
Module: github.com/bnema/gordon. Go 1.25+. Single binary, no external database.
Build & Test Commands
# Build
go build ./...
# Test — full suite (~1800 tests, 48 packages)
go test ./...
# Test — single package
go test ./internal/usecase/container/...
# Test — single test function
go test ./internal/usecase/container/... -run TestService_ListNetworks -v
# Test — by layer
go test ./internal/adapters/... # adapters only
go test ./internal/usecase/... # use cases only
# Lint (MUST pass before any commit)
golangci-lint run ./...
# Generate mocks (after changing boundaries/in or boundaries/out interfaces)
mockery
Lint config is in .golangci.yml (v2): errcheck, gocyclo (max 15), gosec, govet, staticcheck.
Errcheck is relaxed for Close, Write, Publish, Stop, RemoveContainer, and zerowrap log methods.
Test files are exempt from gocyclo, errcheck, and gosec.
Architecture — Hexagonal
internal/
domain/ # Entities, value objects, sentinel errors, labels
boundaries/ # Port interfaces (in = driving, out = driven)
in/ # Service interfaces (ConfigService, ContainerService, etc.)
out/ # Infrastructure interfaces (ContainerRuntime, BlobStorage, etc.)
usecase/ # Business logic (container/, config/, registry/, proxy/, etc.)
adapters/ # Implementations
in/ # Driving adapters
http/admin/ # Admin HTTP API (handler.go)
cli/ # Cobra CLI commands
remote/ # HTTP client for remote Gordon instances
ui/ # Terminal UI components (tables, spinners, styles)
out/ # Driven adapters (docker/, filesystem/, sqlite/)
dto/ # Data transfer objects (request/response structs)
app/ # Application bootstrap (Kernel wiring)
Dependencies flow inward: adapters → boundaries → domain. Never import adapters from domain/usecase.
Code Style
Imports
Three groups separated by blank lines: stdlib, external, internal.
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/bnema/gordon/internal/domain"
)
Error Handling
- Sentinel errors in
internal/domain/errors.go — use errors.New, check with errors.Is.
- Wrap errors with context:
fmt.Errorf("failed to list routes: %w", err).
- Usecase/service methods return
error. Handlers translate to HTTP status codes.
- CLI commands return
error from RunE — cobra prints it.
Logging
Use zerowrap (structured zerolog wrapper) in usecase and adapter layers:
ctx = zerowrap.CtxWithFields(ctx, map[string]any{
zerowrap.FieldLayer: "usecase",
zerowrap.FieldUseCase: "AddRoute",
"domain": route.Domain,
})
log := zerowrap.FromCtx(ctx)
log.Info().Str("image", route.Image).Msg("route added")
CLI commands do NOT use zerowrap — they use cliWriteLine/cliWritef for output.
CLI Commands (Cobra)
- Each command lives in its own file under
internal/adapters/in/cli/.
- Use
cmd.OutOrStdout() for output (testable), never raw fmt.Println.
- Use helpers:
cliWriteLine(out, msg), cliWritef(out, fmt, args...), cliRenderTitle, cliRenderMeta, cliRenderMuted, cliRenderSuccess, cliRenderWarning.
- All list/show commands support
--json via writeJSON(out, v).
- Use
cmd.Context() for context (not context.Background()).
- Commands register in
root.go under groupServer, groupManage, or groupClient.
- Local/remote dispatch via
resolveControlPlane(configPath) or GetRemoteClient().
ControlPlane Pattern
ControlPlane interface (controlplane.go) abstracts local vs remote operations.
controlplane_remote.go — delegates to remote.Client HTTP methods.
controlplane_local.go — calls service interfaces directly.
- Test fakes in
push_test.go — update when adding interface methods.
HTTP Admin Handlers
- Single
Handler struct in handler.go with matchRoute dispatcher.
- Permission checks:
HasAccess(ctx, resource, action) at the top of each handler.
- Response via
h.sendJSON(w, status, payload) or h.sendError(w, status, msg).
- DTOs in
internal/adapters/dto/ with JSON tags.
Mocks
Generated by mockery (config in .mockery.yaml). Output: internal/boundaries/in/mocks/.
Only boundary interfaces are mocked. Run mockery after modifying any interface in boundaries/.
Testing
- Framework:
testing + testify (assert/require).
- Test files:
*_test.go in the same package.
- Use
httptest.NewServer for HTTP handler tests.
- Use mockery-generated mocks for boundary interfaces.
- Hand-rolled fakes for CLI
ControlPlane in test files.
Naming
- Files:
snake_case.go.
- Types:
PascalCase. Interfaces describe capability (ConfigService, ContainerRuntime).
- Functions:
PascalCase exported, camelCase unexported.
- CLI constructors:
newXxxCmd() returns *cobra.Command.
- Runner functions:
runXxx(ctx, ..., out, jsonOut).
- Domain labels: constants in
domain/labels.go prefixed with Label.
Documentation
- CLI docs in
docs/cli/. Every command page has ## Related links at the bottom.
- Docs follow the pattern: header, subcommands table, usage examples, flags table, JSON output example.
Key Rules
- Run
golangci-lint run ./... before committing — CI enforces it.
- gocyclo max 15 — extract helpers to reduce complexity.
- errcheck enabled — always handle all error returns gracefully by wrapping with context (fmt.Errorf) and logging critical errors with zerowrap.
source
1# AGENTS.md — Gordon
2
3Guidelines for AI coding agents operating in this repository.
4
5## Project
6
7Gordon is a self-hosted container deployment platform: Docker registry + reverse proxy + auto-deploy.
8Module: `github.com/bnema/gordon`. Go 1.25+. Single binary, no external database.
9
10## Build & Test Commands
11
12```bash
13# Build
14go build ./...
15
16# Test — full suite (~1800 tests, 48 packages)
17go test ./...
18
19# Test — single package
20go test ./internal/usecase/container/...
21
22# Test — single test function
23go test ./internal/usecase/container/... -run TestService_ListNetworks -v
24
25# Test — by layer
26go test ./internal/adapters/... # adapters only
27go test ./internal/usecase/... # use cases only
28
29# Lint (MUST pass before any commit)
30golangci-lint run ./...
31
32# Generate mocks (after changing boundaries/in or boundaries/out interfaces)
33mockery
34```
35
36Lint config is in `.golangci.yml` (v2): errcheck, gocyclo (max 15), gosec, govet, staticcheck.
37Errcheck is relaxed for `Close`, `Write`, `Publish`, `Stop`, `RemoveContainer`, and zerowrap log methods.
38Test files are exempt from gocyclo, errcheck, and gosec.
39
40## Architecture — Hexagonal
41
42```
43internal/
44 domain/ # Entities, value objects, sentinel errors, labels
45 boundaries/ # Port interfaces (in = driving, out = driven)
46 in/ # Service interfaces (ConfigService, ContainerService, etc.)
47 out/ # Infrastructure interfaces (ContainerRuntime, BlobStorage, etc.)
48 usecase/ # Business logic (container/, config/, registry/, proxy/, etc.)
49 adapters/ # Implementations
50 in/ # Driving adapters
51 http/admin/ # Admin HTTP API (handler.go)
52 cli/ # Cobra CLI commands
53 remote/ # HTTP client for remote Gordon instances
54 ui/ # Terminal UI components (tables, spinners, styles)
55 out/ # Driven adapters (docker/, filesystem/, sqlite/)
56 dto/ # Data transfer objects (request/response structs)
57 app/ # Application bootstrap (Kernel wiring)
58```
59
60Dependencies flow inward: adapters → boundaries → domain. Never import adapters from domain/usecase.
61
62## Code Style
63
64### Imports
65
66Three groups separated by blank lines: stdlib, external, internal.
67
68```go
69import (
70 "context"
71 "fmt"
72
73 "github.com/spf13/cobra"
74
75 "github.com/bnema/gordon/internal/domain"
76)
77```
78
79### Error Handling
80
81- Sentinel errors in `internal/domain/errors.go` — use `errors.New`, check with `errors.Is`.
82- Wrap errors with context: `fmt.Errorf("failed to list routes: %w", err)`.
83- Usecase/service methods return `error`. Handlers translate to HTTP status codes.
84- CLI commands return `error` from `RunE` — cobra prints it.
85
86### Logging
87
88Use `zerowrap` (structured zerolog wrapper) in usecase and adapter layers:
89
90```go
91ctx = zerowrap.CtxWithFields(ctx, map[string]any{
92 zerowrap.FieldLayer: "usecase",
93 zerowrap.FieldUseCase: "AddRoute",
94 "domain": route.Domain,
95})
96log := zerowrap.FromCtx(ctx)
97log.Info().Str("image", route.Image).Msg("route added")
98```
99
100CLI commands do NOT use zerowrap — they use `cliWriteLine`/`cliWritef` for output.
101
102### CLI Commands (Cobra)
103
104- Each command lives in its own file under `internal/adapters/in/cli/`.
105- Use `cmd.OutOrStdout()` for output (testable), never raw `fmt.Println`.
106- Use helpers: `cliWriteLine(out, msg)`, `cliWritef(out, fmt, args...)`, `cliRenderTitle`, `cliRenderMeta`, `cliRenderMuted`, `cliRenderSuccess`, `cliRenderWarning`.
107- All list/show commands support `--json` via `writeJSON(out, v)`.
108- Use `cmd.Context()` for context (not `context.Background()`).
109- Commands register in `root.go` under `groupServer`, `groupManage`, or `groupClient`.
110- Local/remote dispatch via `resolveControlPlane(configPath)` or `GetRemoteClient()`.
111
112### ControlPlane Pattern
113
114`ControlPlane` interface (`controlplane.go`) abstracts local vs remote operations.
115- `controlplane_remote.go` — delegates to `remote.Client` HTTP methods.
116- `controlplane_local.go` — calls service interfaces directly.
117- Test fakes in `push_test.go` — update when adding interface methods.
118
119### HTTP Admin Handlers
120
121- Single `Handler` struct in `handler.go` with `matchRoute` dispatcher.
122- Permission checks: `HasAccess(ctx, resource, action)` at the top of each handler.
123- Response via `h.sendJSON(w, status, payload)` or `h.sendError(w, status, msg)`.
124- DTOs in `internal/adapters/dto/` with JSON tags.
125
126### Mocks
127
128Generated by mockery (config in `.mockery.yaml`). Output: `internal/boundaries/in/mocks/`.
129Only boundary interfaces are mocked. Run `mockery` after modifying any interface in `boundaries/`.
130
131### Testing
132
133- Framework: `testing` + `testify` (assert/require).
134- Test files: `*_test.go` in the same package.
135- Use `httptest.NewServer` for HTTP handler tests.
136- Use mockery-generated mocks for boundary interfaces.
137- Hand-rolled fakes for CLI `ControlPlane` in test files.
138
139### Naming
140
141- Files: `snake_case.go`.
142- Types: `PascalCase`. Interfaces describe capability (`ConfigService`, `ContainerRuntime`).
143- Functions: `PascalCase` exported, `camelCase` unexported.
144- CLI constructors: `newXxxCmd()` returns `*cobra.Command`.
145- Runner functions: `runXxx(ctx, ..., out, jsonOut)`.
146- Domain labels: constants in `domain/labels.go` prefixed with `Label`.
147
148### Documentation
149
150- CLI docs in `docs/cli/`. Every command page has `## Related` links at the bottom.
151- Docs follow the pattern: header, subcommands table, usage examples, flags table, JSON output example.
152
153## Key Rules
154
1551. **Run `golangci-lint run ./...` before committing** — CI enforces it.
1562. **gocyclo max 15** — extract helpers to reduce complexity.
1573. **errcheck enabled** — always handle all error returns gracefully by wrapping with context (fmt.Errorf) and logging critical errors with zerowrap.
158