JavaRush /Courses /Go SELF /Release ritual: fmt/test/vet/lint + smoke scenarios

Release ritual: fmt/test/vet/lint + smoke scenarios

Go SELF
Level 55, Lesson 4
Available

1. Why you need a release ritual

When you are just learning, it feels like “works on my machine” is basically a release. But reality is harsh: the code can run for you and crash for a user because you forgot to format files, skipped a test, accidentally broke the output contract, or missed a vet warning. A release ritual is not magic and not bureaucracy; it is a sequence of the same checks you do before telling the world: “this version is safe to take.”

Imagine you are shipping a small CLI tool (our learning task manager). For the user, three things matter: the binary starts, commands work, errors are understandable, and exit codes are stable. For you as a developer, one more thing matters: if something broke, you find out before the release, not from an angry message like “everything crashed at 3:00 a.m.”

The ritual is good because it is repeatable. Ideally, you have one “pipeline” (locally and in CI), and it always does the same thing.

Let’s sketch the pipeline idea:

flowchart TD
    A[Code changed] --> B[gofmt / goimports]
    B --> C[go test ./...]
    C --> D[go vet ./...]
    D --> E[linters]
    E --> F[build]
    F --> G[smoke scenarios]
    G --> H[release]

The point is simple: if you fail an early step, later ones are pointless. Formatting did not pass — tests will still trigger reviewer complaints. Tests failed — smoke scenarios will not guarantee anything.

2. Quality gate: what we check and why the order matters

Before a release, it is useful to think not “what else should I run,” but “which risks am I closing.” Each step of the ritual catches its own class of problems. Formatting catches style chaos and indirectly prevents meaningless Git conflicts. Tests catch logical regressions and broken function contracts. go vet catches suspicious places that compile but smell like trouble (for example, incorrect Printf formats or mistakes in test infrastructure). Linters catch “seems fine, but better not”: dead code, odd constructs, potential bugs that are not always obvious in review.

What matters is that the order saves time and nerves. If you run a linter before formatting, the report will be full of noise and you will miss the important part. If you run smoke scenarios before tests, you can “click through” the happy path and still ship a version that breaks in edge cases.

It is convenient to keep this in a single table:

Step Command (idea) What it protects If it fails — what to do
Format
gofmt / goimports
readability, stable diffs format and commit the changes
Tests
go test ./...
logic regressions, output contract fix the code or the test, add a case
Vet
go vet ./...
“compiles, but risky” fix the warning; do not ignore it “for later”
Linter
staticcheck / golangci-lint
quality, risk of hidden bugs fix it or consciously suppress the rule
Smoke manual scenarios the “live” feel of the product lock in UX, contracts for stdout/stderr, exit codes

In this lecture, we will not turn this into a religious war about “which linter is better.” We treat the ritual as a habit: the same order, the same expectations.

3. Formatting: gofmt as part of the language

If you previously wrote in languages where code style is debated longer than architecture, gofmt looks like a life preserver: it just says “we do it this way,” and that’s it. In Go, formatting is not a personal preference and not “the team’s code style,” but a baseline ecosystem standard. That is why formatting is the first step in the release ritual: it is cheap, fast, and it removes noise before you start looking for real bugs.

In practice, this means that before a release you must make sure the repository is already formatted. In most teams this is automated (IDE, pre-commit hook), but the ritual is good because it does not require faith that “it will handle itself.”

To feel why this matters, recall that Go really likes code to be read the same way. Otherwise you spend brainpower on whitespace instead of meaning. And in programming your brain is already busy with more important things — like why a test fails only in CI.

A mini example from our CLI app. Imagine someone wrote this:

package apperr

type AppError struct{Kind Kind;Op string;Err error}

This compiles. But it looks like the author wrote the code in an elevator between floors. After gofmt it will be fine:

package apperr

type AppError struct {
	Kind Kind
	Op   string
	Err  error
}

The ritual here is very simple: before releasing, you bring the project to a state where gofmt changes nothing. Then the next step — tests — will not “drown” in meaningless diffs.

4. Tests: go test as contract insurance

Tests in Go are not “an optional extra for perfectionists,” but a normal way to avoid shipping a regression every 30 minutes. Especially in a CLI app, where it is important not only to “compute correctly,” but also to preserve the agreement: what we print to stdout, what goes to stderr, and which exit codes we return. The go test tool under the hood builds a test runner and runs package tests automatically, so you do not need to write a “main for tests” by hand.

