JavaRush /Курсы /Go SELF /Release checklist: сборка, smoke‑tests, документация

Release checklist: сборка, smoke‑tests, документация

Go SELF
38 уровень , 3 лекция
Открыта

1. Введение

Когда проект маленький, кажется, что “релиз” — это просто “я пушнул, оно собралось”. Но как только появляется второй человек, вторая платформа, второй сценарий запуска или просто второй день после написания кода, выясняется, что память — плохая система управления качеством. Release checklist нужен, чтобы качество было процедурой, а не настроением.

Release checklist — это не “ещё одна бумажка для бюрократии”. Это договор команды (даже если команда — это вы и ваш кот), что релиз делается одинаково: с одними и теми же проверками, в одном и том же порядке, с понятным результатом “готово/не готово”. В идеале checklist должен быть настолько простым, чтобы его можно было выполнить, даже если вы не выспались и случайно открыли IDE не с той стороны.

Важно различать два близких понятия. Quality gate отвечает на вопрос: “можно ли это вообще мёржить/считать готовым изменением?”. Release checklist отвечает на вопрос: “можно ли это выпускать как версию, на которую будут ориентироваться пользователи/коллеги/скрипты/прод?”.

Каким должен быть хороший checklist

Хороший release checklist — это список пунктов, которые можно выполнить и одинаково интерпретировать. Формулировка “проверь, что всё норм” — это не пункт, это просьба к вселенной. Формулировка “go test ./... проходит” — это пункт, потому что у него есть бинарный ответ.

Чтобы было проще удерживать качество чеклиста, удобно думать, что каждый пункт отвечает на один из трёх вопросов: “мы это соберём?”, “оно работает хотя бы в базовом сценарии?”, “пользователь поймёт, как этим пользоваться и что изменилось?”. Когда пункты начинают повторять друг друга или становятся слишком общими, чеклист превращается в тот самый листок, который все подписывают не читая — то есть в декорацию.

Ниже — пример “формата” мысли (не как строгий шаблон, а как ориентир):

Пункт Что подтверждаем Что считаем результатом
Сборка проект компилируется в нужной конфигурации бинарник/артефакт собран без ошибок
Smoke‑test базовый сценарий не сломан быстрый запуск “минимального пути” успешен
Документация инструкции и примеры соответствуют реальности README/GoDoc/Examples не врут
Совместимость контракт не сломан внешнее поведение не “поехало”

2. Сборка: “компилируется” — фундамент релиза

Сборка кажется скучной ровно до того момента, пока она не ломается за 15 минут до демонстрации. Тогда внезапно выясняется, что “скучное” — это то, что вы не хотите чинить в пятницу вечером. Сборка в релизном чеклисте — это подтверждение того, что проект собирается из чистого состояния и что сборка не зависит от магии вашей машины.

В учебном приложении (пусть это будет наш CLI‑трекер задач tasker) сборка обычно означает, что пакет main в cmd/tasker компилируется, а зависимости подтягиваются корректно. В простейшем виде вы проверяете сборку командой go build, но важнее смысл: вы обязаны увидеть ошибку сборки до релиза, а не от пользователя, который просто хотел попробовать вашу программу.

Мини‑пример: “мейн, который обязан собираться” (файл cmd/tasker/main.go):


package main

import (
	"fmt"
)

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

Да, это смешно простое. Но именно такими кусочками кода компилятор говорит вам правду: “всё, что импортируется, существует; всё, что вызывается, существует; синтаксис честный”.

Проблема, которая часто всплывает на релизе, — случайные зависимости от локальных настроек. Например, в одном окружении у вас “подхватывается” файл, который не закоммичен, или сборка проходит, потому что IDE что‑то генерирует. Release checklist как раз должен вытаскивать такие баги на свет.

3. Smoke‑tests: быстрый sanity‑check, который спасает от позора

Smoke‑test (он же “дымовой тест”) — это не замена unit‑тестам и не попытка “протестировать всё”. Это короткая проверка, что приложение хотя бы запускается и делает один‑два ключевых действия. Почему “дымовой”? Потому что если из устройства сразу пошёл дым — неважно, насколько идеально вы покрасили корпус.

Смысл smoke‑test в релизном чеклисте особенно хорошо виден на CLI‑программах. Тесты пакетов могут быть зелёными, но пользователь запускает tasker и получает, например, пустой вывод или ошибку аргументов. Smoke‑test ловит такие вещи, потому что он проверяет вертикальный сценарий: от входа до выхода.

