Imported from vancanhuit/devopsbin (
AGENTS.md). Install upstream withnpx skills add vancanhuit/devopsbin. Copyright stays with the author.
Development Guidelines
Use mise tasks consistently:
# Frontend
mise run web:format:check
mise run web:format
mise run web:lint
mise run web:check
mise run web:test
mise run web:build
# Backend
mise run api:generate
mise run api:sqlc
mise run api:format
mise run api:vulncheck
mise run api:lint
mise run api:test:unit
mise run api:test:integration
# App (Go server with the embedded SPA)
mise run app:build
mise run app:run
# Database
mise run db:migrate
# Docker
mise run docker:hadolint
mise run docker:build
mise run docker:prune
# Compose
mise run compose:dev:up
mise run compose:dev:down
mise run compose:test:up
mise run compose:test:down
mise run compose:cluster:up
mise run compose:cluster:down
mise run compose:sentinel:up
mise run compose:sentinel:down
# Release
mise run github:release --dry-run
# Lint / smoke
mise run lint:shellcheck
mise run lint:ruff
mise run lint:yaml <file>...
mise run lint:json <file>...
mise run smoke:dev
mise run smoke:cluster
mise run smoke:sentinel
OpenAPI
api/openapi.yamlis the single source of truth for the HTTP contract. Edit the spec first, then regenerate code — never hand-edit generated files.- Regenerate both sides after any spec change:
mise run api:generate(oapi-codegen → Go server types) andmise run web:generate(openapi-generatortypescript-fetch→ TS client). Commit the spec and regenerated output together so they never drift. - Treat generated code as read-only build output: don't patch it, and reuse the generated types instead of redeclaring request/response shapes by hand.
- Target OpenAPI 3.0; keep the spec valid and lint-clean before generating.
- Model the contract explicitly: name every schema, mark
requiredfields, setnullable/formats/enums precisely, and reuse$refcomponents instead of duplicating inline shapes. - Define error responses (and their schema) for every operation, not just the
happy path; give each operation a stable, unique
operationId. - Evolve the API backward-compatibly: add optional fields rather than changing or
removing existing ones; version via the path prefix (
/api/v1) for breaking changes.
Go
- Run
mise run api:formatandmise run api:lintbefore committing; keep the build clean withmise run api:vulncheck. - Accept
context.Contextas the first parameter of any function that does I/O and propagate it; never store a context in a struct. - Wrap errors with
fmt.Errorf("...: %w", err)to preserve the chain; inspect witherrors.Is/errors.Asrather than string matching. Handle every error — never discard one with_unless intentional and commented. - Return early on errors to keep the happy path unindented.
defercleanup (Close,Rollback,Unlock) right after acquiring the resource; check the error of deferredCloseon writes.- Guard shared state with a mutex or channel; run tests and CI with
-race. - Start goroutines with a clear lifecycle (ctx cancellation or
errgroup); never leak them. Don't start a goroutine you can't stop. - Keep interfaces small and define them at the consumer, not the producer.
- Use the standard
log/slogfor structured logging; neverlog.Fatal/panicoutsidemainor truly unrecoverable startup paths.
Database / transactions
- Use the request
context.Contextfor every query so cancellation and timeouts propagate to the database. - Run multi-statement units of work in a single transaction (
pgxBeginTx/pool.Begin).defer tx.Rollback(ctx)immediately — a rollback after a successfulCommitis a safe no-op — and onlyCommiton the success path. - Keep transactions short: do no network calls or slow work while holding one, to avoid long-held locks and connection-pool starvation.
- Always use parameterized queries (never string-concatenate SQL) to prevent injection.
- Set an explicit isolation level when correctness depends on it, and be ready to retry on serialization failures.
- Always
Close/release rows and checkrows.Err()after iterating. - Make schema migrations forward-only and backward-compatible (expand/contract); never edit an already-applied migration.
Redis (go-redis v9)
- Treat Redis as a cache, not a source of truth: tolerate misses and evictions, and always be able to rebuild a value from the database.
- Pass the request
context.Contextto every command so timeouts and cancellation propagate; use a bounded timeout for cache calls. - Distinguish a cache miss from a real error: check
errors.Is(err, redis.Nil)and fall through to the source instead of failing the request. - Never let a cache failure break the request path — log and degrade gracefully (serve from the DB) rather than returning an error to the caller.
- Always set an explicit TTL on cache keys (
SET ... EX); never write keys that live forever, and prefer a small jitter on TTLs to avoid thundering-herd expiry. - Namespace keys with a clear, versioned prefix (e.g.
link:v1:<id>) so formats can evolve without colliding. - Batch round-trips with pipelines and keep values small; serialize with a single agreed format. Avoid unbounded keys/values.
- On writes, keep the cache consistent with the DB (write-through or invalidate after the committed transaction); don't update the cache before the DB commit.
- Use atomic operations (
INCR,SETNX, Lua scripts) instead of read-modify-write races, and set a TTL on any lock keys.
TypeScript
- Run
mise run web:check(svelte-check + tsc) andmise run web:lintbefore committing; format withmise run web:format. - Keep
stricton; do not useany— preferunknownplus narrowing, generics, or precise types. Avoid non-null assertions (!); narrow instead. - Prefer
type/interfacedefinitions over inline shapes; derive types from a single source of truth and reuse the generated API client types rather than redeclaring response shapes. - Use
constby default, discriminated unions for state, and exhaustiveswitch(with aneverdefault) so new cases are caught at compile time. - Handle
null/undefinedexplicitly with optional chaining and nullish coalescing; avoid truthiness checks that hide0/"". - Keep modules side-effect free where possible and use
import typefor type-only imports.
Svelte (5, runes)
- Use runes:
$statefor reactive state,$derivedfor computed values (never recompute manually), and$effectonly for genuine side effects — not to sync derived state. - Declare component inputs with
$props()and keep props read-only; communicate upward via callback props or events, not by mutating parent state. - Prefer
$derivedover$effectfor transformations; an$effectthat only assigns to another$stateis usually a$derivedin disguise. - Keep markup logic minimal — move non-trivial computation into
$derivedor helpers; use keyed{#each}blocks ({#each items as item (item.id)}) for stable list updates. - Scope styles to the component (default Svelte scoping) and use Tailwind utilities for layout; avoid global styles outside the app shell.
- Guard browser-only APIs and clean up listeners/timers in the
$effectreturn function. - Keep components small and focused; lift shared reactive logic into
.svelte.tsmodules using runes.