JavaRush /Courses /Go SELF /Release checklist: build, smoke tests, documentation

Release checklist: build, smoke tests, documentation

Go SELF
Level 38, Lesson 3
Available

1. Introduction

When a project is small, it feels like a “release” is just “I pushed, it built.” But as soon as you get a second person, a second platform, a second run scenario, or simply the second day after writing the code, it turns out memory is a bad quality-management system. A release checklist exists so that quality is a procedure, not a mood.

A release checklist is not “just another piece of paperwork for bureaucracy.” It’s a team agreement (even if the “team” is you and your cat) that a release is done the same way: with the same checks, in the same order, with a clear result of “ready/not ready.” Ideally, the checklist should be so simple that you can complete it even if you didn’t sleep well and accidentally opened your IDE from the wrong side.

It’s important to distinguish two similar concepts. A quality gate answers: “can this be merged / considered a ready change at all?” A release checklist answers: “can this be shipped as a version that users/teammates/scripts/prod will rely on?”

What a good checklist should look like

A good release checklist is a list of items you can execute and interpret consistently. The wording “check that everything is fine” is not an item; it’s a request to the universe. The wording “go test ./... passes” is an item, because it has a binary answer.

To make checklist quality easier to maintain, it helps to think of each item as answering one of three questions: “can we build this?”, “does it work in at least the basic scenario?”, “will a user understand how to use it and what changed?” When items start repeating each other or become too vague, the checklist turns into that signature sheet everyone signs without reading—i.e., decoration.

Below is an example of the “thought format” (not a strict template, but a guideline):

Item What we confirm What we consider the result
Build the project compiles in the required configuration the binary/artifact is built without errors
Smoke test the basic scenario isn’t broken a quick run of the “minimum path” is successful
Documentation instructions and examples match reality README/GoDoc/Examples don’t lie
Compatibility the contract isn’t broken external behavior didn’t “shift” unexpectedly

2. Build: “it compiles” is the foundation of a release

Building seems boring right up until the moment it breaks 15 minutes before a demo. Then, suddenly, it turns out that “boring” is what you don’t want to fix on a Friday night. In a release checklist, the build step confirms that the project builds from a clean state and that the build does not depend on magic on your machine.

In a learning app (let it be our CLI task tracker tasker), “build” usually means that the main package in cmd/tasker compiles and dependencies are pulled correctly. In the simplest form, you verify the build with go build, but the meaning is more important: you must see a build error before the release, not from a user who just wanted to try your program.

Mini example: “a main that must build” (file cmd/tasker/main.go):


package main

import (
	"fmt"
)

func main() {
	fmt.Println("tasker: hello") // tasker: hello
}

Yes, it’s ridiculously simple. But this is exactly how the compiler tells you the truth: “everything you import exists; everything you call exists; the syntax is valid.”

A problem that often shows up during release is accidental dependencies on local settings. For example, in one environment you “pick up” a file that wasn’t committed, or the build passes because the IDE generates something. A release checklist should surface bugs like that.

3. Smoke tests: a quick sanity check that saves you from embarrassment

A smoke test (a.k.a. a “smoke test”) is not a replacement for unit tests and not an attempt to “test everything.” It’s a short check that the application at least starts and performs one or two key actions. Why “smoke”? Because if smoke starts coming out of a device immediately, it doesn’t matter how perfectly you painted the case.

The point of a smoke test in a release checklist is especially clear for CLI programs. Package tests may be green, but the user runs tasker and gets, for example, empty output or an argument error. A smoke test catches such things because it checks a vertical scenario: from input to output.

For the learning tasker, we can implement a smoke test as a regular Go test that calls the “entry point” of your CLI layer (or app layer) and compares the output. The important part is that it should be very fast and very stable.

A mini smoke-test example that doesn’t require any complex tools (file cmd/tasker/smoke_test.go):

package main

import (
	"strings"
	"testing"
)

func TestSmoke_Hello(t *testing.T) {
	out := new(strings.Builder)

	// Let's imagine that mainLogic writes to out.
	mainLogic(out)

	if got := out.String(); got != "tasker: hello\n" {
		t.Fatalf("got %q", got)
	}
}

And next to it—the minimal logic that we “poke” with the test (file cmd/tasker/logic.go):

package main

import (
	"fmt"
	"io"
)

func mainLogic(w io.Writer) {
	fmt.Fprintln(w, "tasker: hello") // tasker: hello
}

Notice two things. First, we’re not starting a process and not doing anything “heavy”: a smoke test must be fast, otherwise it will stop being a smoke test and start being “a small integration hell.” Second, we intentionally injected io.Writer, because writing directly to os.Stdout is inconvenient for tests.

In practice, smoke tests can also be manual (“run the binary and check three commands”), but in a release checklist it’s better to love what can be automated, because hands get tired and go test doesn’t.

4. Documentation: README, GoDoc, and examples that don’t lie

Documentation in a checklist looks strange until the first time you ship a version and a user does exactly what the README says—and gets an error. That’s when you understand the philosophical depth of “documentation is also an interface.” If the interface lies, it’s not the user’s fault; it’s the interface’s fault.

