1. Why you need a release bundle
If you’re just getting started, it’s very easy to fall into a trap: it feels like the goal of development is to produce a file that runs. And that’s true… for about the first half hour. Then reality starts asking questions, and they all sound like different variants of “what is this thing and how do I use it?”. A user (or you, one week later) is not obligated to remember what flags the program has, what commands it supports, where to see the version, and what to do if “it doesn’t run on Windows”.
A release bundle is an attempt to be a polite engineer. We don’t just hand over “here’s a binary, good luck”; we deliver a small set of artifacts that answers the basic questions: what it is, how to run it, how to check the version, where to find examples, what changed. It’s like giving someone a coffee grinder: sure, you could hand them just a motor and some wires, but an instruction manual and a couple of recipes increase the chances that you’ll remain friends.
Release bundle as a distribution contract
The word “contract” here is not legal; it’s engineering. We state: anyone who downloads our program should be able to do a few things without telepathy. First, understand how to run the program and where the help is. Second, understand which exact version it is and what platform it was built for. Third, see short examples of typical usage. Fourth, understand what changed compared to the previous version (or at least see that “there were changes”).
Importantly, this contract also works for you. A month from now you’ll be looking at a bug report “nothing works”, and it will suddenly turn out that the most useful question is: “can you show the output of todo --version?”. If the version isn’t embedded in the binary and printed, you start playing engineering roulette: “I think it was a release… or not a release… or maybe I built this last week…”.
Minimum release bundle contents
In this lecture, we’re not discussing specific archivers, installers, or publishing releases on services—that’s a separate world where you can drown in buttons. We need a simple, clear, portable idea: a folder (or an archive) with predictable files inside.
A helpful way to think about it: there is a “heart” (the binary) and “wrapping” that makes the heart usable outside your laptop.
Below is the minimum set worth keeping in mind as a default.
| Element | Example name | What it’s for | Key idea |
|---|---|---|---|
| Binary | or |
The thing you run | The name must differentiate platforms and version |
| README | or |
“How to use” + quickstart | README is the project’s interface for a human |
| Examples | |
Ready-to-run commands/scenarios | Examples save more time than “descriptions” |
| Notes | / |
“What changed” | Releases are typically accompanied by notes |
Notice: this is not a list “to make it pretty”; it’s the minimal set that turns running into a predictable action. And yes, if you’re thinking “I already understand everything”, congratulations—you’re not you yet. Future you will appreciate it.
2. What a bundle looks like for the todo project
From this point on, we’ll talk as if we have a training CLI application todo (we build it throughout the course): it can create tasks, show a list, mark them as done, store data, and print all of it to stdout. We won’t extend functionality right now; we’ll “package” what we already have.
Let’s imagine we prepared a release version 1.3.0 for Linux amd64. Then you can think of the release bundle as a directory:
todo_1.3.0_linux_amd64/
todo
README.md
RELEASE_NOTES.md
examples/
quickstart.txt
demo-session.txt
And for Windows:
todo_1.3.0_windows_amd64/
todo.exe
README.md
RELEASE_NOTES.md
examples/
quickstart.txt
demo-session.txt
Even without archives and a “real release process”, the main thing is already visible: the user opens the folder and understands where to look.
5. Artifact naming
When you make a single binary “for yourself”, you can name it a.out or final_final2_really. But as soon as there is more than one artifact, confusion starts: which one is for macOS, which is for Linux, which is new, which is old, why one doesn’t run (spoiler: because you’re trying to run a binary for a different architecture).
That’s why in releases we aim for a boring but informative naming scheme:
- application name (todo)
- version (1.3.0)
- target OS (linux, windows, darwin)
- architecture (amd64, arm64)
- .exe extension for Windows
You can assemble this string in Go (we’ve already had the idea earlier), and it’s useful to keep the logic in one place so you don’t later hunt across the project for 12 different formats.
A small example (we’re not making it complicated, just showing the idea; this can live in internal/buildinfo or nearby):
package buildinfo
import (
"fmt"
"runtime"
)
func ArtifactName(app, version string) string {
return fmt.Sprintf("%s_%s_%s_%s", app, version, runtime.GOOS, runtime.GOARCH)
}
If you call it and print it, you’ll get something like:
package main
import (
"fmt"
"example.com/todo/internal/buildinfo"
)
func main() {
fmt.Println(buildinfo.ArtifactName("todo", "1.3.0")) // todo_1.3.0_linux_amd64
}
The point isn’t to generate release names directly inside the application (although sometimes that’s useful), but to get used to the format and stop relying on memory.
6. README: what matters in it
Many people write README files by the principle “just have one”. The result is a file that says: “This is the todo application. It does todo.” Thanks, but that’s like a microwave manual that says “Heats food”.
A good CLI README is a short roadmap. It has one main goal: a person who sees the project for the first time should be able, in 2–3 minutes, to:
- run the binary,
- see help,
- execute one typical scenario,
- understand where to check the version,
- understand where to write if everything broke (or at least what to attach to a report).
In a README, these blocks are almost always useful:
- What it is (2–3 sentences, no novel).
- Quickstart: 3–6 commands you can copy.
- Help: how to see --help and --version.
- Where examples live: a link to the examples/ folder.
- Limitations (if any): e.g., “the data file is stored there”.
To make it more tangible, here’s a “skeleton” README (as text, not dogma). Notice: this isn’t a “requirements list”, but a minimal communication scenario with a human.
# todo
A small CLI task tracker: add a task, show the list, mark as done.
## Quickstart
1) Check the version:
todo --version
2) See help:
todo --help
3) Add a task:
todo add "buy milk"
4) Show the list:
todo list
In this format, the README actually works: the user copies commands, gets results, and starts trusting the tool. And yes, “trust” in a CLI is when it doesn’t force you to read its source code.
7. examples/: usage examples
The examples/ folder sounds optional until you try to explain to a user how to reproduce an issue. Examples are good because, first, they are “living documentation”, and second, they establish a standard style for using your utility.
In our todo, a convenient example is a “demo session”: a set of commands and the expected output (at least partially). Even if the user doesn’t read the whole file, they’ll see the template: what commands exist and what they print.
An example file examples/quickstart.txt can look like plain text. Note: this is not Go code, so we’re not compiling anything here—we’re just showing “how to use it”.
# Quickstart: todo
todo --version
todo --help
todo add "read the lecture about release bundle"
todo list
todo done 1
todo list
The trick is that these examples can (if you want) serve as a basis for a manual smoke check: you open the file, run through the commands, and you can already see that the binary is “alive”. Even if you don’t build CI and automated release tests, the habit of having a “run scenario” in examples/ is very engineering-minded.
8. Release notes: RELEASE_NOTES.md
“Release notes” sounds solid, like you have a company of a thousand people and a marketing department. In practice, it’s just an honest answer to the question: “What changed?”. In the Go world, this is a normal culture: releases come with notes, and even the Go language itself regularly says “read the release notes” in release announcements.
For our todo, release notes can be short. What matters is not the amount of text, but the structure. A convenient format is version, date (if you track it), and a few lines by meaning.
Here’s an example of what RELEASE_NOTES.md can look like:
# Release notes
## 1.3.0
Added:
- --version flag (shows version, commit, and build date)
Fixed:
- clearer error messages for an invalid task id
Changed:
- task list output became more stable (sorting)
If you noticed, there are bullet points—and yes, sometimes it’s genuinely worse without them: change notes are easier to scan “line by line”. Inside the lecture we generally avoid lists, but release notes are exactly the genre where short bullets are appropriate, because it’s not an “explanation”, it’s a “change log”.
9. What the binary must be able to do inside a bundle
This section matters because it connects “files next to it” and “program behavior”. From the user’s point of view, a release bundle is not only the README. It’s also confidence that the binary itself doesn’t behave like a capricious cat: “today I run, tomorrow I don’t feel like it”.
That’s why a CLI almost always should have two “diagnostic” branches that don’t depend on the rest of the logic:
- --help (or help as a command) — shows usage.
- --version — shows the version.
We already implemented --version. Now we need to make sure --help is also friendly, and that help/usage doesn’t hide important information.
Here’s a small example of how you can wire flag.Usage to “About” from buildinfo. This isn’t the “only correct” option, but it shows the idea well: help is part of the contract.
package main
import (
"flag"
"fmt"
"os"
"example.com/todo/internal/buildinfo"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, buildinfo.About())
fmt.Fprintln(os.Stderr, "Usage: todo [flags] <cmd> [args]")
flag.PrintDefaults()
}
flag.Parse()
}
The user types todo --help and sees not a soulless “Usage”, but also the product’s identity. And if they then send you a screenshot, you already have version/commit right in that text.
A simple release bundle model in an engineer’s head
It’s useful to lock in a model here so you don’t slide back into “I just built a binary”. You can imagine the process as a pipeline, even if you do it by hand in three commands.
flowchart TD
A[Sources + go.mod + dependencies] --> B[go build]
B --> C["Binary (target GOOS/GOARCH)"]
C --> D[Check: --version / --help]
D --> E[Release folder: binary + README + examples + notes]
The most interesting part here is the “check” step. Psychologically, it turns the binary into an artifact: you didn’t just get a file—you verified that it self-describes and that a minimal “map of the terrain” is next to it.
10. Common mistakes when preparing a release bundle
Error No. 1: thinking that a release is “upload the binary”.
This usually ends with users writing “doesn’t work”, and you start extracting information from them with pliers: what OS, what version, how they ran it. A release bundle exists specifically so the minimal answers are nearby: in the README and in --version/--help.
Error No. 2: not distinguishing artifacts by platforms and architectures.
When todo for linux/amd64 and todo for darwin/arm64 sit next to each other with the same name, someone will definitely run the “wrong one”. And it will be that same person who later says “your Go is weird”. The fix is boring but correct naming: app_version_GOOS_GOARCH, and .exe for Windows.
Error No. 3: making the README “about everything”, but without a copyable quickstart.
Paradoxically, a long description is worse than a short set of commands. The user doesn’t want philosophy; they want “give me 3 commands so I can see results”. If quickstart is missing, the person most often closes the folder. Or opens the source. And then they don’t close it… because they’re fixing your UX.
Error No. 4: hiding examples in text instead of providing examples/.
When examples exist only inside the README, they’re harder to maintain and harder to reference in support: “open examples/demo-session.txt and run the commands”. The examples/ folder creates a feeling that the project was used by “someone other than the authors”.
Error No. 5: having no release notes and then not remembering what changed.
This is classic not only for users, but also for the author. You made “a small fix”, shipped a new file, and two weeks later you can’t explain how 1.3.0 differs from 1.2.9. A short RELEASE_NOTES.md disciplines both you and the project.
GO TO FULL VERSION