Every Release Is a Test: The Hidden Logic Connecting Code Assertions and CLI Distribution

min dulle

Hatched by min dulle

Sep 08, 2026

11 min read

91%

0

What if releasing software is not a delivery problem at all, but a testing problem performed in public?

A command line tool can work perfectly on a developer’s laptop and still fail as a product. Its binary may compile, yet the installation instructions may be wrong. The package may be published, yet the executable may not be discoverable. The release may be available for one operating system while quietly broken for another. In each case, the code has passed one test, but the tool has failed the larger one: can a real person reliably obtain it and use it?

This reveals a useful connection between two activities that teams often treat as unrelated. A well designed test follows a simple structure: setup, action, assertion. A well designed software release follows the same structure. You prepare an environment, perform the release, then verify that the resulting artifact behaves as promised.

The deeper lesson is not merely that releases should have more checks. It is that every boundary in software is a test boundary. A function crosses a boundary when it receives input. A command line tool crosses one when it enters a user’s terminal. Distribution is therefore not an administrative afterthought. It is the final and most consequential test of the product’s contract.

The Smallest Useful Model of Trust

A test is valuable because it turns an expectation into an observable event. Instead of vaguely believing that a function works, we arrange a specific situation, invoke the function, and check the result.

The structure is deceptively simple:

  1. Setup: create the conditions under which the behavior should occur.
  2. Action: perform the behavior being examined.
  3. Assertion: compare what happened with what should have happened.

Imagine testing a command called weather. The setup might provide a fake location service and a temporary configuration file. The action invokes weather London. The assertion checks that the output contains the expected forecast and that the process exits successfully.

This pattern does more than organize test code. It defines a theory of trust. Trust is earned when a system behaves predictably under a known condition. The setup establishes the world, the action creates the event, and the assertion gives us a standard by which to judge the outcome.

The same logic applies to distributing a command line tool. Suppose a team builds a Go application and wants users to install it through Homebrew. The setup includes compiling binaries for the relevant platforms, generating checksums, producing a release archive, and creating the package metadata that Homebrew needs. The action is the automated publication of those artifacts through a release workflow. The assertion is not simply that the workflow completed. It is that a fresh user can install the tool, invoke it, and receive the promised behavior.

The distinction matters. A green build is an assertion about a build. It is not necessarily an assertion about a product.

A release is successful only when the user’s first experience passes its own setup, action, and assertion sequence.

This gives us a stronger definition of software quality. Quality is not a property trapped inside source code. It is the consistency of a chain of expectations, from local logic to public use.

Distribution Is the Test You Cannot Run Only in the Repository

Most testing happens close to the code. Developers construct controlled conditions because control makes failures easier to isolate. That is useful, but it creates a danger: the closer a test is to the implementation, the farther it may be from the user’s reality.

A locally executed test might prove that the application responds correctly when started from the project directory, with a particular version of the language runtime, a populated configuration file, and dependencies already installed. A user installing through Homebrew has none of those assumptions. Their setup is different. The operating system may differ. The machine architecture may differ. The current directory is probably unrelated to the project. The user may not know which environment variables exist, or even what the tool was built with.

This is why distribution should be understood as environment expansion. It takes a program out of the narrow laboratory where it was created and places it into many unfamiliar laboratories operated by strangers.

A release system such as GoReleaser and GitHub Actions is valuable not merely because it saves commands. Its deeper role is to make the expansion repeatable. It can build the same project for several platforms, package the outputs consistently, calculate checksums, publish a release, and update the Homebrew formula or tap. The automation transforms a fragile sequence of human memory into an executable procedure.

That procedure has its own test anatomy.

Setup means identifying the matrix of environments that the product claims to support. This includes operating systems, processor architectures, installation channels, configuration states, and permissions. If the tool claims to support macOS on both Intel and Apple silicon, those are not marketing details. They are test conditions.

Action means running the release pipeline. The code is compiled, artifacts are packaged, metadata is generated, and the distribution channel is updated. The pipeline is not simply moving files. It is exercising the assumptions that connect source code to a usable installation.

Assertion means installing from the channel as a user would. It means checking that the right binary arrives, the checksum matches, the executable is on the path, the command starts, and a representative task succeeds. It may also mean verifying that an upgrade works, that an unsupported platform fails clearly, and that the tool reports useful errors when configuration is missing.

Without the assertion stage, automation can become a theatrical performance. The machinery moves, logs appear, and a release page fills with files. Yet the only question that matters remains unanswered: did the intended behavior survive the journey?

The Difference Between Artifact Correctness and Experience Correctness

A useful mental model is to distinguish artifact correctness from experience correctness.

Artifact correctness asks whether the things produced by the pipeline are internally valid. Is the binary present? Is it the expected size? Does its checksum match? Does the archive contain the intended files? Does the package metadata point to a real URL?

Experience correctness asks whether a person can complete the job the tool exists to perform. Can they find the installation command? Can they install without reading the repository? Does the command work from any directory? Does the error message explain what to do next? Does an upgrade preserve their configuration?

The first category is easier to automate and therefore often receives more attention. The second category is harder because it crosses technical and human boundaries. Still, it can be made concrete.

Consider a simple release assertion sequence:

Setup:
  Use a clean virtual machine or container.
  Install Homebrew.
  Use no project files or preexisting configuration.

Action:
  Install the released command line tool through Homebrew.
  Run the tool with a basic, documented command.

Assertion:
  The installation succeeds.
  The executable is available on the path.
  The command returns the expected output.
  The exit status communicates success or failure correctly.

This test is powerful because it rejects hidden assistance. It does not allow a developer’s shell configuration, cached dependency, or remembered workaround to rescue the process. It approximates the conditions under which trust is actually formed.

The principle generalizes beyond Homebrew. A web application can pass unit tests while failing when deployed behind a proxy. A library can pass its internal suite while breaking when installed through a package manager. A container can build successfully while lacking the permissions required at runtime. A mobile application can compile while its store metadata, signing, or upgrade path is broken.

In each case, the error comes from testing a component while promising an experience.

Users do not consume binaries, archives, or workflows. They consume successful outcomes.

Automation Turns Memory Into a Contract

Manual release processes fail in a predictable way. They depend on memory, local state, and a sequence of small decisions that are rarely documented with enough precision. Someone builds the binaries, someone else edits the formula, a third person checks the release page, and eventually a user discovers that one platform points to yesterday’s version.

The problem is not that people are careless. The problem is that manual work has weak assertions. A person may look at a list of artifacts and infer that everything is fine. An automated system can be required to prove more specific claims.

This is where continuous integration becomes more than a place to run tests. A GitHub Actions workflow can serve as a public contract compiler. It translates intentions such as “this tool supports common platforms and can be installed with Homebrew” into repeatable operations and checks.

A mature release workflow might contain several layers:

  • Build binaries for every promised operating system and architecture.
  • Package each binary with predictable names and directory structures.
  • Generate checksums and attach them to the release.
  • Publish versioned artifacts from a tagged commit.
  • Update the Homebrew distribution metadata.
  • Install the package in a clean environment.
  • Run a smoke test against the installed executable.
  • Verify that the reported version matches the published version.

Notice that the final steps are not redundant. They test the transitions between systems. The binary may be correct before packaging and inaccessible after packaging. The archive may be correct while the package formula references the wrong checksum. The package may install while the executable reports an old version. Each transition can preserve or corrupt the contract.

This suggests a broader engineering rule: test the seams, not only the parts. Components often behave correctly in isolation because their local assumptions are satisfied. Failures emerge where one system hands responsibility to another: compiler to archive, archive to release page, release page to package manager, package manager to shell, shell to user.

The release pipeline is therefore a chain of assertions. Every link should answer a narrowly framed question. “Did the build finish?” is weak. “Can a clean Apple silicon machine install version 2.4.1 through Homebrew and execute the documented command?” is strong.

Designing Tests Around Promises, Not Implementation Details

The most useful tests begin with a promise stated from the outside. Before writing a workflow, ask what a user should be able to believe.

For a command line tool, the promises might be:

  • I can install it using one documented command.
  • The installation selects the correct binary for my machine.
  • The command is available without navigating to a project directory.
  • The version I installed is identifiable.
  • A failure gives me enough information to recover.
  • Updating does not unexpectedly erase my configuration.

Each promise can be converted into setup, action, and assertion. This conversion is a practical design technique because it exposes vague claims. “Easy to install” is difficult to test. “A new user can install the latest release on a clean supported machine with one command” is testable.

The same technique improves the application itself. If the tool promises stable machine readable output, assert that output format. If it promises safe repetition, run the command twice and compare the resulting state. If it promises useful exit codes, invoke it with valid and invalid inputs and check both the messages and status values.

The key is to avoid testing implementation trivia unless the trivia is part of the promise. A test that asserts the existence of a particular internal function may break during a harmless refactor. A test that asserts that a user receives the right result remains valuable even when the internals change.

This does not mean internal tests are unimportant. They are fast instruments for locating faults. But they operate at different distances from the user. A healthy test strategy resembles a set of concentric circles:

  1. Logic tests examine individual decisions quickly.
  2. Integration tests examine communication between components.
  3. Distribution tests examine installation and execution in a clean environment.
  4. Outcome tests examine whether the user can complete the intended task.

The farther a test travels from the code, the more expensive it may be. That is precisely why it should be reserved for the promises whose failure would be most damaging. A small number of carefully chosen release assertions can protect more trust than hundreds of narrow checks that never leave the repository.

Key Takeaways

  • Treat distribution as a test boundary. The moment software leaves the repository, it enters a new environment with new assumptions. Test that transition explicitly.
  • Use setup, action, and assertion for releases. Define the clean environment, execute the installation path, and verify the user’s actual result.
  • Separate artifact correctness from experience correctness. A valid binary and a successful user journey are related, but neither proves the other.
  • Automate every repeatable promise. Use release tooling and continuous integration to build, package, publish, install, and smoke test without relying on memory.
  • Test seams between systems. Check the handoffs from compiler to archive, archive to release, release to package manager, and package manager to executable.

The Real Product Is the Chain That Survives Contact With Reality

Software teams often talk as if the product were the code and distribution were the act of carrying that code somewhere else. That framing hides the central difficulty. The product is not the source tree, and it is not even the compiled binary. The product is the reliable chain from intention to outcome.

A test makes that chain visible in miniature. It says: under these conditions, after this action, we expect this result. A release does the same thing at a larger scale. It declares: on these machines, through this installation channel, after this sequence of automated transformations, users should receive a working tool.

Once we see releases as tests, several practices become obvious. Release automation is executable specification. Package managers are part of the application boundary. Clean installation environments are not optional demonstrations but realistic fixtures. Smoke tests after publication are assertions about the actual product, not ceremonial confirmations that a workflow ran.

The most important shift is psychological. Stop asking whether the code is ready to release. Ask whether the promise is ready to be tested in the world.

Because the world is where software finally runs, and a green check inside the repository is only the setup.

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