Skip to content

Commit 0993fd0

Browse files
committed
feat(ci): add lint, tidy, coverage, and formatting CI jobs
1 parent 5aa1dae commit 0993fd0

33 files changed

Lines changed: 1768 additions & 100 deletions

File tree

.github/workflows/ci.yml

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,109 @@ on:
99
- opened
1010
- reopened
1111
- synchronize
12+
- labeled
13+
- unlabeled
1214
merge_group:
1315

1416
permissions:
1517
contents: read
16-
pull-requests: read
18+
pull-requests: write
1719

1820
jobs:
1921
# ---------------------------------------------------------------------------
20-
# BUILD AND UNIT TESTS (special case - Gazelle + build + unit tests)
22+
# LINT (all tools from Bazel — gofmt via Go SDK, yamllint via Go binary)
2123
# ---------------------------------------------------------------------------
22-
build-and-unit-test:
23-
name: Build and Unit Test
24+
lint:
25+
name: Lint
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: ./.github/actions/setup
30+
31+
- name: Run linters
32+
run: make lint
33+
34+
# ---------------------------------------------------------------------------
35+
# TIDY (module files + BUILD files in sync)
36+
# ---------------------------------------------------------------------------
37+
tidy:
38+
name: Tidy
2439
runs-on: ubuntu-latest
2540
steps:
2641
- uses: actions/checkout@v4
2742
- uses: ./.github/actions/setup
2843

44+
- name: Check module files are tidy
45+
run: make check-tidy
46+
2947
- name: Check BUILD files are up to date
30-
run: |
31-
echo "Running Gazelle to check BUILD files..." >&2
32-
make gazelle
33-
if ! git diff --quiet; then
34-
echo "BUILD files are out of date!" >&2
35-
echo "" >&2
36-
echo "The following files were modified by Gazelle:" >&2
37-
git diff --name-only >&2
38-
echo "" >&2
39-
echo "Please run 'make gazelle' locally and commit the changes." >&2
40-
exit 1
41-
fi
42-
echo "BUILD files are up to date" >&2
48+
run: make check-gazelle
49+
50+
# ---------------------------------------------------------------------------
51+
# BUILD AND UNIT TESTS
52+
# ---------------------------------------------------------------------------
53+
build-and-unit-test:
54+
name: Build and Unit Test
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v4
58+
with:
59+
# Full history needed for diff coverage (git merge-base origin/main HEAD).
60+
fetch-depth: 0
61+
- uses: ./.github/actions/setup
4362

4463
- name: Build project
4564
run: make build
4665