For the release ritual, one simple idea matters: a release without green tests is not courage; it is gambling. And the house, as we know, almost always wins.

It is also useful to remember example tests: examples that are both tests and documentation. They run via go test and compare stdout against the comment // Output:. This fits CLI logic very well, where we want to lock in message formatting and not break it with an accidental refactor.

Let’s make a small example test for our user-message function (say apperr.UserMessage). The code is short, but it locks in the format:

package apperr_test

import (
	"errors"
	"fmt"

	"example/apperr"
)

func ExampleUserMessage_validation() {
	err := apperr.Validation("parse args", errors.New("missing -title"))
	fmt.Println(apperr.UserMessage(err)) // invalid input: missing -title
	// Output: invalid input: missing -title
}

Notice an important psychological effect. The test does not “prove mathematically” that everything is good, but it catches the most dangerous thing: you changed the format, scripts started parsing it differently, and users noticed before you did. The pre-release ritual guarantees such tests are always run.

5. go vet: when the compiler is silent but common sense coughs

go vet is not a linter in the “I do not like your code” sense. It is a tool that looks for common dangerous mistakes and suspicious constructs. Its main strength is that it catches things that compile but can lead to surprises. Before a release, this is especially important: such bugs like to show up for users, not for the author, because a user will always find the most awkward input and the least suitable platform.

A good example is Printf formatting. The code can “sort of” work, but the output will be wrong or even break at runtime. vet can highlight mismatches between placeholders and arguments. This does not guarantee you are always correct, but it greatly reduces the number of silly mistakes.

A mini example (it compiles, but vet will be unhappy):

package main

import "fmt"

func main() {
	name := "Alice"
	fmt.Printf("hello %d\n", name) // vet: %d expects integer
}

It is also nice that in newer Go versions vet gets smarter specifically in the testing area: analyzers appear that help find typical mistakes in test, benchmark, fuzz-test, and example-test declarations. This matters for a release because a broken test can look like “the test just did not run,” and you may mistakenly think everything is green.

The ritual rule here is boring and useful: if go vet complains, we do not “remember it and fix later”; we fix it now. Before a release, “later” usually means “after users found the bug.”

6. Linters: protection from slow future problems

After gofmt, go test, and go vet, you often want to say: “okay, enough, I’m tired.” That feeling is normal. But linters are useful because they catch issues that rarely explode right now, but are excellent at accumulating technical debt. They find not only errors, but also signals: “this code is suspicious,” “this condition is always true,” “you are doing extra work,” “you are ignoring a returned error.”

If vet is a notary (checks obvious legal problems), then a linter is a meticulous editor who says: “here you can be misunderstood, and here in six months you will curse your past self.”

An example of a “small thing” that does not break the program but is unpleasant. Suppose someone in our CLI code did this:

package main

import "fmt"

func renderHeader() string {
	return fmt.Sprintf("Tasks") // redundant Sprintf; can just be "Tasks"
}

It is not a crime. But in a large project, hundreds of such “small things” accumulate, and the code starts to look like an attic: everything seems useful, but you cannot walk through it.

For the release ritual, discipline matters: the linter must run the same way every time, not “when you feel like it.” And it is very important not to go to extremes: if the linter suggests a debatable change, you should either fix the code so it becomes clearer, or consciously decide to keep it and, if needed, document a suppression. Otherwise, the linter turns into an irritation generator instead of a quality tool.

7. Smoke scenarios: a quick live run-through

Smoke tests (or smoke scenarios) are a short set of checks that confirm the application starts and executes key commands. This is not load testing, not “cover every branch,” and not an attempt to replace unit tests. It is more like asking: “if I hand the binary to a person, can they do basic things without falling into despair?”

Why do we need smoke scenarios if we have tests? Because tests usually verify details (functions, methods, formats), while a smoke scenario verifies the “glue”: the binary built, flags parse, help prints to the right place, the exit code really matches the policy, and errors did not leak into stdout.

The most useful part of smoke scenarios is to lock expectations down as concretely as possible: which commands to run and what roughly should happen (at least in terms of exit code and the presence of key strings).

Below is an example smoke table for our CLI task manager (let’s call the binary todo). Important: this is not a list of “ideas,” but a small pre-release agreement.

