Lesson 2 · Ship it — code → prod plumbing
Linting & githooks
The quality gates around a commit — what golangci-lint enforces, a hand-rolled analyzer that watches for a very specific bug, and the hooks on your git commits (one of which quietly does nothing).
Your win: say what the lint config actually enforces, explain the custom
sqlclosecheck and its twist, and name what each githook does.
golangci-lint: an explicit enable-list
.golangci.yaml starts from default: none and turns on a
named set — no surprises from upstream defaults:1
bodyclose copyloopvar errcheck goconst gocritic gocyclo gosec govet
ineffassign misspell prealloc revive rowserrcheck staticcheck
unconvert unparam unused whitespace
Two things to notice. For database code the coverage is bodyclose (HTTP bodies) and
rowserrcheck (rows.Err()) — and the upstream sqlclosecheck
linter is not in this list. And run.tests: false — test files
are not linted at all. Formatters (gci/gofmt/goimports) enforce import grouping
standard → github.com/manabie-com → default.
The custom sqlclosecheck — bespoke, and with a twist
So how does the repo guard against unclosed rows? A hand-written analyzer under
cmd/custom_lint/, built on Go's SSA analysis framework:
var pgxPackage = "github.com/jackc/pgx/v4" // it targets pgx.Rows, not database/sql
...
pass.Reportf(pos, `pgx.Rows was not closed after query. Please add "defer rows.Close()" to your code`)
It flags a pgx.Rows that's never closed, or closed without defer.
Why it matters: an unclosed Rows holds a connection, and enough of them
exhaust the pool and hang the service.2
(Cute detail: its own doc string says "Checks that sql.Rows is closed," but the code targets
pgx Rows — this is entirely the repo's own analyzer, not the well-known upstream one.)
make test-sqlclosecheck-lint only runs it against cmd/custom_lint/testdata/
and diffs the output — a self-test of the analyzer, not a scan of product code. And the
repo-wide CI invocation is commented out:
# .github/workflows/tiered.pre_merge.yml:311
# - name: Run sqlclosecheck
# go run ./cmd/custom_lint/main.go ./internal/... ./features/... ./cmd/server/...
So the guard exists, is tested, and is not enforced on real code. Recognising
"a check that's configured but doesn't run" is exactly the kind of thing that separates reading a
repo from understanding it.2
gitleaks: secret scanning (that can silently no-op)
The pre-commit hook runs gitleaks over staged changes to catch
hardcoded secrets — gitleaks protect --staged --config=.gitleaks.toml --redact. But:
.githooks/common.bash checks for the binary and, if it's missing, warns and
skips rather than failing. So local enforcement is best-effort; the real guarantee is the
CI gitleaks job. .gitleaks.toml extends the defaults and adds custom
rules (GCP service-account keys, API keys, Slack, AWS, FCM…).3
The githooks (installed by make init)
| Hook | Does |
|---|---|
commit-msg | requires 1–2 Jira IDs matching (LT|RMP)-NNN — not "JIRA-123"; skips merge/hotfix branches |
prepare-commit-msg | auto-prepends [LT-XXX] extracted from the branch name |
pre-commit | runs gitleaks (see above) |
| pre-push | does not exist |
make init installs them with git config core.hooksPath .githooks (a repo
directory, not .git/hooks). Run make lint for golangci-lint run
— though note CI pins v2.8.0 while local uses whatever's on your PATH, so they can
disagree.3
golangci-lint & the sqlclosecheck idea
The linter runner's config docs, and the upstream analyzer whose idea the repo re-implemented.
→ golangci-lint.run/docs ·
ryanrolds/sqlclosecheck ·
gitleaks
→ in-repo .golangci.yaml, cmd/custom_lint/, .githooks/
Check yourself (from memory)
Q1. What does the custom sqlclosecheck analyzer check?
pgx.Rows not closed (or closed without
defer) — unclosed rows exhaust the connection pool.
Q2. Is the custom sqlclosecheck enforced on the codebase?
make test-sqlclosecheck-lint only vets
testdata/; the repo-wide CI call is commented out. The guard exists but doesn't run.
Q3. What does the commit-msg hook require?
(LT|RMP)-NNN (not generic "JIRA-").
A separate hook auto-prepends it from the branch name.
.golangci.yaml default: none
+ explicit enable-list; DB coverage = bodyclose+rowserrcheck (NOT upstream
sqlclosecheck); run.tests: false (tests unlinted); gci import grouping.
Custom sqlclosecheck: bespoke SSA analyzer (cmd/custom_lint/) flagging
pgx.Rows not closed / closed without defer (→ pool exhaustion). Twist:
self-tests on testdata/ only; CI run commented out → not enforced.
gitleaks: pre-commit gitleaks protect --staged, but skips if
not installed; CI is the real guard. Hooks (make init →
core.hooksPath .githooks): commit-msg needs LT-/RMP-,
prepare-commit-msg auto-prefixes, pre-commit = gitleaks; no pre-push.1. In-repo (verified): .golangci.yaml:5-23,81 (enable-list, run.tests: false).
2. In-repo (verified): cmd/custom_lint/sqlclosecheck/sqlclosecheck.go:30,74; Makefile:115-120 (self-test); .github/workflows/tiered.pre_merge.yml:194,311,313 (commented out).
3. In-repo: .githooks/{common.bash,commit-msg,pre-commit,prepare-commit-msg}, .gitleaks.toml, Makefile:17-25 (make init).