47-
- name: Run unit tests
48-
run: make test || echo "No unit tests found"
66+
- name: Run unit tests with coverage
67+
if: "!contains(github.event.pull_request.labels.*.name, 'COVERAGE_EXEMPTION')"
68+
run: make check-coverage
69+
70+
- name: Run unit tests (coverage exempted)
71+
if: contains(github.event.pull_request.labels.*.name, 'COVERAGE_EXEMPTION')
72+
run: make coverage
73+
74+
- name: Coverage summary
75+
if: always()
76+
run: |
77+
if [ -f coverage-html/summary.txt ]; then
78+
echo "### Coverage Report" >> "$GITHUB_STEP_SUMMARY"
79+
echo "" >> "$GITHUB_STEP_SUMMARY"
80+
cat coverage-html/summary.txt >> "$GITHUB_STEP_SUMMARY"
81+
fi
82+
83+
- name: Notify coverage exemption
84+
if: >-
85+
github.event_name == 'pull_request' &&
86+
contains(github.event.pull_request.labels.*.name, 'COVERAGE_EXEMPTION')
87+
env:
88+
GH_TOKEN: ${{ github.token }}
89+
run: |
90+
SUMMARY=""
91+
if [ -f coverage-html/summary.txt ]; then
92+
SUMMARY=$(cat coverage-html/summary.txt)
93+
fi
94+
ARTIFACT_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts"
95+
BODY=$(cat <<EOF
96+
> [!CAUTION]
97+
> **Coverage enforcement has been exempted** for this PR via the \`COVERAGE_EXEMPTION\` label.
98+
> Reviewers: please verify this exemption is justified before approving.
99+
100+
${SUMMARY}
101+
102+
[View full coverage report](${ARTIFACT_URL})
103+
EOF
104+
)
105+
gh pr comment ${{ github.event.pull_request.number }} --body "$BODY"
106+
107+
- name: Upload coverage report
108+
if: always()
109+
uses: actions/upload-artifact@v4
110+
with:
111+
name: coverage-report
112+
path: coverage-html/
113+
if-no-files-found: ignore
114+
retention-days: 14
49115

50116
# ---------------------------------------------------------------------------
51117
# INTEGRATION TESTS (e2e, gateway, orchestrator)
@@ -120,6 +186,8 @@ jobs:
120186
name: Required Checks
121187
runs-on: ubuntu-latest
122188
needs:
189+
- lint
190+
- tidy
123191
- build-and-unit-test
124192
- e2e
125193
- gateway-integration-test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ MODULE.bazel.lock
1212
bin/
1313
.docker-bin/
1414

15+
# Coverage
16+
coverage-html/
17+
1518
# Make completion cache
1619
.make_targets_cache

CLAUDE.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ submitqueue/
4848
│ ├── counter/ # Sequential number generation (interface + mysql/)
4949
│ ├── queue/ # Messaging queue abstraction (interface + sql/)
5050
│ └── storage/ # Storage abstraction (interface + mysql/)
51-
├── core/ # Shared infrastructure packages reused across services
51+
├── core/ # Shared infrastructure packages reused across services
5252
│ ├── consumer/ # Queue consumption framework (lifecycle, ack/nack, routing)
5353
│ └── errs/ # Error classification framework (user vs infra, retryability)
54+
├── tool/ # Development and CI tooling
55+
│ ├── coverage/ # Coverage report tool (LCOV parsing, HTML, diff coverage)
56+
│ └── linter/yamllint/ # YAML syntax validator
5457
├── example/server/ # Runnable servers with Docker Compose
5558
├── test/
5659
│ ├── e2e/ # End-to-end tests (full stack)
@@ -157,10 +160,17 @@ integration-test: build-all-linux ## Run all integration tests (auto-builds bina
157160
```bash
158161
make build # Build all services
159162
make test # Run unit tests
163+
make coverage # Run unit tests with coverage + HTML report
164+
make check-coverage # Run coverage with threshold enforcement
165+
make lint # Run all linters (fmt + YAML)
166+
make fmt # Format Go code
167+
make check-tidy # Check go.mod and MODULE.bazel are tidy
168+
make check-gazelle # Check BUILD.bazel files are up to date
169+
make tidy # Run go mod tidy + bazel mod tidy
170+
make gazelle # Update BUILD.bazel files
160171
make integration-test # Run all integration tests (Docker-based)
161172
make e2e-test # Run end-to-end tests
162173
make proto # Regenerate proto files
163-
make gazelle # Update BUILD.bazel files
164174
make local-start # Start full stack with Docker Compose
165175
make local-ps # Show running containers and ports
166176
make local-logs # View logs from all services
@@ -256,6 +266,18 @@ deps = [
256266

257267
See [doc/howto/TESTING.md](doc/howto/TESTING.md) for full testing guide.
258268

269+
### CI and Validation
270+
271+
CI runs on every PR and enforces all checks via a `required-checks` gate. **Before committing, validate locally:**
272+
273+
1. `make fmt` — format Go code (CI will reject unformatted code)
274+
2. `make lint` — run all linters (fmt check + YAML validation)
275+
3. `make check-tidy` — ensure `go.mod` and `MODULE.bazel` are tidy
276+
4. `make check-gazelle` — ensure `BUILD.bazel` files are up to date
277+
5. `make check-coverage` — run unit tests with coverage threshold enforcement
278+
279+
See [doc/howto/COVERAGE.md](doc/howto/COVERAGE.md) for coverage thresholds, exemptions, and HTML reports.
280+
259281
### Code Style
260282

261283
1. **Structured logging**`zap.SugaredLogger` with `Debugw`/`Infow`/`Errorw(msg, key, val, ...)`. Never unstructured methods.

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use_repo(
3636
"com_github_spf13_cobra",
3737
"com_github_stretchr_testify",
3838
"com_github_uber_go_tally_v4",
39+
"in_gopkg_yaml_v3",
3940
"org_golang_google_grpc",
4041
"org_golang_google_protobuf",
4142
"org_uber_go_fx",

Makefile

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,46 @@ ORCHESTRATOR_COMPOSE_FILE = example/server/orchestrator/docker-compose.yml
1010
# Fixed project name for local manual testing (tests use unique random names)
1111
LOCAL_PROJECT = submitqueue
1212

13+
# Minimum overall coverage percentage (override with: make check-coverage COVERAGE_THRESHOLD=80)
14+
COVERAGE_THRESHOLD ?= 80
15+
16+
# Minimum diff coverage percentage for new/changed lines (override with: make check-coverage DIFF_COVERAGE_THRESHOLD=90)
17+
DIFF_COVERAGE_THRESHOLD ?= 85
18+
1319
# Set REPO_ROOT for docker-compose
1420
export REPO_ROOT := $(shell pwd)
1521

16-
.PHONY: build build-all-linux build-gateway-linux build-orchestrator-linux clean clean-proto deps e2e-test gazelle integration-test integration-test-extensions integration-test-gateway integration-test-orchestrator local-clean local-gateway-start local-gateway-stop local-init-schemas local-logs local-orchestrator-start local-orchestrator-stop local-ps local-restart local-start local-stop proto query-deps query-targets run-client-gateway run-client-orchestrator run-queue-admin test test-no-cache help
22+
# Runs the coverage tool with optional diff detection.
23+
# Usage: $(call run_coverage,extra flags...)
24+
define run_coverage
25+
@LCOV=$$($(BAZEL) info output_path)/_coverage/_coverage_report.dat; \
26+
MERGE_BASE=$$(git merge-base origin/main HEAD 2>/dev/null || echo ""); \
27+
DIFF_ARGS=""; \
28+
if [ -n "$$MERGE_BASE" ] && [ "$$(git rev-parse HEAD)" != "$$MERGE_BASE" ]; then \
29+
git diff $$MERGE_BASE HEAD > /tmp/coverage-diff.patch; \
30+
DIFF_ARGS="-diff /tmp/coverage-diff.patch"; \
31+
fi; \
32+
$(BAZEL) run //tool/coverage -- \
33+
$$DIFF_ARGS \
34+
-summary $(CURDIR)/coverage-html/summary.txt \
35+
-html $(CURDIR)/coverage-html \
36+
-src $(CURDIR) \
37+
$(1) \
38+
$$LCOV
39+
endef
40+
41+
# Fails if git working tree is dirty. Usage: $(call assert_clean,fix command)
42+
define assert_clean
43+
@if ! git diff --quiet; then \
44+
echo "The following files need updating:" >&2; \
45+
git diff --name-only >&2; \
46+
echo "" >&2; \
47+
echo "Please run '$(1)' locally and commit the changes." >&2; \
48+
exit 1; \
49+
fi
50+
endef
51+
52+
.PHONY: build build-all-linux build-gateway-linux build-orchestrator-linux check-coverage check-gazelle check-tidy clean clean-proto coverage deps e2e-test fmt gazelle integration-test integration-test-extensions integration-test-gateway integration-test-orchestrator lint lint-fmt lint-yaml local-clean local-gateway-start local-gateway-stop local-init-schemas local-logs local-orchestrator-start local-orchestrator-stop local-ps local-restart local-start local-stop proto query-deps query-targets run-client-gateway run-client-orchestrator run-queue-admin test test-no-cache tidy tidy-bazel tidy-go help
1753

1854

1955
build: ## Build all services and examples
@@ -41,6 +77,21 @@ build-orchestrator-linux: ## Build Orchestrator Linux binary for Docker
4177
cp -f bazel-bin/example/server/orchestrator/orchestrator .docker-bin/orchestrator
4278
@echo "Orchestrator Linux binary ready at .docker-bin/orchestrator"
4379

80+
check-coverage: ## Enforce coverage thresholds (overall: $(COVERAGE_THRESHOLD)%, diff: $(DIFF_COVERAGE_THRESHOLD)%)
81+
@echo "Running tests with coverage..."
82+
@$(BAZEL) coverage //... --test_tag_filters=-manual,-integration --combined_report=lcov
83+
$(call run_coverage,-threshold $(COVERAGE_THRESHOLD) -diff-threshold $(DIFF_COVERAGE_THRESHOLD))
84+
85+
check-gazelle: ## Check BUILD.bazel files are up to date
86+
@echo "Running Gazelle to check BUILD files..."
87+
@$(BAZEL) run //:gazelle
88+
$(call assert_clean,make gazelle)
89+
@echo "BUILD files are up to date."
90+
91+
check-tidy: tidy ## Check that go.mod and MODULE.bazel are tidy
92+
$(call assert_clean,make tidy)
93+
@echo "Module files are up to date."
94+
4495
clean: ## Clean generated files and binaries
4596
@echo "Cleaning with Bazel..."
4697
@$(BAZEL) clean
@@ -53,16 +104,24 @@ clean-proto: ## Clean generated proto files
53104
@rm -rf orchestrator/protopb/*.pb.go
54105
@echo "Proto clean complete!"
55106

56-
deps: ## Install Go dependencies
57-
@echo "Installing Go dependencies..."
58-
@go mod download
59-
@go mod tidy
107+
coverage: ## Run tests with coverage, generate HTML report
108+
@echo "Running tests with coverage..."
109+
@$(BAZEL) coverage //... --test_tag_filters=-manual,-integration --combined_report=lcov
110+
$(call run_coverage,)
111+
112+
deps: tidy-go ## Download and tidy Go dependencies
60113
@echo "Dependencies installed!"
61114

62115
e2e-test: build-all-linux ## Run end-to-end tests (hermetic, auto-builds binaries)
63116
@echo "Running end-to-end tests..."
64117
@$(BAZEL) test //test/e2e:e2e_test --test_output=streamed
65118

119+
fmt: ## Format Go code
120+
@GOFMT=$$($(BAZEL) run @rules_go//go -- env GOROOT 2>/dev/null | tail -1)/bin/gofmt; \
121+
echo "Formatting Go code (using $$GOFMT)..."; \
122+
$$GOFMT -w .; \
123+
echo "Formatting complete!"
124+
66125
gazelle: ## Update BUILD.bazel files
67126
@echo "Running Gazelle to update BUILD files..."
68127
@$(BAZEL) run //:gazelle
@@ -83,6 +142,18 @@ integration-test-orchestrator: build-orchestrator-linux ## Run Orchestrator inte
83142
@echo "Running Orchestrator integration tests..."
84143
@$(BAZEL) test //test/integration/orchestrator:orchestrator_test --test_output=streamed
85144

145+
lint: lint-fmt lint-yaml ## Run all linters
146+
@echo "All lint checks passed."
147+
148+
lint-fmt: fmt ## Check Go code formatting (fails if unformatted)
149+
$(call assert_clean,make fmt)
150+
@echo "All Go files are properly formatted."
151+
152+
lint-yaml: ## Check YAML files for syntax errors
153+
@echo "Checking YAML files..."
154+
@$(BAZEL) run //tool/linter/yamllint -- .
155+
@echo "All YAML files are valid."
156+
86157
local-clean: ## Stop and remove all local services, volumes, and images
87158
@echo "Cleaning all services and data..."
88159
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(LOCAL_PROJECT) down -v --rmi local
@@ -229,6 +300,16 @@ test-no-cache: ## Run unit tests without cache (force re-run)
229300
@echo "Running unit tests (no cache)..."
230301
@$(BAZEL) test //... --test_tag_filters=-manual,-integration --nocache_test_results
231302

303+
tidy: tidy-go tidy-bazel ## Run go mod tidy and bazel mod tidy
304+
305+
tidy-bazel: ## Run bazel mod tidy
306+
@echo "Running bazel mod tidy..."
307+
@$(BAZEL) mod tidy
308+
309+
tidy-go: ## Run go mod tidy
310+
@echo "Running go mod tidy..."
311+
@$(BAZEL) run @rules_go//go -- mod tidy -e
312+
232313
help: ## Show this help message
233314
@echo "Available targets:"
234315
@echo ""

core/consumer/registry.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ type TopicConfig struct {
5757
// TopicRegistry provides queue, topic name, and subscription config for topics.
5858
// Each topic can have a different queue backend and topic name.
5959
type TopicRegistry struct {
60-
queues map[TopicKey]queue.Queue
61-
topicNames map[TopicKey]string
60+
queues map[TopicKey]queue.Queue
61+
topicNames map[TopicKey]string
6262
subscriptionConfigs map[topicGroup]queue.SubscriptionConfig
6363
}
6464

core/consumer/registry_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ func TestNewTopicRegistry(t *testing.T) {
1818
registry, err := consumer.NewTopicRegistry(
1919
[]consumer.TopicConfig{
2020
{
21-
Key: consumer.TopicKeyRequest,
22-
Name: "request",
21+
Key: consumer.TopicKeyRequest,
22+
Name: "request",
2323
Queue: mockQ,
2424
Subscription: extqueue.DefaultSubscriptionConfig(
2525
"worker-1", "group-a",
@@ -44,7 +44,7 @@ func TestNewTopicRegistry(t *testing.T) {
4444

4545
func TestNewTopicRegistry_InvalidTopicName(t *testing.T) {
4646
tests := []struct {
47-
name string
47+
name string
4848
topicName string
4949
}{
5050
{

0 commit comments

Comments
 (0)