ADR-0074: Reusable Go CI workflow + coverage gate (pilot)¶
- Status: Accepted
- Date: 2026-05-23
- Driver: 2026-05-23 architectural review §5.1 + §4.7
Context¶
The repository ships 14 separate Go modules (each with its own
go.mod) plus the root-level packages under pkg/. The CI workflow
(ci.yml) currently inlines a copy of the same go-setup + vet + test
sequence per module:
- uses: actions/setup-go@v5
with:
go-version-file: ${{ matrix.service }}/go.mod
cache-dependency-path: ${{ matrix.service }}/go.sum
- run: go vet ./...
- run: go test -race -count=1 ./...
- run: go build -o /dev/null ./...
Three problems:
- Duplication. Any policy change (e.g. add
-coverprofile, bump Go version, switch toactions/setup-go@v6) requires a fan-out edit across every job. - No coverage gate. The architectural review (§4.7) flagged that
we run
go testwithout-coverprofile, so there is no measurable coverage threshold and no regression alarm if a refactor drops it. - Inconsistency risk. Without a single canonical setup, individual
jobs drift (e.g. one forgets
-race, another forgets-count=1).
Decision¶
- Introduce
.github/workflows/_setup-go.ymlas a reusable workflow (workflow_call). Single canonical implementation of: actions/checkout@v6actions/setup-go@v5(uses caller'sgo.modversion)go vet ./...go test [-race] -count=1 [-coverprofile] <test-pattern>- Coverage enforcement: when
coverage-min > 0, computego tool cover -funcline-coverage and fail the job if below threshold. - Upload
coverage.outas a build artifact (14-day retention). -
go build -o /dev/null ./... -
Introduce
.github/workflows/coverage-pilot.ymlto exercise the reusable workflow against three packages that already have strong test suites (measured locally before adopting):
| Package | Measured | Gate |
|---|---|---|
pkg/octopus/crdt/orset |
92.5 % | 70 % |
pkg/octopus/common/log |
100 % | 80 % |
pkg/oobi/satellite/p2p/metrics |
95.9 % | 70 % |
Gates are intentionally below current coverage so the pilot
bakes in for ~4 weeks without false positives on unrelated PRs.
Once a gate is green on main for 4 weeks, the operator ratchets
it up by +5 percentage points.
- Existing
ci.ymljobs are NOT migrated yet. Thego-servicesmatrix (persona-seeder, mock-engine, har-engine) is onrequired_status_checksformainbranch protection — switching touses:changes the GitHub-rendered check name, which would require updating branch protection. That migration is a follow-up task with explicit operator approval.
Rollout plan (after this PR merges)¶
| Wave | When | Action |
|---|---|---|
| W0 | Now | Land _setup-go.yml + coverage-pilot.yml. Pilot runs on PR but is NOT required. |
| W1 | +4 weeks | Promote coverage-pilot jobs to required_status_checks via gh api. |
| W2 | +8 weeks | Migrate ci.yml go-services matrix to consume _setup-go.yml. Update required_status_checks to new check names in same PR. |
| W3 | +12 weeks | Add coverage gates to pkg/octopus/connect-art/internal/signaling (RedisStore, ≈88 %), pkg/oobi/satellite/p2p/orchestrator, pkg/acme-client. |
| W4 | +16 weeks | Ratchet existing gates +5 %. |
Doc-audit retry — companion change¶
The same PR adds tools/ci/retry.sh (3-attempt exponential
backoff) and wires it into doc-audit.yml's Drift + Link gates.
These are the two CI jobs documented as flaky in
feedback_doc_audit_drift_job_flaky.md, where the runner dies
mid-traversal on transient network blips. The current workaround
(admin-merge bypass) weakens the gate's value; auto-retry makes it
self-healing instead.
Consequences¶
Positive¶
- One place to change Go CI policy. Future changes (Go version
bump, switch to
actions/setup-go@v6, new flag) are one PR. - First measurable coverage gate for any Go package — closes §4.7 audit recommendation.
- Flaky doc-audit gates become self-healing instead of needing
admin-merge bypass. Removes the
--adminfrom the documented workaround path for these jobs.
Negative¶
- Three pilot jobs initially. Adds ~15 s × 3 to CI time on
affected PRs. Mitigated by
paths:filter — pilot only runs when pilot packages or the workflow itself change. - Pilot scope is narrow. Most packages stay uncovered until W3. Trade-off: a wide gate at low % gives a false sense of security and blocks unrelated PRs.
Alternatives considered¶
- Wide-and-shallow gate (all modules at 50 %) — Rejected: 50 % is not a useful bar; gives false safety on packages that need 80 %+ (signaling, p2p) while incorrectly green-stamping packages whose coverage is high but uncovered packages would pull the average down.
- Codecov / Coveralls.io — Rejected (this turn): adds a third-
party dependency + GitHub App + token management. The Go stdlib
covertool already gives us what we need. nick-fields/retry@v3action for retry — Rejected: shell retry is 30 lines, deterministic, and doesn't pull a third-party marketplace action into the supply chain.
See also¶
.github/workflows/_setup-go.yml— the reusable workflow.github/workflows/coverage-pilot.yml— pilot consumertools/ci/retry.sh— doc-audit retry helperfeedback_doc_audit_drift_job_flaky.md— memory of pain it closes