Contributing
The mechanics of working in the repository. The golden rule: always use make,
never raw go — the targets set build tags, env, and tooling correctly.
Build and test
make check # fmt + lint + the fast tests
make fmt | lint # individually
make test_unit # unit
make test_integration # needs a database (internal/testutil/dbtest)
make test_acceptance # end-to-end
make test_web # vitest
make test_all
Test targets accept ARGS for passthrough — go test flags for the backend,
vitest flags for the web. The build tags are unit_tests, integration_tests,
and acceptance_tests.
Build artifacts and images are covered in Building And Release Artifacts (and summarized in Install).
Schema vs. migrations
This trips people up, so internalize the split:
| Directory | Holds | Rule |
|---|---|---|
schema/ | The declarative table definitions (Atlas). | All table/column/index changes go here. |
migrations/ | Data-only migrations — seed inserts, backfills. | Never put schema DDL here. Every migration must be idempotent (ON CONFLICT, IS DISTINCT FROM). |
AUTO_APPROVE=1 make atlas_schema_apply # apply the declarative schema
make atlas_migrate_set_baseline # bootstrap, then…
make atlas_migrate_apply # run data migrations
To change a table, edit schema/ and re-apply — you don’t hand-write DDL
migrations.
The cross-stack explore index (gantrycd_explore) has the same split in
schema-explore/ + migrations-explore/, provisioned the same way via
make atlas_explore_schema_apply / atlas_explore_migrate_set_baseline /
atlas_explore_migrate_apply (a separate Atlas env, so its destructive diff
never sees the core tables). It is recreatable from runs, so it has no data
migrations today — only the baseline anchor.
Conventions that matter
These are enforced socially and, where possible, by lint and tests. The full set
is in CLAUDE.md and the Reference section;
the load-bearing ones:
- No backwards compatibility. GantryCD favors clean breaking changes over compatibility shims. Don’t annotate changes as deprecated or write “remove after one release” — just make the change.
- Respect the layers. Handlers parse and authorize; services hold logic; repositories do SQL; jobs go through services/repos. See Backend internals.
- Respect the communication boundaries. Web → Backend, Runner → Backend, and Backend → its dependencies. Nothing else.
- No trivial wrappers. Inline a one-line
errors.Asrather than adding anIsNotFoundErrorhelper; the deliberately-removed ones should stay removed. - No SQL string interpolation of user input. Bind values as parameters —
this is absolute in
internal/authzand everywhere else. - Typed errors and typed values. Return
pkg/domainerrors and check witherrors.As; internal APIs pass typed structs, not[]byte— JSON marshaling lives only at the HTTP and S3 boundaries. - Context logging.
contextutil.GetLogger(ctx),snake_caseattribute keys.
Where the deep docs live
The Reference section holds the detailed implementation docs — authz, the deployment state machine, the runner protocol, configuration, error handling, rate limiting, and more. When you change behavior, that’s the page you update in the same change.