Go has a particularly nice thing: an example can be a test. That is, you don’t just write “how to use it,” you write “how to use it so it compiles and produces the expected stdout.” This approach is officially supported: Example... functions in _test.go are compiled and (if there is // Output:) executed and verified.

Let’s say our app has a task package that formats a task for a list.

Mini example of “executable documentation” (file task/example_test.go):

package task

import "fmt"

func ExampleTitleForList() {
	fmt.Println(TitleForList("buy milk", false))
	// Output: [ ] buy milk
}

This is both documentation (people will see it) and a test (it will be checked by go test). And that’s great, because “the example got stale” becomes not a “someone will notice someday” situation, but a failing test right before the release.

In a release checklist, README is usually checked not by “is it pretty,” but by “is it runnable.” If the README contains an install-and-run command, it must be current. If it contains input/output examples, they must match current behavior. If it describes flags, that description must be synced with the real CLI.

5. Compatibility: what it means to “not break the contract”

Compatibility is the trickiest part of a release, because the compiler can be happy, tests can be happy, and the user can still be unhappy. The reason is simple: you test your codebase, while the user tests their codebase that depends on yours. And their tests may fail even if everything is “green” on your side.

In Go, compatibility is tightly tied to the idea of an exported API. The Go team has lots of material on why “you can’t just change what’s exported”: even changing the value type while keeping a similar set of methods can break someone else’s code, because their code may require a specific type. It sounds abstract, but it happens all the time in practice: you change a struct, change a signature, change error text—and you break scenarios.

The very idea of compatibility in Go is stated soberly: you can’t guarantee that no change will ever break anything, but you can greatly reduce the risk and be careful with the contract.

To feel what a “breaking change” is, let’s look at a simple example at the task package level.

Suppose you had this type:

package task

type Task struct {
	Title string
	Done  bool
}

A user could write:

package main

import "example.com/tasker/task"

func markDone(t task.Task) task.Task {
	t.Done = true
	return t
}

And now you decided to “improve the model” by hiding fields, enforcing invariants, adding methods. That may be a great decision, but for the user it’s already a different contract:

package task

type Task struct {
	title string
	done  bool
}

func (t Task) Title() string { return t.title }

Your program is “better structured,” but someone else’s code no longer compiles. That’s a breaking change. And this is where the release checklist should force you to at least ask yourself: “are we ready to break external users, or is this library only for us?”

Compatibility is not only about “exported names.” It’s also about observable behavior: output format, stability of ordering, error texts, and the ability to distinguish errors via errors.Is/errors.As. For example, if external code recognizes ErrNotFound, then changing it to “return fmt.Errorf("not found") for no reason” changes the contract. Even if the user “doesn’t care in meaning,” their code may already make decisions based on errors.Is.

6. Example release checklist for a learning project

A good checklist doesn’t have to be complex. It has to be explicit. You can keep it in README, in docs/release.md, in a wiki, even in a comment—the location is secondary. What matters is that the steps are fixed and repeatable.

Below is an example of a “human” checklist for tasker. It’s intentionally short: the release process should help you ship, not get in the way of shipping.

Release checklist (tasker):
1) Formatting: gofmt/goimports (so the diff is clean)
2) Tests: go test ./... (including Example tests)
3) Analysis: go vet ./...
4) Project linters (if enabled) with no new critical findings
5) Build the binary: go build ./cmd/tasker
6) Smoke test: minimal startup and one basic scenario
7) Documentation: README/GoDoc/examples match current behavior
8) Compatibility: no unexpected breaking changes in exported API and errors

There’s no magic in this text. There’s only one important property: if tomorrow another person makes a release, they will do it the same way.

For clarity, you can think of the release process as a “pipeline,” where each step filters out its own class of problems:

flowchart TD
    A[Code ready] --> B[Formatting]
    B --> C[Tests]
    C --> D[go vet]
    D --> E[Linters]
    E --> F[Build]
    F --> G[Smoke test]
    G --> H[Documentation]
    H --> I[Compatibility check]
    I --> J[Release]

The order isn’t accidental: first we remove noise, then we check behavior, then we search for likely defects, then we confirm the artifact truly builds and the user won’t feel like “I don’t understand how to use this at all.”

7. Common mistakes when preparing a release

Mistake #1: assuming “green tests” automatically means “safe to release.”
Tests check what you wrote in tests. They are not required to check building the binary, the basic startup scenario, README correctness, or especially external API compatibility. Releases break not because tests are bad, but because a release is broader than unit tests.

Mistake #2: making the smoke test too heavy and too slow.
A smoke test that takes a minute quickly turns into “let’s skip it, we don’t have time.” A smoke test must be fast and somewhat dumb: start, do the minimum, confirm the app didn’t fall apart on startup. The more complex the smoke test, the higher the chance it will be flaky.

Mistake #3: keeping documentation separate from code, with no verification.
If README and examples are not checked, they inevitably become outdated. This is not a moral judgement of authors; it’s the law of entropy. In Go, it’s especially painful to ignore Example tests, because the language and tooling directly nudge you toward executable documentation.

Mistake #4: “slightly” changing the exported API without considering it important.
Exported is “expensive”: it lives in other people’s code. Even changing a type while keeping similar behavior can break programs that rely on a specific type. If a project has external users, exported things should be treated like a public promise.

Mistake #5: relying on memory instead of an explicit process.
The most common reason for skipping a step is not malice and not laziness, but plain fatigue. That’s exactly why a checklist exists: so the release is done not by “the most attentive person in the world,” but by a normal person who wants to sleep, eat, and not fix prod at 02:40 at night.

1
Task
Go SELF, level 38, lesson 3
Locked
Release checklist
Release checklist
1
Task
Go SELF, level 38, lesson 3
Locked
Checklist Generator
Checklist Generator
1
Task
Go SELF, level 38, lesson 3
Locked
Smoke scenario
Smoke scenario
1
Task
Go SELF, level 38, lesson 3
Locked
Catalog price
Catalog price
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION