Contributing
Conventions for contributing to go-unifi — formatting, context-first methods, error wrapping, the generated-vs-hand-written rule, and the pre-push gate.
This page collects the conventions a change to go-unifi is expected to follow. Most are enforced by tooling; run the pre-push gate before opening a PR and CI should be green.
The cardinal rule: never edit generated files
Files named *.generated.go start with // Code generated … DO NOT EDIT. Never hand-edit them. A daily CI
job regenerates and overwrites them. To change generated output, edit
customizations.yml and regenerate, or add
a hand-written sibling .go file.
This is the single most important convention — see the architecture overview for the generated-vs-hand-written split.
Wrapping generated code
Generated CRUD methods are private (getUser, listUser, createUser). Expose them through public
wrappers in the hand-written sibling (GetUser, ListUser, …), and put any custom logic — search-by-MAC,
field initialization, custom marshaling — in the wrapper, not the generated file. That way a regeneration never
clobbers your behavior.
A few more client conventions:
- Transport: use
c.Get/Post/Put/Patch/Delete(orc.Do) fromrequests.go; don't build rawhttp.Requests. - Errors: surface API failures as
ServerError(status, method, URL, code, validation details). Return theErrNotFoundsentinel when a single-resource GET finds nothing. - JSON edge cases: for empty-string-or-int, string-or-number, or enabled/disabled fields, use the helpers
in
json.go(emptyStringInt,numberOrString,booleanishString) rather than ad-hoc unmarshaling. - Validation: use go-playground/validator struct tags; register custom regex
validators via
NewCustomRegexValidatorinvalidation.go. - One-way Official dependency:
unifi/officialmust import nothing fromunifi. The transport is injected as the structuralDoerand the capability check as aGate.
Go style
| Rule | Enforced by |
|---|---|
| Tabs for indentation | gofmt family |
| Max line length 200 | golangci-lint |
| Import grouping & ordering | goimports + gci |
| Stricter formatting | gofumpt |
Run the formatters with golangci-lint fmt (aliased to make fmt). A few language conventions on top:
- Context first. Every client method takes
context.Contextas its first argument. - Wrap errors with
%wand context. Never return a bare error that adds no information — name what failed:fmt.Errorf("injecting validation tags: %w", err). The exception: if the callee already wraps with sufficient context, bubbling it up verbatim is fine. - Comments explain why, not what. Keep them short (≈2 lines); only exceed that for genuinely complex logic that naming and structure can't make self-evident. Don't narrate obvious code.
- Logging. The client holds a named
log Loggerfield (not embedded) and exposes aLogger()accessor. The default logger islog/slog-backed — don't import logrus in theunifipackage.
go-unifi targets Go 1.26.
Commit messages
Use conventional commits. The type you choose drives the
changelog: feat, fix, perf, and security
appear in release notes; a ! marker (feat!:) flags a breaking change; chore/ci/docs/refactor/
style/test/build are treated as internal noise and excluded.
The pre-push gate
make check # build + lint + test — the pre-push gateOr run the steps individually:
go build ./...
golangci-lint run
go test ./...If you touched anything that feeds code generation (customizations.yml, a template, a snapshot, the
DeviceState enum, or the Client interface), also confirm the generated output is current — CI fails on a
diff:
go generate unifi/codegen.go && git diff --exit-code # mirrors CI's test-codegen
go generate unifi/device.go && git diff --exit-code # the DeviceState stringergolangci-lint reads the Go version from go.mod. If a tool manager shadows your Go install with an older
version, formatting and linting can fail spuriously — make sure the go on your PATH matches go.mod.