Development
go test ./... # unit tests + modules against local in-test servers — no network
go vet ./...
go build -o checkfleet ./cmd/checkfleet
Both go vet ./... and go test ./... must be green before a change lands —
they are the same checks CI runs.
Layout
| Path | Responsibility |
|---|---|
internal/engine/ |
The contract: Check, Finding (status OK/WARN/BAD/ERROR), Run (checks run concurrently, each with its own timeout; results flattened in check order then worst-first sorted → deterministic), Summarize/Worst, and the typed YAML config with defaults (LoadConfig). |
internal/output/ |
Renderers: Text, Markdown, JSON. |
internal/checks/<name>/ |
One package per module, implementing engine.Check. |
internal/inventory/ |
Minimal Ansible INI inventory parser. |
cmd/checkfleet/ |
The CLI (stdlib flag, subcommands). |
Adding a module
- Create
internal/checks/<name>/implementingengine.Check. - Add its typed config to
internal/engine/config.go(with defaults inLoadConfig). - Wire it into
cmd/checkfleet/main.go. - Test it against a local fixture server — an
httptestserver, or a TLS cert generated on the fly with a known expiry. A test that touches the internet or real infrastructure is a bug.
Integration suite (opt-in)
Unit tests must stay offline (in-test servers only). To exercise the modules
against real services there’s a separate, opt-in suite gated behind the
integration build tag, so go test ./... never runs it.
docker compose -f docker-compose.integration.yml up -d --build --wait
go test -tags integration -v ./test/integration/...
checkfleet check all --config checkfleet.integration.yml # end-to-end smoke
docker compose -f docker-compose.integration.yml down -v
docker-compose.integration.ymlbrings up redis, nats, consul, haproxy, postgres, patroni (+etcd), and keycloak, each published on127.0.0.1with a healthcheck so--waitmakes readiness deterministic. Support files live indeploy/integration/(HAProxy config, the in-compose Patroni image).checkfleet.integration.ymlpoints every module at those local ports.- The suite’s contract is deliberately loose: it asserts reachability — at
least one non-
ERRORfinding per module — not exact status (that stays covered by the unit tests). NATS runs standalone, so its meta-cluster finding isBADby design (a single node is not an HA cluster); it’s exit-neutral. - CI runs all of this in
.github/workflows/integration.yml, a job kept separate from thetestjob inci.yml.
Fuzzing the parsers
The parsers that read untrusted external input are fuzzed (CF-36): parseM3U8
(HLS manifests), parseMessage (the hand-rolled DNS wire decoder), parseCSV
(HAProxy stats), and the /jsz decode + meta-cluster analysis (NATS). Each has
a white-box Fuzz* target in its package.
go test ./internal/checks/dns -run '^$' -fuzz '^FuzzParseMessage$' -fuzztime=30s
The seed corpora run as ordinary tests under go test ./..., so a known
crasher can never regress. The Fuzz
workflow fuzzes every target on a schedule, on demand, and on PRs that touch a
parser; any crasher lands in internal/checks/<mod>/testdata/fuzz/ and is
uploaded as an artifact.
Conventions
- Status
ERRORmeans “the check could not measure” (network, handshake) — not “the target is unhealthy” (that’sBAD). - The worst-first, stable finding sort is a de-facto API — don’t break it.
- The only dependency is
gopkg.in/yaml.v3; add others only with strong justification. - Todos live in
BACKLOG.mdwith stableCF-nids — not scattered in code comments.
Backlog ↔ GitHub issues
BACKLOG.md is the single source of truth, and issues are derived from it —
never the other way around.
internal/backlogparses the file intoCF-nitems (tested).cmd/backlog-syncturns each item into a GitHub issue: labelbacklog, grouped by milestone (the##sections). Checking an item ([x]) closes its issue; unchecking reopens it. Matching is by theCF-ntitle prefix, so the sync is idempotent.-
The
Backlog syncworkflow runs it on every push tomainthat touchesBACKLOG.md(or the tool). Run it by hand with:go run ./cmd/backlog-sync -dry-run # preview go run ./cmd/backlog-sync # apply (needs an authenticated gh)
Don’t open or close backlog issues by hand — edit BACKLOG.md instead.
Releasing
Every vX.Y.Z tag triggers .github/workflows/release.yml, which runs
goreleaser (.goreleaser.yaml): cross-platform
archives (linux/darwin/windows × amd64/arm64), checksums.txt,
GitHub release notes from the commit log, and a Homebrew cask.
Validate the config locally without publishing:
goreleaser check # lint the config
goreleaser release --snapshot --clean # full build + archives into dist/, no upload
Homebrew tap: enabled. On every v* tag goreleaser pushes the cask to
Allan-Nava/homebrew-tap (the
repo and the HOMEBREW_TAP_GITHUB_TOKEN secret are set up, skip_upload:
"false"), so:
brew install Allan-Nava/tap/checkfleet
The cask ships the release archive’s prebuilt binary (darwin amd64/arm64) and
strips the com.apple.quarantine attribute on install (the binary is
unsigned). Only tags after the tap was enabled carry the cask — older
releases won’t install via brew.
CI quality gates
The test job runs go test with a coverage profile and prints the total to the
job summary (go tool cover -func=cover.out). Coverage is reported for
visibility — there is no hard threshold that fails the build. Run it locally with
go test -coverprofile=cover.out ./... && go tool cover -func=cover.out | tail -1
(or -html=cover.out for a browsable report).
Beyond go vet + go test, CI runs a lint job:
govulncheck(gate) — fails the build on vulnerabilities your code actually reaches (run with the latest stable Go, so patched stdlib issues clear automatically). Run it locally withgo run golang.org/x/vuln/cmd/govulncheck@latest ./....golangci-lint(gate) — fails the build on any lint finding. The enabled linters are pinned in.golangci.yml(errcheck, govet, ineffassign, staticcheck, unused). Run it locally withgo run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run ./.... Keep the pinned version on a release built with Go >= thego.modversion: an older binary refuses to load the config (“the Go language version … is lower than the targeted Go version”).