Development
The supported everyday workflow runs Go tooling inside the project’s Docker development image. That removes host-version drift and means a clean machine needs Docker rather than a separately curated Go toolchain.
The Makefile has two execution shapes:
- ordinary build, lint, formatting, dependency, and vulnerability work runs in the development container with the repository mounted at
/work; make test-unituses the ordinary socketless container; full, integration, and coverage targets mount the Docker socket for Testcontainers.
The exact scripts and override lookup live in the framework Make-script deep dive.
Common targets
| Target | Runs where | Purpose |
|---|---|---|
make dev-image | Docker build | Build the development image used by normal tooling. |
make test | Docker + socket | Race-enabled go test ./.... |
make test-unit | Docker | Race-enabled go test ./... without Docker-socket access. |
make test-integration | Docker + socket | Uncached, race-enabled test run with a ten-minute timeout. |
make test-coverage | Docker + socket | Race-enabled coverage check; default floor is MIN_TEST_COVERAGE=90. |
make lint | Docker | shfmt, ShellCheck, go fix diff check, and golangci-lint. |
make lint-fix | Docker | Apply supported formatting and lint fixes. Review its diff. |
make format | Docker | Run gofumpt and shfmt. |
make sec | Docker | Full security scan: govulncheck + semgrep, merged into sec.sarif for the Security tab. Fails on any finding. |
make generate | Docker | Execute package-local go:generate directives. |
make build | Docker | Build a static executable under build/. |
make docker-build | Docker | Build the production image. |
make run-dev | Docker | Build/run the development image with the race detector. |
Use make help as the current target list. Do not replace these calls with a host go test or host linter in project automation: that gives two different toolchains two chances to disagree.
CPU limit
Every dev container runs with --cpus set to the host thread count minus two, never below 1, so lint and tests leave the machine responsive. --cpus is the hard quota, and Go 1.25+ sizes GOMAXPROCS from it automatically. Containers that tests start themselves are not limited. These Make variables change it:
| Variable | Default | Effect |
|---|---|---|
DEV_CPUS_RESERVED | 2 | Threads kept free for the host. |
DEV_CPUS | host threads minus DEV_CPUS_RESERVED, minimum 1 | Threads the container may use, for example make lint DEV_CPUS=4. |
DEV_CPU_LIMIT_ARGS | --cpus=$(DEV_CPUS) | The Docker flags added to DEV_RUN and DEV_RUN_DIND. Set it empty (DEV_CPU_LIMIT_ARGS=) to remove the limit. |
DEV_RUN_EXTRA_ARGS | empty | Extra docker run arguments for DEV_RUN. |
DEV_RUN_DIND_EXTRA_ARGS | empty | Extra docker run arguments for DEV_RUN_DIND. |
Tests, integration tests, and coverage
make test is the standard full suite. It enables Go’s race detector and has Docker available for Testcontainers-backed tests. make test-integration uses the same package pattern but disables the Go test cache and applies a ten-minute timeout. Use it when checking changes to real-infrastructure tests. Use make test-unit for the same race-enabled package walk without exposing the Docker socket; Testcontainers-backed cases cannot start infrastructure in that target.
make test-coverage also runs the race detector. It instruments every module package (-coverpkg=<module>/...) and runs -tags=integration, so a test under tests/ credits coverage to the production package it drives, and services are covered. A service-heavy project does not need to override the script to gate its own code. A service that runs out-of-process in a real container is covered too: the script exports SERVICEPACK_COVDATA_DIR, an integration test mounts it into that container as GOCOVERDIR, and the native covdata is merged into the total. Only what is not hand-written code under test is excluded from the floor: cmd/ mains, the tests/ harness, generated *.gen.go, the service-manager mocks, and the framework’s own example-* / hello-world demo services. Override the threshold deliberately:
make test-coverage MIN_TEST_COVERAGE=95
The target writes coverage-percent.txt for the badge workflow and removes the temporary coverage profiles. Testcontainers needs access to the Docker daemon; if Docker cannot create containers, fix Docker access rather than working around the integration tests.
Integration tests live under tests/. tests/testinfra is the harness: its baseline Setup builds the servicepack image from the repo Dockerfile and runs it, and tests/integration boots that image to confirm the app comes up with whatever services are registered. Both are a starting point you grow. Extend Infra with the real dependencies your services need (a database, a cache, a broker via testcontainers-go) and add tests that drive them. tests/ is protected by the update’s mechanical floor, so a framework update never overwrites your copy. The Docker runner already supports this: DEV_RUN_DIND runs on the host network, so testcontainers’ host-published ports are reachable from the test process.
Build outputs and runtime identity
make build uses the pinned Go build image, installs the static-build requirements inside that temporary container, and writes build/<module-tail> back with your host UID/GID. It injects three values with linker flags:
main.appName: the final segment of the module path; it sets the binary name and root Cobra command name;main.buildCommit: the checked-outHEADcommit when available.main.buildVersion: the exact Git tag atHEAD, ordevfor an untagged source build.
cmd/main.go puts those into the global log scope as binary, commit, and version. Build output is therefore traceable without hardcoding identity in service code. A source tree without a resolvable Git HEAD builds, but has no commit field to inject.
make docker-build is a separate production-image path. Do not assume a Dockerfile build and make build have the same customization hooks; inspect the relevant script before changing either.
Dependencies and code generation
Use the Make targets so dependency metadata and vendor/ stay synchronized:
make dep
make pkg-add PKG=example.com/module@v1.2.3
make pkg-update PKG=example.com/module
make pkg-upgrade
make pkg-remove PKG=example.com/module
The framework commits Go’s vendor/ tree. Do not edit vendored sources by hand. make service already regenerates service registration; use make service-registration after manual service changes. Treat internal/pkg/services/services.gen.go as generated output.
Override, do not fork framework plumbing
Makefile includes Makefile.servicepack. Define the same target in your project Makefile to replace a framework target, or add your own targets next to it. Framework script lookup prefers scripts/make/<script>.sh over scripts/make/servicepack/<script>.sh.
cp scripts/make/servicepack/test.sh scripts/make/test.sh
# edit scripts/make/test.sh for project-specific behavior
Likewise, project Dockerfile, Dockerfile.dev, cmd/init.go, and cmd/commands.go are yours. The framework-owned versions are updated by make servicepack-update; see framework updates.
Before you hand off a change
For a normal code change, run the narrowest useful target first, then the relevant full check, for example make test, make lint, and make test-coverage when a framework behavior changes. The repository’s pre-commit hook calls make lint && make test-coverage, so it will repeat those checks during the project’s normal commit flow.
Read getting started for ownership boundaries and architecture for where a change should live.