Почему ручное версионирование — это боль каждого Go-разработчика
Вы когда-нибудь ловили себя на мысли: «Какая версия сейчас работает на проде?» или тратили час, чтобы понять, какой коммит вызвал баг в бинарнике, собранном месяц назад? Если да — вы попали по адресу. Вопрос «Application auto build versioning» — один из самых популярных на Stack Overflow (тысячи просмотров и десятки решений). И не зря: в современной разработке, где CI/CD пайплайны гоняют сборки каждые 10 минут, ручное проставление версий — это путь к хаосу.
В этой статье мы разберем автоматическое версионирование сборок в Go от А до Я. Вы узнаете, как внедрить систему, которая сама проставляет номер версии, хеш коммита, дату сборки и даже флаг «грязного» репозитория. Код будет работать как в локальной разработке, так и в GitHub Actions, GitLab CI или Jenkins.
Проблема: что не так с ручным version.go?
Многие начинающие разработчики создают файл version.go и пишут:
package main
var Version = "1.0.0"
И это работает ровно до первого коммита. Потом начинается:
- «Я забыл обновить версию перед сборкой»
- «На проде одна версия, в коде — другая»
- «Какой коммит соответствует этой сборке?»
Проблема в том, что версия — это метаданные сборки, а не часть исходного кода. Она должна генерироваться автоматически на этапе компиляции.
Решение: ldflags — секретное оружие Go
Go-компилятор позволяет «подставлять» значения переменных во время сборки через флаг -ldflags. Это и есть auto build versioning в его классическом виде. Мы не храним версию в коде — мы передаём её компилятору.
Шаг 1: Определяем структуру версии
Создадим пакет version с переменными, которые будут заполняться на этапе сборки:
// version/version.go
package version
var (
// Version — семантическая версия (задаётся вручную или через git tag)
Version = "dev"
// Commit — короткий хеш коммита
Commit = "none"
// BuildTime — время сборки в формате ISO 8601
BuildTime = "unknown"
// IsDirty — флаг, что репозиторий содержит незакоммиченные изменения
IsDirty = "false"
)
func String() string {
return "v" + Version + " (commit: " + Commit + ", built: " + BuildTime + ")"
}
Шаг 2: Собираем с ldflags
Теперь самое интересное. Вместо go build мы будем использовать скрипт или Makefile, который вычисляет нужные значения и передаёт их компилятору:
#!/bin/bash
# build.sh
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none")
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
IS_DIRTY=$(git diff --quiet || echo "true")
go build -ldflags="\
-X 'main/version.Version=$VERSION' \
-X 'main/version.Commit=$COMMIT' \
-X 'main/version.BuildTime=$BUILD_TIME' \
-X 'main/version.IsDirty=$IS_DIRTY'" \
-o myapp .
Важно: Путь main/version.Version зависит от вашей структуры проекта. Если пакет version лежит в корне — пишите main/version.Version. Если в отдельной директории — github.com/ваш-username/проект/version.Version.
Шаг 3: Используем в приложении
Теперь в любом месте программы можно вывести версию:
// main.go
package main
import (
"fmt"
"main/version"
)
func main() {
fmt.Println("App version:", version.String())
fmt.Println("Is dirty repo:", version.IsDirty)
}
Соберите и запустите:
$ ./build.sh
$ ./myapp
App version: v1.2.3 (commit: a1b2c3d, built: 2024-03-15T10:30:00Z)
Is dirty repo: false
Продвинутый уровень: семантическое версионирование с git tags
Хотите, чтобы версия автоматически бралась из git-тегов? Используйте git describe. Если тег совпадает с коммитом — получите чистую версию. Если между тегом и HEAD есть коммиты — будет что-то вроде v1.0.0-3-g123abc (3 коммита после тега).
Дополнительно можно генерировать build metadata — например, имя ветки или номер билда CI:
// version/version.go (дополнение)
var (
Branch = "unknown"
BuildID = "0"
GoVersion = runtime.Version()
)
func FullInfo() string {
return fmt.Sprintf("%s | branch: %s | build: %s | go: %s",
String(), Branch, BuildID, GoVersion)
}
В скрипте сборки добавляем:
BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_ID=${CI_BUILD_ID:-0}
CI/CD: автоматическое версионирование в пайплайнах
Настоящая магия начинается, когда вы подключаете auto build versioning к CI/CD. Рассмотрим пример для GitHub Actions.
Пример: .github/workflows/release.yml
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # важно для получения тегов
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build with versioning
run: |
VERSION=$(git describe --tags --always --dirty)
COMMIT=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
go build -ldflags="\
-X 'main/version.Version=$VERSION' \
-X 'main/version.Commit=$COMMIT' \
-X 'main/version.BuildTime=$BUILD_TIME'" \
-o myapp .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: myapp-${{ github.ref_name }}
path: ./myapp
Теперь каждый новый тег v1.2.3 автоматически создаёт бинарник с правильной версией. Никаких ручных правок!
Лучшие практики и частые ошибки
- Не используйте runtime-функции для версионирования —
os.Execили чтение файлов замедляют старт приложения. Вс