Для учебного tasker мы можем сделать smoke‑test как обычный Go‑тест, который вызывает “точку входа” вашего CLI‑слоя (или app‑слоя) и сравнивает вывод. Важно, что это должно быть очень быстро и очень стабильно.

Мини‑пример smoke‑теста, который не требует никаких сложных инструментов (файл cmd/tasker/smoke_test.go):

package main

import (
	"strings"
	"testing"
)

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

	// Представим, что mainLogic пишет в out.
	mainLogic(out)

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

И рядом — минимальная логика, которую мы “дёргаем” тестом (файл cmd/tasker/logic.go):

package main

import (
	"fmt"
	"io"
)

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

Обратите внимание на две вещи. Во‑первых, мы не запускаем процесс и не делаем ничего “тяжёлого”: smoke‑test должен быть быстрым, иначе он перестанет быть smoke‑test и начнёт быть “маленьким интеграционным адом”. Во‑вторых, мы специально сделали инъекцию io.Writer, потому что писать прямо в os.Stdout неудобно для тестов.

На практике smoke‑tests могут быть и ручными (“запусти бинарник и проверь три команды”), но в релизном чеклисте лучше любить автоматизируемое, потому что руки устают, а go test — нет.

4. Документация: README, GoDoc и примеры, которые не врут

Документация в чеклисте выглядит странно до первого случая, когда вы выпускаете версию, а пользователь делает ровно то, что написано в README — и получает ошибку. В этот момент вы понимаете философскую глубину фразы “документация — это тоже интерфейс”. Если интерфейс врёт, виноват не пользователь: виноват интерфейс.

В Go есть особенно приятная штука: пример может быть тестом. То есть вы не просто пишете “как пользоваться”, а пишете “как пользоваться так, чтобы это компилировалось и давало ожидаемый stdout”. Такой подход официально поддержан: Example...-функции в _test.go компилируются и (если есть // Output:) выполняются и проверяются.

Представим, что в нашем приложении есть пакет task, который форматирует задачу для списка.

Мини‑пример “исполняемой документации” (файл task/example_test.go):

package task

import "fmt"

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

Это одновременно документация (её увидят люди) и тест (её проверит go test). И это шикарно, потому что “пример протух” становится не ситуацией “кто‑то когда‑то заметит”, а красным тестом прямо перед релизом.

README в релизном чеклисте обычно проверяют не по принципу “он красивый”, а по принципу “он запускаемый”. Если в README есть команда установки и запуска, она должна быть актуальна. Если есть примеры ввода/вывода, они должны совпадать с текущим поведением. Если есть описание флагов, оно должно быть синхронизировано с реальным CLI.

5. Совместимость: что значит “не сломать контракт”

Совместимость — это самая коварная часть релиза, потому что компилятор может быть доволен, тесты могут быть довольны, а пользователь — нет. Причина простая: вы тестируете вашу кодовую базу, а пользователь тестирует свою кодовую базу, которая зависит от вашей. И вот у него‑то тесты могут упасть, даже если у вас “всё зелёное”.

В Go совместимость тесно связана с идеей экспортируемого API. У Go‑команды есть много материалов о том, почему “нельзя просто так взять и поменять экспортируемое”: даже изменение типа значения при сохранении похожего набора методов может ломать чужой код, потому что чужой код может требовать конкретный тип. Это звучит абстрактно, но на практике встречается постоянно: меняете структуру, меняете сигнатуру, меняете текст ошибок — и ломаете сценарии.

Сама идея совместимости в Go сформулирована трезво: нельзя гарантировать, что никакое изменение никогда ничего не сломает, но можно очень сильно уменьшать вероятность и быть аккуратными с контрактом.

Чтобы почувствовать, что такое “ломающее изменение”, посмотрим на простой пример на уровне пакета task.

Допустим, у вас был такой тип:

package task

type Task struct {
	Title string
	Done  bool
}

Пользователь мог написать:

package main

import "example.com/tasker/task"

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

А теперь вы решили “улучшить модель” и спрятать поля, сделать инварианты, добавить методы. Это может быть отличное решение, но для пользователя это уже другой контракт:

package task

type Task struct {
	title string
	done  bool
}

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

Ваша программа стала “лучше устроена”, но чужой код не компилируется. Это и есть breaking change. И вот здесь release checklist должен заставить вас хотя бы задать себе вопрос: “мы готовы ломать внешних пользователей, или это библиотека только для нас?”.

Совместимость — это не только “экспортируемые имена”. Это ещё и наблюдаемое поведение: формат вывода, стабильность порядка, тексты ошибок, возможность различать ошибки через errors.Is/errors.As. Например, если внешний код распознаёт ErrNotFound, то изменение на “возвращаем fmt.Errorf("not found") без причины” меняет контракт. Даже если пользователю “по смыслу всё равно”, его код уже мог принимать решения по errors.Is.

6. Пример release checklist для учебного проекта

Хороший чеклист не обязан быть сложным. Он обязан быть явным. Его можно хранить в README, в docs/release.md, в wiki, даже в комментарии — место вторично. Важно, чтобы шаги были фиксированы и повторяемы.

Ниже — пример “человеческого” чеклиста для tasker. Он нарочно короткий: релизный процесс должен помогать выпускать, а не мешать выпускать.

Release checklist (tasker):
1) Форматирование: gofmt/goimports (чтобы дифф был чистым)
2) Тесты: go test ./... (включая Example-тесты)
3) Анализ: go vet ./...
4) Линтеры проекта (если подключены) без новых критичных замечаний
5) Сборка бинарника: go build ./cmd/tasker
6) Smoke-test: минимальный запуск и один базовый сценарий
7) Документация: README/GoDoc/примеры соответствуют текущему поведению
8) Совместимость: нет неожиданных breaking changes в экспортируемом API и ошибках

