Code conventions and implementation contracts
Code conventions and implementation contracts
Section titled “Code conventions and implementation contracts”Status: Authoritative engineering convention reference.
This document owns durable code-level contracts for gondolier. AGENTS.md
keeps a short checklist; this file carries the details future agents and
reviewers should enforce.
Go-specific conventions
Section titled “Go-specific conventions”-
Module path:
git.rbtr.dev/laputacloudco/gondolier. All imports use this prefix. -
Package naming: short, lowercase, single-word package names. No underscores, no camelCase.
-
Export policy: only export what the public API needs. Internal helpers go in unexported files or in the same package.
-
Error handling: explicit error checks on every error-producing call. No swallowed errors. Use
%wfor wrapping errors that callers might inspect witherrors.Is. Return sentinel errors for expected failure modes. -
Context propagation: every external call (database, forge API, HTTP, KMS) must accept and respect a
context.Context. Pass request-scoped context through the entire call chain. -
Logging: structured logging with consistent keys. Never log raw tenant tokens, OAuth secrets, or decrypted credential blobs. Use
<REDACTED>as a placeholder when a credential field is relevant for debugging.log.Error().Str("tenant_id", t.ID).Str("token_preview", "<REDACTED>").Msg("forge API call failed") -
Configuration: use env vars or a config file for deployment settings. No hardcoded secrets. Configuration loading must validate and fail fast on missing required values.
Encryption and credentials
Section titled “Encryption and credentials”- Envelope encryption: tenant forge tokens are envelope-encrypted with a
per-tenant DEK, which is itself encrypted with a KMS-backed master key.
See
pkg/crypto/for the implementation contract. - Never log plaintext: audit every
log.*,fmt.*,slog.*, andzap.*call in any diff touching credentials. Thegoseclinter can help catch obvious patterns, but manual review is required. - Decryption scope: a decrypted token lives in memory only for the duration of one worker tick. Clear it from variables and slice buffers when done.
- Credential rotation: rotating a tenant’s token must atomically replace the old encrypted blob. No window where both old and new tokens are valid.
Data model and database
Section titled “Data model and database”- Migrations: all database changes go through versioned migration files in
migrations/. Migrations must be forward-compatible — new code must handle both old and new schema. - Null safety: use nullable types for columns that can be NULL. Do not use zero values to represent missing data.
- Transactions: wrap multi-row, multi-table changes in transactions. Use
explicit
BEGIN,COMMIT, andROLLBACK. - Connection pooling: configure
MaxOpenConns,MaxIdleConns,ConnMaxLifetime, andConnMaxIdleTimeon every database connection. Never use zero values.
API design
Section titled “API design”-
RESTful paths: resource-oriented URLs (
/tenants,/tenants/:id/connections,/tenants/:id/repos). No verbs in paths. -
Request/Response: all API responses have a consistent envelope:
{"status": "ok|error", "data": {...}, "error": {"code": "...", "message": "..."}} -
Pagination: all list endpoints support
?page=1&limit=50. Default limit is 50, max is 100. -
Rate limiting: per-tenant rate limiting on API endpoints. Use a sliding window algorithm.
-
Idempotency: mutation endpoints accept an
Idempotency-Keyheader. Responses for the same key are cached and returned without re-executing.
Multi-tenant isolation
Section titled “Multi-tenant isolation”- Tenant context: every request must carry a tenant context. The context propagates through all database queries, forge API calls, and internal calls. Verify tenant scope on every database operation.
- Worker isolation: a worker handles one tenant at a time. It must not hold another tenant’s decrypted token in memory.
- Error isolation: errors from one tenant’s operations must not affect other tenants. Wrap tenant-scoped errors in a tenant-scoped context.
Forgejo API interactions
Section titled “Forgejo API interactions”- Token scope: tokens are scoped to the minimum required permissions. On
Gitea 1.23+, use granular OAuth2 scopes (
read:repository,write:repository). On Forgejo, fall back to PAT with repo-scoped permissions. - Rate limit awareness: each forge instance has its own rate limit. Implement per-instance rate limiting with backoff on 429/5xx.
- Timeline-based auto-merge detection: shunt’s merge queue works by detecting
Forgejo’s
merge_when_checks_succeedbehavior. Do not rely on Forgejo events being delivered in a specific order. - Branch protection: shunt enforces merge queue policy through Forgejo branch
protection rules (required status + push allow-list). The
merge-queuestatus is written by the shunt gate workflow, not by this service.
Testing
Section titled “Testing”-
Table-driven tests: all Go tests use table-driven test style.
func TestSomething(t *testing.T) {tests := []struct{name stringinput stringwant intwantErr bool}{{"valid", "hello", 5, false},{"invalid", "", 0, true},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {// ...})}} -
Integration tests: database and forge API integration tests use test containers or a dedicated test database. Integration tests live in
tests/integration/. -
Mock interfaces: external dependencies (database, forge API, KMS) are accessed through interfaces. Tests use mock implementations.
-
Test coverage: aim for high coverage on business logic and edge cases. Integration tests are expected to cover critical paths.
Logging and observability
Section titled “Logging and observability”- Structured logging: use a structured logger (zap, slog, or zerolog) with
consistent fields:
tenant_id,repo,action,duration_ms. - Audit trail: all credential access, tenant changes, and forge API calls are logged for audit. Audit logs must not contain raw credentials.
- Metrics: expose basic metrics (requests served, errors, queue depth) via
an HTTP metrics endpoint at
/metrics. - Health checks: expose
/healthz(ready) and/readyz(healthy) endpoints.
CI workflow conventions
Section titled “CI workflow conventions”- CI must not skip the
mq/**gate: themq/**push trigger has nopaths-ignore. Forgejo reports an empty changed-file set for newly created queue branches, so anypaths-ignorewould suppress the gate and strand the queue. - Workflow naming: CI workflows live in
.forgejo/workflows/. The main CI isci.yaml. The merge queue gate isshunt-merge-queue.yaml. - OpenID Connect: workflows use
enable-openid-connect: truefor keyless authentication to OpenBao. Do not add long-lived secrets to workflow files.
Productionization SOP: documentation and observability
Section titled “Productionization SOP: documentation and observability”A feature is not production-ready until it is both documented and observable.
- Documentation (design + implementation). Any non-trivial feature or system
gets a durable doc that explains the design and the implementation, so the
next engineer learns the model instead of reverse-engineering it. Keep
AGENTS.mdshort and link the focused doc. - Observability (instrumentation / telemetry). Ship signals that make the feature’s behavior visible: primary action, success, failure/empty, and for safety-critical paths the protective action taken.
- PR body. State
Docs: <paths>(orDocs: N/A) andTelemetry: <events>(orTelemetry: N/A).