docs: add AGENTS.md for AI coding agents
- commit
- f03e6ca512c5d4e5eb9ae7158fe1b4606e6383ad
- author
- brice · · 2026-03-14 03:51 UTC
- parent
- 265ee4d
- signature
- unsigned
1 file · +161 -0
A AGENTS.md +161 -0
| @@ -0,0 +1,161 @@ | |||
| 1 | + | # AGENTS.md — Gordon | |
| 2 | + | ||
| 3 | + | Guidelines for AI coding agents operating in this repository. | |
| 4 | + | ||
| 5 | + | ## Project | |
| 6 | + | ||
| 7 | + | Gordon is a self-hosted container deployment platform: Docker registry + reverse proxy + auto-deploy. | |
| 8 | + | Module: `github.com/bnema/gordon`. Go 1.25+. Single binary, no external database. | |
| 9 | + | ||
| 10 | + | ## Build & Test Commands | |
| 11 | + | ||
| 12 | + | ```bash | |
| 13 | + | # Build | |
| 14 | + | go build ./... | |
| 15 | + | ||
| 16 | + | # Test — full suite (~1800 tests, 48 packages) | |
| 17 | + | go test ./... | |
| 18 | + | ||
| 19 | + | # Test — single package | |
| 20 | + | go test ./internal/usecase/container/... | |
| 21 | + | ||
| 22 | + | # Test — single test function | |
| 23 | + | go test ./internal/usecase/container/... -run TestService_ListNetworks -v | |
| 24 | + | ||
| 25 | + | # Test — by layer | |
| 26 | + | go test ./internal/adapters/... # adapters only | |
| 27 | + | go test ./internal/usecase/... # use cases only | |
| 28 | + | ||
| 29 | + | # Lint (MUST pass before any commit) | |
| 30 | + | golangci-lint run ./... | |
| 31 | + | ||
| 32 | + | # Generate mocks (after changing boundaries/in or boundaries/out interfaces) | |
| 33 | + | mockery | |
| 34 | + | ``` | |
| 35 | + | ||
| 36 | + | Lint config is in `.golangci.yml` (v2): errcheck, gocyclo (max 15), gosec, govet, staticcheck. | |
| 37 | + | Errcheck is relaxed for `Close`, `Write`, `Publish`, `Stop`, `RemoveContainer`, and zerowrap log methods. | |
| 38 | + | Test files are exempt from gocyclo, errcheck, and gosec. | |
| 39 | + | ||
| 40 | + | ## Architecture — Hexagonal | |
| 41 | + | ||
| 42 | + | ``` | |
| 43 | + | internal/ | |
| 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 | + | ||
| 60 | + | Dependencies flow inward: adapters → boundaries → domain. Never import adapters from domain/usecase. | |
| 61 | + | ||
| 62 | + | ## Code Style | |
| 63 | + | ||
| 64 | + | ### Imports | |
| 65 | + | ||
| 66 | + | Three groups separated by blank lines: stdlib, external, internal. | |
| 67 | + | ||
| 68 | + | ```go | |
| 69 | + | import ( | |
| 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 | + | ||
| 88 | + | Use `zerowrap` (structured zerolog wrapper) in usecase and adapter layers: | |
| 89 | + | ||
| 90 | + | ```go | |
| 91 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 92 | + | zerowrap.FieldLayer: "usecase", | |
| 93 | + | zerowrap.FieldUseCase: "AddRoute", | |
| 94 | + | "domain": route.Domain, | |
| 95 | + | }) | |
| 96 | + | log := zerowrap.FromCtx(ctx) | |
| 97 | + | log.Info().Str("image", route.Image).Msg("route added") | |
| 98 | + | ``` | |
| 99 | + | ||
| 100 | + | CLI 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 | + | ||
| 128 | + | Generated by mockery (config in `.mockery.yaml`). Output: `internal/boundaries/in/mocks/`. | |
| 129 | + | Only 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 | + | ||
| 155 | + | 1. **Run `golangci-lint run ./...` before committing** — CI enforces it. | |
| 156 | + | 2. **gocyclo max 15** — extract helpers to reduce complexity. | |
| 157 | + | 3. **errcheck enabled** — handle or explicitly discard (`_, _ =`) all error returns. | |
| 158 | + | 4. **`AddRoute` is an upsert** — same domain+image = noop, different image = overwrite. No `ErrRouteExists` in practice. | |
| 159 | + | 5. **Never expose Docker IDs or raw labels** in CLI/API output — security hardening. | |
| 160 | + | 6. **Networks are filtered** by both name prefix AND `gordon.managed=true` label. | |
| 161 | + | 7. **Secrets list shows names only** — values are never exposed via CLI or API (by design). | |
| @@ -0,0 +1,161 @@ | |||||
| 1 | + | # AGENTS.md — Gordon | |||
| 2 | + | ||||
| 3 | + | Guidelines for AI coding agents operating in this repository. | |||
| 4 | + | ||||
| 5 | + | ## Project | |||
| 6 | + | ||||
| 7 | + | Gordon is a self-hosted container deployment platform: Docker registry + reverse proxy + auto-deploy. | |||
| 8 | + | Module: `github.com/bnema/gordon`. Go 1.25+. Single binary, no external database. | |||
| 9 | + | ||||
| 10 | + | ## Build & Test Commands | |||
| 11 | + | ||||
| 12 | + | ```bash | |||
| 13 | + | # Build | |||
| 14 | + | go build ./... | |||
| 15 | + | ||||
| 16 | + | # Test — full suite (~1800 tests, 48 packages) | |||
| 17 | + | go test ./... | |||
| 18 | + | ||||
| 19 | + | # Test — single package | |||
| 20 | + | go test ./internal/usecase/container/... | |||
| 21 | + | ||||
| 22 | + | # Test — single test function | |||
| 23 | + | go test ./internal/usecase/container/... -run TestService_ListNetworks -v | |||
| 24 | + | ||||
| 25 | + | # Test — by layer | |||
| 26 | + | go test ./internal/adapters/... # adapters only | |||
| 27 | + | go test ./internal/usecase/... # use cases only | |||
| 28 | + | ||||
| 29 | + | # Lint (MUST pass before any commit) | |||
| 30 | + | golangci-lint run ./... | |||
| 31 | + | ||||
| 32 | + | # Generate mocks (after changing boundaries/in or boundaries/out interfaces) | |||
| 33 | + | mockery | |||
| 34 | + | ``` | |||
| 35 | + | ||||
| 36 | + | Lint config is in `.golangci.yml` (v2): errcheck, gocyclo (max 15), gosec, govet, staticcheck. | |||
| 37 | + | Errcheck is relaxed for `Close`, `Write`, `Publish`, `Stop`, `RemoveContainer`, and zerowrap log methods. | |||
| 38 | + | Test files are exempt from gocyclo, errcheck, and gosec. | |||
| 39 | + | ||||
| 40 | + | ## Architecture — Hexagonal | |||
| 41 | + | ||||
| 42 | + | ``` | |||
| 43 | + | internal/ | |||
| 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 | + | ||||
| 60 | + | Dependencies flow inward: adapters → boundaries → domain. Never import adapters from domain/usecase. | |||
| 61 | + | ||||
| 62 | + | ## Code Style | |||
| 63 | + | ||||
| 64 | + | ### Imports | |||
| 65 | + | ||||
| 66 | + | Three groups separated by blank lines: stdlib, external, internal. | |||
| 67 | + | ||||
| 68 | + | ```go | |||
| 69 | + | import ( | |||
| 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 | + | ||||
| 88 | + | Use `zerowrap` (structured zerolog wrapper) in usecase and adapter layers: | |||
| 89 | + | ||||
| 90 | + | ```go | |||
| 91 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 92 | + | zerowrap.FieldLayer: "usecase", | |||
| 93 | + | zerowrap.FieldUseCase: "AddRoute", | |||
| 94 | + | "domain": route.Domain, | |||
| 95 | + | }) | |||
| 96 | + | log := zerowrap.FromCtx(ctx) | |||
| 97 | + | log.Info().Str("image", route.Image).Msg("route added") | |||
| 98 | + | ``` | |||
| 99 | + | ||||
| 100 | + | CLI 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 | + | ||||
| 128 | + | Generated by mockery (config in `.mockery.yaml`). Output: `internal/boundaries/in/mocks/`. | |||
| 129 | + | Only 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 | + | ||||
| 155 | + | 1. **Run `golangci-lint run ./...` before committing** — CI enforces it. | |||
| 156 | + | 2. **gocyclo max 15** — extract helpers to reduce complexity. | |||
| 157 | + | 3. **errcheck enabled** — handle or explicitly discard (`_, _ =`) all error returns. | |||
| 158 | + | 4. **`AddRoute` is an upsert** — same domain+image = noop, different image = overwrite. No `ErrRouteExists` in practice. | |||
| 159 | + | 5. **Never expose Docker IDs or raw labels** in CLI/API output — security hardening. | |||
| 160 | + | 6. **Networks are filtered** by both name prefix AND `gordon.managed=true` label. | |||
| 161 | + | 7. **Secrets list shows names only** — values are never exposed via CLI or API (by design). | |||