Автонумерация сборок Go: решаем проблему версионирования раз и навсегда

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ручное версионирование — это боль каждого 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 или чтение файлов замедляют старт приложения. Вс

Блоги

Рекомендаций по книгам