Stage 7 · Master
CI/CD Pipeline
CI for a Go Workspace and Multi-Module Monorepo
How a Go workspace changes CI design, why a naive go test ./... fails from the repo root, and the pipeline shape Meridian should adopt before adding container builds and deployments.
Why Workspace CI Is Different
A Go workspace is not a single Go module. It is a coordination file that tells the Go toolchain which modules should be treated together during development. That distinction matters in CI. The repository root in meridian contains go.work, but the actual modules live at apps/gateway, apps/identity-service, apps/community-service, apps/billing-service, and libs/platform. A good CI pipeline must understand those real module boundaries instead of pretending the repository root is one giant package tree.
The go.work Gotcha
The non-obvious trap is that go test ./... from the repository root does not mean 'walk every module listed in go.work and test them all.' The ./... pattern expands relative to a module root, not to the workspace file as a magical monorepo boundary. From a go.work-only root, the command either skips what was intended or fails to traverse the module graph the way many engineers assume it should. That is why meridian's Makefile uses explicit full import path patterns instead.
WORKSPACE_PATTERNS := github.com/thesyscoder/meridian/apps/... github.com/thesyscoder/meridian/libs/...
test:
go test $(WORKSPACE_PATTERNS)
vet:
go vet $(WORKSPACE_PATTERNS)
tidy:
go work syncThis is the correct lesson to generalize: in a multi-module workspace, CI should call the packages that actually exist instead of trusting a recursive shorthand that was designed around module roots.
A green pipeline built on go test ./... from the wrong directory can quietly omit packages. That is more dangerous than an obvious failure because engineers believe the repository was fully verified when it was not.
What the Real Makefile Already Teaches
The real Makefile already provides the correct CI primitives: run-gateway, run-identity, run-community, run-billing, build-gateway, build-identity, build-community, build-billing, plus workspace-wide build, test, vet, and tidy. CI should reuse these conventions instead of re-encoding repository knowledge in many separate shell snippets. Reusing build targets makes local development and CI tell the same story.
Pipeline Design
The first useful CI workflow for Meridian does not need clever change detection yet. Because every service still exposes only /healthz, the most valuable initial pipeline is straightforward and reliable: sync the workspace, run workspace-wide tests and vet, then build each service binary explicitly. Later, once the repository grows more expensive, selective fan-out can be added. The beginner lesson here is important: clarity usually beats optimization in the first correct version of CI.
.github/workflows/ci.ymlcreateResponsibility: Runs workspace synchronization, tests, vet, and per-service builds on every pull request and main-branch push.
Why now: The repository already has enough module structure that correctness should be enforced automatically before merge.
.github/workflows/ci.ymlmodifyResponsibility: Later evolve into a matrix build once container images and heavier tests exist.
Why now: The first version should optimize for correctness and readability; fan-out is the second step, not the first.
Connects to: Builds directly on Makefile targets that already exist in the reference repository.
Reference Workflow
name: ci\n\non:\n pull_request:\n push:\n branches: [main]\n\njobs:\n workspace-checks:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-go@v5\n with:\n go-version-file: go.work\n - name: Sync workspace manifests\n run: make tidy && git diff --exit-code go.work\n - name: Run tests across workspace modules\n run: make test\n - name: Run go vet across workspace modules\n run: make vet\n - name: Build each deployable service\n run: make buildThis workflow is intentionally boring. Boring CI is good CI when it is correct. The important part is that make test and make vet already encode the workspace-aware import path patterns.
What Should Stay Global
- go work sync drift checks should remain global because workspace membership is a repository-wide concern.
- Tests and vet should initially remain global because shared code in libs/platform affects every service.
- Per-service binary builds should stay explicit so a broken cmd/<service>/main.go fails the pipeline immediately.
- Selective CI should be introduced later only when repository cost justifies the extra mapping logic.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.