В этом тексте нет магии. Здесь есть только одно важное качество: если завтра другой человек сделает релиз, он сделает его так же.

Для наглядности можно представить процесс релиза как “трубу”, где каждый шаг отсекает свой класс проблем:

flowchart TD
    A[Код готов] --> B[Форматирование]
    B --> C[Тесты]
    C --> D[go vet]
    D --> E[Линтеры]
    E --> F[Сборка]
    F --> G[Smoke-test]
    G --> H[Документация]
    H --> I[Проверка совместимости]
    I --> J[Релиз]

Здесь порядок не случаен: сначала убираем шум, потом проверяем поведение, потом ищем вероятные дефекты, потом подтверждаем, что артефакт действительно собирается и что у пользователя не будет ощущения “я вообще не понял, как этим пользоваться”.

7. Типичные ошибки при подготовке релиза

Ошибка №1: считать, что “зелёные тесты” автоматически означают “можно релизить”.
Тесты проверяют то, что вы написали в тестах. Они не обязаны проверять сборку бинарника, базовый сценарий запуска, корректность README и тем более совместимость внешнего API. Релиз ломается не потому, что тесты плохие, а потому что релиз — шире, чем unit‑тесты.

Ошибка №2: делать smoke‑test слишком тяжёлым и медленным.
Дымовой тест, который занимает минуту, быстро превращается в “ну давайте пропустим, времени нет”. Smoke‑test должен быть быстрым и немного туповатым: запуститься, сделать минимум, убедиться, что приложение не развалилось на старте. Чем сложнее smoke‑test, тем больше шанс, что он будет flaky.

Ошибка №3: держать документацию отдельно от кода, без проверки.
Если README и примеры не проверяются, они неизбежно устаревают. Это не моральная оценка авторов, это закон энтропии. В Go особенно обидно игнорировать Example‑тесты, потому что язык и tooling прямо подталкивают делать документацию исполняемой.

Ошибка №4: “чуть‑чуть” менять экспортируемый API, не считая это важным.
Экспортируемое “дорого”: оно живёт в чужом коде. Даже изменение типа при сохранении похожего поведения может сломать программы, которые завязались на конкретный тип. Если проект предполагает внешних пользователей, к экспортируемому нужно относиться как к публичному обещанию.

Ошибка №5: полагаться на память вместо явного процесса.
Самая частая причина пропущенного шага — не злой умысел и не лень, а банальная усталость. Checklist нужен именно для этого: чтобы релиз делал не “самый внимательный человек в мире”, а обычный человек, который хочет спать, есть и не чинить прод в 02:40 ночи.

1
Задача
Go SELF, 38 уровень, 3 лекция
Недоступна
Чеклист релиза
Чеклист релиза
1
Задача
Go SELF, 38 уровень, 3 лекция
Недоступна
Генератор чеклиста
Генератор чеклиста
1
Задача
Go SELF, 38 уровень, 3 лекция
Недоступна
Дымовой сценарий
Дымовой сценарий
1
Задача
Go SELF, 38 уровень, 3 лекция
Недоступна
Цена по каталогу
Цена по каталогу
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