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

.golangci.yaml:5-23 (the enabled linters)
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: falsetest 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:

cmd/custom_lint/sqlclosecheck/sqlclosecheck.go:30,74
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.)

The flagship twist: the guard doesn't actually run You'd assume this analyzer scans the codebase on every build. It doesn't. 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:

It skips if gitleaks isn't installed .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)

HookDoes
commit-msgrequires 1–2 Jira IDs matching (LT|RMP)-NNN — not "JIRA-123"; skips merge/hotfix branches
prepare-commit-msgauto-prepends [LT-XXX] extracted from the branch name
pre-commitruns gitleaks (see above)
pre-pushdoes 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

Read this next

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?

It flags a 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?

1–2 IDs matching (LT|RMP)-NNN (not generic "JIRA-"). A separate hook auto-prepends it from the branch name.
Recall: the lint config, the custom analyzer's twist, and the githooks.
golangci + sqlclosecheck + gitleaks + hooks, then reveal
golangci-lint: .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 initcore.hooksPath .githooks): commit-msg needs LT-/RMP-, prepare-commit-msg auto-prefixes, pre-commit = gitleaks; no pre-push.
🎯 Interview one-liner "What's your linting setup?" → "golangci-lint with an explicit enable-list — no upstream defaults, and we don't lint test files. We even hand-wrote an SSA analyzer to catch unclosed pgx rows, though honestly it's only wired as a self-test right now. Secret scanning is gitleaks in a pre-commit hook plus CI, and commit messages must carry a Jira key."
Next: secrets — how the repo keeps credentials out of git and gets them to a running service without ever writing plaintext to disk. Ask me if the "configured but not enforced" idea is worth another look — it recurs.

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).