Scenario Command What we expect
Help works
todo -h
exit code 0, usage in stdout or stderr (as you agreed), no panic
Usage error
todo add
(without -title)
exit code 2, short "invalid input …" in stderr
Add a task
todo add -title "buy milk"
exit code 0, a clear result in stdout
List tasks
todo list
exit code 0, stable format (table/lines)
Mark done
todo done -id 1
exit code 0, task is marked
Not found (if you distinguish it)
todo done -id 99999
exit code 1 or a more detailed code (if you added one), message without internals
Version (if present)
todo --version
exit code 0, version is printed stably

Notice that smoke scenarios are intentionally short. Their strength is not depth but regularity. Before release, you do this “run-through” and immediately see: “oh, I accidentally started printing errors to stdout” or “I removed the -h flag and now people don’t understand how to use it.”

The doctor command as support for the smoke run

Sometimes you want the smoke run to be even simpler: one command that checks the basic health of the environment. For example, that the config is valid and the data file is accessible. Such a command is often called doctor, check, selftest. It does not replace tests and it does not replace CI, but for release it is convenient: you run todo doctor and immediately see “the minimum is alive.”

To avoid turning this into a separate product, let’s keep the command very simple: we will check that the path to the task database file exists (or that the directory exists), and if not, we will return KindValidation (exit code 2) with a clear message.

A small function example that fits our AppError style and does not smear os.Exit across the code:

package main

import (
	"errors"
	"os"

	"example/apperr"
)

func runDoctor(dataPath string) error {
	if dataPath == "" {
		return apperr.Validation("doctor", errors.New("missing data path"))
	}
	if _, err := os.Stat(dataPath); err != nil {
		return apperr.Validation("doctor", errors.New("data file is not accessible"))
	}
	return nil
}

Yes, this is intentionally simplified. We do not distinguish “file does not exist” vs “no permissions” — for a basic smoke scenario this is fine: the user should get a short “I cannot work with the file.” Details can remain in logs if you wrap the root cause more carefully.

Now you can add a row to the smoke table:

Scenario Command What we expect
Environment check
todo doctor -data ./tasks.json
exit code 0 with a correct environment, otherwise 2

And here the ritual starts working as a system: tests guarantee that ExitCode and UserMessage are not broken, vet guarantees the absence of common traps, linters smooth out rough edges, and doctor helps you quickly check the binary “live.”

8. Typical mistakes before a release

Before a release, people often make the same mistakes not because they are “bad developers,” but because the brain is tired, you want to move faster, and it feels like “good enough.” The ritual is exactly what you need to save yourself from yourself — especially on a Friday evening.

Mistake #1: releasing without a clean gofmt because “it doesn’t affect runtime.”
Formally, that’s true: the program will start. Practically, you create extra noise in diffs, make review harder, and then suddenly hit a formatting conflict at the worst possible moment. This is pain you can eliminate with one early step.

Mistake #2: “tests take a long time; I checked it manually.”
Manual checking catches the happy path but does not catch edge-case regressions. Tests — especially table-driven and example tests — are needed specifically to lock contracts in. In Go, examples can be tests with verified stdout, so “docs don’t get stale” is not a slogan; it is a real mechanism.

Mistake #3: ignoring go vet because “it’s not a compilation error.”
vet often complains about things that did not “blow up” today but will shoot you in the foot tomorrow. It is especially unpleasant when it concerns output formatting or test infrastructure. Given that go vet evolves and gains new analyzers for tests, ignoring it before a release is a bad investment.

Mistake #4: turning the linter into an enemy and disabling it entirely.
Linters should help, not humiliate. If it is noisy, that means either the chosen ruleset is too aggressive or the code really got dirty. The right strategy is to configure a reasonable minimum and keep it stable, not to turn everything off “until better times.”

Mistake #5: smoke scenarios without fixed expectations.
Such a smoke run is not repeatable and does not provide confidence. It is better to have a small table of 5–8 commands with expected exit codes and key strings than a “five-minute shaman ritual.”

1
Task
Go SELF, level 55, lesson 4
Locked
Release cheat sheet
Release cheat sheet
1
Task
Go SELF, level 55, lesson 4
Locked
Example contract
Example contract
1
Task
Go SELF, level 55, lesson 4
Locked
Scoreboard line
Scoreboard line
1
Task
Go SELF, level 55, lesson 4
Locked
Doctor command
Doctor command
1
Survey/quiz
Error standardization, 55 level, 4 lesson
Unavailable
Error standardization
Error standardization
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION