Skip to content

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:

  1. Duplication. Any policy change (e.g. add -coverprofile, bump Go version, switch to actions/setup-go@v6) requires a fan-out edit across every job.
  2. No coverage gate. The architectural review (§4.7) flagged that we run go test without -coverprofile, so there is no measurable coverage threshold and no regression alarm if a refactor drops it.
  3. Inconsistency risk. Without a single canonical setup, individual jobs drift (e.g. one forgets -race, another forgets -count=1).

Decision

  1. Introduce .github/workflows/_setup-go.yml as a reusable workflow (workflow_call). Single canonical implementation of:
  2. actions/checkout@v6
  3. actions/setup-go@v5 (uses caller's go.mod version)
  4. go vet ./...
  5. go test [-race] -count=1 [-coverprofile] <test-pattern>
  6. Coverage enforcement: when coverage-min > 0, compute go tool cover -func line-coverage and fail the job if below threshold.
  7. Upload coverage.out as a build artifact (14-day retention).
  8. go build -o /dev/null ./...

  9. Introduce .github/workflows/coverage-pilot.yml to 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.

  1. Existing ci.yml jobs are NOT migrated yet. The go-services matrix (persona-seeder, mock-engine, har-engine) is on required_status_checks for main branch protection — switching to uses: 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 --admin from 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 cover tool already gives us what we need.
  • nick-fields/retry@v3 action 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 consumer
  • tools/ci/retry.sh — doc-audit retry helper
  • feedback_doc_audit_drift_job_flaky.md — memory of pain it closes