Table of contents

Saying a project cares about software quality is easy. Pointing at the exact mechanisms that produce it is harder.
Quality is not a final inspection step, and it is not a property you can bolt on before a release. It is the cumulative result of dozens of small, boring, automated decisions that either happen on every commit or do not happen at all.
That is what the title of this article means. An act is something a person performs at a moment in time: a QA phase before the release, a yearly security audit, a reviewer being especially careful on a good day. Acts depend on memory, time, and goodwill, and they are the first casualty of deadline pressure; their results start decaying the moment they end. A system is a standing structure that produces quality as its normal output: automated, continuous, redundant, and measured, regardless of who is busy that week. W. Edwards Deming made the underlying point about manufacturing decades ago: you cannot inspect quality into a product; you have to build it into the process that makes it.
In this article I want to make that concrete. Instead of talking about quality in the abstract, I will walk through the actual measures in place in nurago , an open-source collection of production-oriented Go packages that I maintain. Everything described here is public and verifiable in the repository: the Makefile targets, the linter configuration, the continuous integration (CI) workflows, the test suites. You can clone it and run the whole quality pipeline yourself with a single command.
None of this makes nurago special. It is offered as evidence that this level of rigour is within reach for many projects, including those maintained by one person, once quality is treated as an engineered system.
The Principles Behind the Pipeline
Standards such as ISO/IEC 25010:2023 decompose product quality into nine characteristics: functional suitability, performance efficiency, compatibility, interaction capability, reliability, security, maintainability, flexibility, and safety. Those categories are useful as a checklist, but they do not tell you how to get there. These are the working principles that shape every quality decision in nurago:
- Build quality in, do not inspect it in. Checks run at the earliest possible moment: in the editor, in the local build, on every pull request. Finding a defect at review time is expensive; finding it before the commit is nearly free. This is the “shift-left” idea applied literally.
- Automate everything that can be automated. If a quality rule depends on a human remembering it, it will eventually be skipped. Formatting, linting, testing, coverage, security scanning, and dependency review are all machine-enforced.
- One pipeline, everywhere. The same core quality gate (formatting, linting, tests, coverage) runs identically on a developer laptop and in CI, which removes most of the “works locally but fails the build” gap. CI then layers extra platform-level scans on top (CodeQL, dependency review, secret scanning), in addition to the local checks, never instead of them.
- Adopt open standards and common conventions. Where a recognised standard exists, nurago uses it instead of inventing its own: the conventional Go project layout and godoc conventions for code, and guidance from the Open Worldwide Application Security Project (OWASP) and the National Institute of Standards and Technology (NIST) for security-sensitive functionality. Standards keep the project interoperable, verifiable with off-the-shelf tools, and immediately familiar to newcomers.
- Defence in depth. No single tool catches everything, so independent layers overlap: static analysis, tests with a race detector, fuzzing, property-based API testing, security scanning, and AI-assisted deep reviews all look at the same code from different angles.
- Keep the code small and simple. Complexity is where defects hide. Hard limits on function length and cyclomatic complexity are enforced by the linter, not by reviewer goodwill.
- Make quality visible. Coverage, build status, and security posture are published as badges and public dashboards. Visibility creates accountability, and it lets users of the library verify the claims instead of trusting them.
Everything that follows is these principles turned into tooling.
Quality Metrics: What Gets Measured
You cannot improve, or even protect, what you do not measure. These are the concrete metrics nurago tracks and the thresholds attached to them:
- Unit test coverage: 100%. This is an explicit policy in the contribution guidelines, and the pull request checklist requires that coverage has not dropped. Coverage is collected with
-covermode=atomic(so it is accurate even under concurrency) and published on Coveralls for anyone to inspect. A 100% target is often dismissed as dogmatic; in a foundational library it pays for itself, because it forces every branch, including error paths, to be exercised and it sidesteps the recurring debate about which code “deserves” tests. What coverage cannot prove is that the assertions are meaningful: a test can execute every branch while checking nothing. That blind spot is real, and it is named in the known-gaps section below, together with the mutation testing already being used to close it. - Cyclomatic and cognitive complexity: maximum 10 per function. Enforced by the
gocycloandgocognitlinters. Functions that exceed the limit must be decomposed before they can be merged. - Function size: maximum 100 lines or 50 statements. Enforced by
funlen. Small functions are easier to test, review, and reason about. - Lint findings: zero. The linter runs with
--max-issues-per-linter 0 --max-same-issues 0, meaning every issue is reported and any issue fails the build. There is no accepted baseline of warnings that everyone learns to ignore. - Performance: benchmarked, not guessed. Hot-path packages ship benchmark suites. Every benchmark is compiled and executed once on each test run as a smoke test, and a dedicated
make benchtarget runs the real measurements, with allocation tracking enabled and without the race detector or coverage instrumentation that would distort the numbers. A companionmake benchcmptarget compares the current results against any git reference withbenchstatand fails on statistically significant allocation regressions. Where that automated comparison deliberately stops, and why, is discussed in the known-gaps section below. - Known vulnerabilities: zero tolerated.
govulncheckruns inside the quality gate on every build, CodeQL scanning and dependency review run on every pull request, and any finding blocks the merge.
The test suite itself is sizeable: across the pkg/ tree, test files outnumber source files, and the executed test cases number in the thousands once table-driven subtests are counted. On top of the plain unit tests there is a large body of runnable example functions, benchmark suites for the hot paths, and fuzz targets for the input-handling packages.
The Tools: Overlapping Layers of Verification
Formatting and Style, Fully Mechanical
Style debates waste review time, so style here is mechanical rather than reviewable. Four formatters run as part of the lint gate: gofmt (with simplification enabled), gofumpt (a stricter superset), goimports, and gci for deterministic import ordering. Code either conforms or the build fails.
Static Analysis: Strict by Default, Exceptions by Design
A common approach is to enable a handful of hand-picked linters. nurago inverts the model: the golangci-lint
configuration sets default: all, enabling every available linter, so every new analyser added to golangci-lint is adopted automatically. Several of the enabled linters are themselves aggregates of many rules (staticcheck alone bundles more than a hundred checks, and govet, gocritic, revive, and gosec add dozens each), so the number of distinct checks applied to every line of code runs well into the hundreds. Among the always-on analysers are gosec for security anti-patterns, staticcheck for correctness bugs, revive for style and naming, and misspell for documentation typos.
A strict-by-default policy is only sustainable with a structured escape hatch: no rule set survives contact with real code without exceptions. What matters is that every exception is explicit, narrow, version-controlled, and therefore reviewable. In nurago they exist at three levels:
- Globally disabled linters. A short, curated list of linters that conflict with the project’s style (line-length limits, whitespace rules, variable-name-length checks, and similar) is switched off for the whole codebase in the configuration. Each entry in that list is a documented style decision, not a silent omission.
- Scoped exceptions in the configuration. Some rules are relaxed only where they do not make sense: duplication and function-length checks are lifted for test files (table-driven tests are naturally long and repetitive), the naming rule is disabled for packages whose identifiers follow external conventions, a specific
goseccheck with a high false-positive rate is excluded, and generated code such as mocks is skipped entirely, along with the standard exclusion presets for well-known false positives. - Inline, justified suppressions. Where a linter is silenced in code with
//nolint, the annotation targets a specific rule on a specific line and carries an explanatory comment. Exceptions are visible and auditable, never blanket.
The linter itself is version-pinned and installed by the build system, so every contributor and every CI run analyses the code with exactly the same tool. Pinning also resolves the tension hidden in “every new analyser is adopted automatically”: new linters actually arrive only when the pin is bumped, and the dedicated update target makes that a deliberate, reviewed event, in which any findings from newly added analysers are fixed, or granted a documented exception, in the same change.
Testing: One Command, Many Dimensions
The single make test target packs several verification dimensions into one run:
go test -shuffle=on -tags=unit,benchmark -covermode=atomic \
-bench=. -benchtime=1x -race -failfast \
-coverprofile=target/report/coverage.out ./pkg/...
Each flag is there for a reason:
-raceruns the Go race detector on every test, catching data races that the tests exercise, the kind of defect that otherwise tends to surface only in production under load.-shuffle=onrandomises test execution order on every run, flushing out hidden dependencies between tests; the chosen seed is printed, so an order-dependent failure can be reproduced.-bench=. -benchtime=1xexecutes every benchmark once as a smoke test, so a benchmark that no longer compiles or runs surfaces immediately (the real measurements live in the separatemake benchtarget described earlier).-covermode=atomicproduces coverage counts that stay accurate even when tests exercise code concurrently.-failfaststops a package’s tests at the first failure. This is a feedback-speed choice: the pipeline runs before every commit, where the common case is a single fresh failure introduced by the change in progress, not a backlog of unrelated ones.
On top of the table-driven unit tests, the suite includes:
- Example tests: a large set of
Example*functions that double as documentation on pkg.go.dev and are compiled on every test run, with most also checked against their declared output, which keeps the documented examples from drifting out of date. - Fuzz tests for parsers and input-handling packages (password hashing, JWT parsing, string metrics, DNS name handling, splitting, paging, and more), using Go’s native fuzzing to throw generated inputs at the code.
- Generated mocks via
go.uber.org/mock, regenerated from scratch by the build (make generatedeletes and recreates them), so mocks track the interfaces they stand in for instead of being maintained by hand.
Integration and API-Contract Testing
The repository ships a complete reference web service in examples/service, and that service carries its own, stricter quality gate:
- OpenAPI-first contracts. The service’s APIs (public, private, and monitoring) are each defined by an OpenAPI specification, and Schemathesis property-tests the running service against those specifications with hundreds of generated requests, checking every response for conformance.
- Scenario tests with venom exercise realistic API workflows, including external dependencies simulated with an HTTP mock server.
- Ephemeral integration environments. A docker-compose setup spins up the service with real databases, runs the full integration suite, and tears everything down, both locally and in CI.
- Configuration validation. Runtime configuration files are validated against a JSON Schema as part of the quality gate, so a malformed configuration file surfaces as a build failure rather than at deployment time.
Security Scanning
Security is layered across the pipeline rather than delegated to one tool:
- CodeQL semantic analysis runs on every push and pull request, plus a weekly scheduled scan to catch newly published query packs against unchanged code.
- Dependency review blocks pull requests that introduce dependencies with known vulnerabilities.
govulncheck, Go’s official vulnerability scanner, runs as part of the quality gate on every build, locally and in CI, for both the library and the example service. It checks the module graph, the standard library, and the toolchain against the Go vulnerability database, and its call-graph reachability analysis reports only vulnerabilities in code paths that appear reachable, which keeps findings actionable rather than noisy. Like the rest of the tooling, it is installed and version-controlled by the build system.- Secret scanning with push protection guards against credentials ever landing in the history.
gosecflags insecure coding patterns at lint time, before the code is even committed.- Standards-guided secure design. Security-sensitive packages follow published guidance rather than ad-hoc choices: password hashing uses Argon2id with defaults matching the second recommended option set of RFC 9106 (stronger than the OWASP Password Storage minimum) and stores hashes in the standard Password Hashing Competition (PHC) string format, and compromised-credential checks follow NIST-style acceptance rules. CodeQL and gosec findings map to the Common Weakness Enumeration (CWE), so reports stay comparable across tools.
- A published security policy (
SECURITY.md) gives researchers a clear, private reporting channel.
The project also holds the Open Source Security Foundation (OpenSSF) Best Practices badge, an externally defined checklist covering change control, reporting, quality, and security practices.
AI-Assisted Deep Reviews: Raising the Baseline
Deterministic tools only find what their rules can describe. To look beyond that boundary, the codebase periodically goes through multi-stage review campaigns performed with large language models (LLMs). Each campaign runs multiple independent scan passes over the code, hunting for logic defects, security weaknesses, concurrency hazards, and subtle inconsistencies between code, documentation, and tests that no pattern-based tool can express.
By the definition given at the start of this article, such a campaign is an act rather than a system, and that is exactly how it is used. Acts raise the bar. Systems defend it. The deterministic pipeline defends everything its rules can describe: formatting, complexity, coverage, known vulnerability classes. The review campaigns look for what those rules cannot express, and they are worth repeating at the moments when the bar should move: after a major new package lands, or when a substantially more capable model generation becomes available.
The layer stays disciplined under two further rules. It complements the deterministic pipeline and never replaces it, so an AI review is never a merge gate; its output is not reproducible the way a linter’s is. And every finding is treated as a lead rather than a verdict, manually triaged, reproduced where applicable, and fixed through the same review process as any other bug report.
Where a finding reveals a recurring class of problem rather than a single bug, the fix is made systemic: a new test, a tightened configuration, a documented convention. That is how the output of an act gets folded into the system.
Known Gaps, Named on Purpose
No quality system is ever complete, and one that claims to be should not be trusted. Naming the missing layers is part of the system, because it turns vague unease into a concrete backlog.
Working through that backlog also sharpens a design rule that is easy to miss: not every check belongs in the per-commit gate. A check too slow or too environment-sensitive to run reliably on every change does not become useful by being forced into CI. It becomes a flaky gate that teaches everyone to ignore failures. The right place for such a check is an automated but on-demand step, scripted in the Makefile so it runs the same way every time, invoked deliberately at the moments when its results can be trusted. Two areas currently sit on that boundary:
- Benchmark regression gating. The benchmarks are kept permanently runnable. Besides the once-per-test-run smoke execution, CI runs the full benchmark suite in its own parallel job, so a benchmark that breaks fails the build without slowing the main gate. The statistical comparison is automated too, but locally:
make benchcmpbenchmarks both the working tree and a base git reference (mainby default) with a fixed iteration count repeated several times, hands both result sets tobenchstat, and fails on any statistically significant increase in allocations per operation, the one benchmark metric deterministic enough to gate on. What has deliberately not been built is the obvious-sounding next step, an automatic comparison in the CI pipeline. Benchmarking two revisions with enough repetitions for statistical significance is slow, and shared CI runners are too noisy for timing numbers to be compared consistently; a gate that fails on runner jitter is worse than no gate. So timing regressions are caught by running the comparison deliberately, on stable hardware, before merging performance-sensitive changes. What remains missing is that this invocation is a habit rather than a mechanism. - Mutation testing. Coverage proves that code is executed, and says nothing about whether the assertions around it are meaningful. Mutation testing closes that gap by deliberately breaking the code in small ways and checking that at least one test fails for every mutation; surviving mutants point at tests that watch but do not check. It is the natural companion to a 100% coverage policy, and the audit such a policy needs to stay meaningful. Two things keep it in the known-gaps section rather than in the gate. It has been applied to some packages, not yet the whole tree, so the technique’s own coverage is still partial. And it is run by hand, deliberately, for the same reason as the benchmarks but more so: mutation runs are orders of magnitude slower than the test suite they exercise, and the current tools have enough quirks that forcing them into a per-change check would produce nothing but a flaky gate. The direction is therefore a Makefile target: an on-demand mutation run, extended package by package, used as a periodic audit of test quality after a large batch of new tests lands or before a major release, with surviving mutants triaged into the same backlog as any other defect.
Both entries refine this article’s thesis in the same direction. The system is more than the set of checks that run on every commit. It is the whole arrangement, including the documented decisions about which checks run on demand and why. A slow check with a scripted entry point and a named trigger is part of the system. A check that exists only as a good intention is not.
The Processes: Making It Stick
Tools only matter if the workflow actually runs them. This is the process side of the system.
One Command for the Whole Pipeline
The entire local quality pipeline is a single command:
make x
which chains version sync, formatting, a clean build, dependency download, code generation, and the full quality gate (qa: linter govulncheck test coverage), then builds and tests the example service too. A Docker-based variant (make dbuild) runs the identical pipeline in a container for a reproducible environment. There is no secret sequence of steps that only the maintainer knows.
The pipeline is also deliberately CI-agnostic. Every step lives in the repository’s Makefiles, so the GitHub Actions workflows are thin wrappers that invoke the same make targets everyone runs locally. Migrating to GitLab CI, Jenkins, or any other automation platform would mean rewriting a few lines of workflow YAML, not the quality gate itself. The pipeline belongs to the project, not to the CI vendor, and the containerised variant makes it runnable on any machine with Docker.
Continuous Integration as a Merge Gate
A set of dedicated GitHub Actions workflows, each with least-privilege permissions, gates every change to main:
- check: lints, tests, and benchmarks the library in parallel jobs, uploading coverage to Coveralls.
- check_example: lints, tests, builds, and integration-tests the reference service, including the Docker image.
- codeql: security analysis on every change and on a weekly schedule.
- dependency-review: audits dependency changes on every pull request.
- release: publishes a GitHub release whenever a version tag is pushed.
A pull request that fails any gate does not merge. There is no sanctioned manual override in the process.
Code Review with a Checklist
Every change goes through review, routed by a CODEOWNERS file. The pull request template is a checklist that encodes the quality contract: the full make x pipeline passes, coverage has not dropped, documentation and examples accompany new features, and the version has been bumped. Contribution guidelines (CONTRIBUTING.md) state the expectations up front, including the 100% coverage policy, and a code of conduct governs the community.
Versioning, Releases, and Dependencies
- Semantic versioning from a plain
VERSIONfile, bumped as part of every change and propagated automatically into the example service. - Tagged releases via
make tag, with release notes curated by hand for every release, sometimes seeded from a semi-automatically generated draft of the merged changes. - Pinned tooling: the linter, integration tools, and build utilities are all version-pinned, and dedicated make targets (
make updateall) upgrade the Go toolchain, the linter, and all module dependencies in a controlled, reviewable way rather than by drift.
Why This Matters Beyond One Project
If you are evaluating an open-source library for production use, features tell you what it can do today. The quality system tells you how likely it is to still be doing it correctly in three years. “Is the code good today?” is the easy question. “What structure forces it to stay good tomorrow?” is the one that predicts anything. Public coverage, enforced complexity limits, security scanning, and a reproducible pipeline are what separate a dependency from a liability.
And if you own a codebase, the transferable lesson is that none of this requires a large platform team:
- Put the entire quality gate behind one command that runs identically everywhere.
- Turn every subjective rule (style, complexity, size) into a machine-enforced limit.
- Measure the things you care about, publish the numbers, and fail the build when they regress.
- Layer independent checks: linters, race-enabled tests, fuzzing, contract tests, and security scans each catch what the others miss.
- Encode the process in templates and checklists so it survives busy weeks and new contributors.
None of it requires a greenfield codebase either. On an existing project the absolute targets described here are usually out of reach on day one, and chasing them immediately is a good way to see the effort abandoned. The transferable move is the ratchet: enforce the strict linter rules only on newly changed lines at first (golangci-lint can restrict findings to code modified since a chosen revision), fail the build when coverage drops rather than when it sits below an absolute number, and tighten the limits release by release. The gate that matters from day one is “no regression”; the absolute targets are simply where the ratchet eventually comes to rest.
None of this is free, and pretending otherwise would undermine the argument. A strict gate raises the bar for first-time contributors, whose pull request may fail on a linter rule they have never heard of. The full pipeline takes minutes rather than seconds, and a 100% coverage policy means some tests exist to exercise a branch rather than to capture an insight. These costs are real, but they are fixed, predictable, and paid in small instalments; the cost of defects is open-ended and tends to arrive all at once, at the worst possible moment. The system is a bet that the first kind of cost is cheaper than the second.
Excellence, in the old line attributed to Aristotle, is not an act but a habit. Software quality works the same way, with one modern advantage: a habit is easiest to keep when a machine enforces it.
See it all in practice:
- Repository: https://github.com/tecnickcom/nurago
- Package docs: https://pkg.go.dev/github.com/tecnickcom/nurago
- Clone it and run
make xto watch the whole pipeline execute.
If this approach resonates with how you want to build software, star the project, borrow the configuration, and adapt the pipeline to your own repositories. That is what open source is for.