Публикация Go приложения в GitHub

  • Tutorial

Пост представляет собой контрольный список (checklist) и его реализацию при публикации Go приложения на Github'е.

TLDR:

  • Makefile как входная точка для выполнения основных действий

  • Линтеры и тесты как инструменты повышающие качество кода

  • Dockerfile как способ распространения приложения

  • Github Actions как возможность автоматической сборки и выкладки приложения при новых изменениях

  • Goreleaser как инструмент для публикации релизов и пакетов

Результатом должно быть опубликованное приложение с настроенными инструментами которые позволяют легко сопровождать приложение в процессе его существования. В качестве реального примера я буду рассматривать утилиту pgcenter.


Disclaimer: Приведенный список не являются абсолютной истинной и является лишь субъективным списком вещей к которым я пришел в процессе публикации приложении. Список может дополняться и изменяться. Все это одно большое ИМХО, если у вас есть альтернативный взгляд или вы уверены/знаете что какие-то вещи можно сделать еще лучше, обязательно дайте знать в комментах.

Будем двигаться от простого к сложному. В случае встречи имен файлов, все они взяты относительно корня проекта где находится код приложения.

Makefile

Makefile хранит в себе служебные сценарии которые приходится часто выполнять при разработке приложения:

  • выполнение линтеров и тестов

  • сборка приложения

  • запуск приложения

  • сборка артефактов типа пакетов, docker образов и т.п.

  • публикация артефактов в сторонние репозитории

  • выполнение операций относительно внешних систем, например SQL миграции если речь идет о корпоративных приложениях В Makefile складываются рутинные операции выполнять которые приходится регулярно. Основная цель Makefile это помочь вам не держать в голове все нужные команды, параметры и аргументы, а собрать и их в одном месте и при необходимости выполнить их и получить результат. Позже Makefile также будет основным сценарием для запуска этих же рутинных процедур в CI/CD. Минимальная версия Makefile может выглядеть так:

PROGRAM_NAME = pgcenter

COMMIT=$(shell git rev-parse --short HEAD)
BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
TAG=$(shell git describe --tags |cut -d- -f1)

LDFLAGS = -ldflags "-X main.gitTag=${TAG} -X main.gitCommit=${COMMIT} -X main.gitBranch=${BRANCH}"

.PHONY: help clean dep build install uninstall 

.DEFAULT_GOAL := help

help: ## Display this help screen.
	@echo "Makefile available targets:"
	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "  * \033[36m%-15s\033[0m %s\n", $$1, $$2}'

dep: ## Download the dependencies.
	go mod download

build: dep ## Build pgcenter executable.
	mkdir -p ./bin
	CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${PROGRAM_NAME} ./cmd

clean: ## Clean build directory.
	rm -f ./bin/${PROGRAM_NAME}
	rmdir ./bin

Давайте разберем важные моменты в приведенном Makefile.

  • В начале файла указываем служебные переменные которые потребуются дальше. Переменных немного, называние программы и информация из Git которая используется при сборке. В частности коммит, тег и ветка которые через сборщика пробрасываются в код приложения и используются для формирования версии программы. При таком подходе версия всегда берется из Git и не нужно хранить строку с версией в коде приложения, помнить о ней, своевременно обновлять. Отмечу что проброс переменных не автоматическая магия, предполагается что в Git используются теги и также есть соответствующие правки в коде для приема Git переменных. Например 1 и 2.

  • Следующий интересный момент это пункт help, он реализует справку для нашего Makefile - обратите внимание на комментарии начинающиеся с двойной решетки после названий пунктов. Эти комментарии как раз и используются в качестве справки если вызвать make без аргументов или явно make help.

  • Следующие пункты являются базовыми для жизненного цикла Go приложения: загрузка зависимостей, сборка и очистка каталога где хранится собранная программа.

При желании список пунктов можно дополнить, что я и буду делать дальше по ходу текста.

Линтеры и тесты

Следующий шаг добавление линтеров. Основная задача линтеров проверять код на предмет "странных" конструкций которые не соответствуют принятым стилям программирования или даже хуже, могут быть неоптимальными и приводить к каким-либо ошибкам. Наличие линтеров позволяют поддерживать хорошее качество кода, ориентироваться на правильный стиль написания кода принятый в языке. Использование линтеров необязательно, но крайне желательно. В go есть масса разных линтеров, я пришел пока к использованию golangci-lint и gosec. Первый включает в себя большой набор разных линтеров (включены при этом не все), второй является линтером с уклоном в соблюдение правил информационной безопасности.

Выполнение линтеров также регулярная задача, поэтому также добавляем их в Makefile

lint: dep ## Lint the source files
	golangci-lint run --timeout 5m -E golint
	gosec -quiet ./...

В приведенном случае для выполнения golangci-lint выставлен таймаут, через флаг -E включаются дополнительные линтеры. Выполнение gosec особо ничем не примечательно, просто рекурсивный обход каталогов.

Очевидно что golangci-lint и gosec должны быть установлены в системе. Их установка проста, описывать тут не буду.

Также код сопровождается тестами, добавим и их выполнение тоже.

test: dep ## Run tests
	go test -race -p 1 -timeout 300s -coverprofile=.test_coverage.txt ./... && \
    	go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $$3}'
	@rm .test_coverage.txt

Команда запускает тесты, формирует файл с покрытием кода этими тестами и печатает результат покрытия.

Dockerfile

Следующим этапом является Dockerfile, который позволит собирать Docker образы нашего приложения.

# stage 1: build
FROM golang:1.15 as build
LABEL stage=intermediate
WORKDIR /app
COPY . .
RUN make build

# stage 2: scratch
FROM scratch as scratch
COPY --from=build /app/bin/pgcenter /bin/pgcenter
CMD ["pgcenter"]

Сборка образа состоит из двух этапов, сборка приложения и его копирование в пустой образ. Таким двухэтапным образом можно упаковывать приложения которые не требуют внешних зависимостей типа библиотек, сертификатов, других программ или чего-то аналогичного.

Помимо сборки Docker образа потребуется место откуда другие пользователи смогут его забрать, например этим местом может быть Docker Hub. Для размещения там образа потребуется аккаунт и реквизиты (логин/пароль).

Сборка и публикация образа также регулярная операция, поэтому добавляем команды в Makefile.

docker-build: ## Build docker image
	docker build -t lesovsky/pgcenter:${TAG} .
	docker image prune --force --filter label=stage=intermediate

docker-push: ## Push docker image to registry
	docker push lesovsky/pgcenter:${TAG}

Обратите внимание, что используется переменная TAG которая определяется в начале Makefile.

Github Actions

Теперь когда у нас есть способ для сборки приложения (Makefile) и публикации (Dockerfile), нам нужен механизм который поможет автоматически выполнять сборку и публикацию обновлений приложения. Здесь нам поможет Github Actions.

Однако в этом месте мы начинаем соприкасаться с организацией процесса, как новые изменения становятся частью существующего кода. Тема довольно обширная, об этом написано много постов, выработаны целые подходы со своими правилами. Поэтому те кто в теме уже и так все знают, а кто не в теме пусть отправляется на поиски чтива Git Flow, Github Flow, Gitlab Flow и их сравнение друг с другом.

В нашем случае все просто. Все изменения будут вливаться в master ветку. От ветки master мы создадим release ветку которая и будет источником релизов. Когда мы захотим сделать релиз, просто создадим новый тег и подтянем все изменения из master в release.

Теперь когда у нас есть понимание того как вносить изменения и делать релизы можно перейти к настройке workflow в Github Actions. Кто знает нормальный однословный русский перевод слову wokflow дайте знать в комментариях.

Будет два workflow:

  • Default (.github/workflows/default.yml) - это flow по-умолчанию, выполняет тесты при поступлении новых изменений.

  • Release (.github/workflows/release.yml) - это релизный flow делает тесты, сборку Docker образов и пакетов для пакетных менеджеров.

Этот workflow запускается при наступлении push и pull request событий в ветке master. Здесь всего одна задача (job), запуск линтеров и тестов. Github Actions имеет хорошие возможности для кастомизации и позволяют описывать очень сложные сценарии. В нашем случае таким примером кастомизации является запуск и выполнение в специально подготовленном контейнере где есть все необходимое для осуществления тестов.

Второй workflow.

---
name: Release

on:
  push:
    branches: [ release ]

jobs:
  test:
    runs-on: ubuntu-latest
    container: lesovsky/pgcenter-testing:v0.0.1

    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Prepare test environment
        run: prepare-test-environment.sh
      - name: Run lint
        run: make lint
      - name: Run test
        run: make test

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Build image
        run: make docker-build
      - name: Log in to Docker Hub
        run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
      - name: Push image to Docker Hub
        run: make docker-push

  goreleaser:
    runs-on: ubuntu-latest
    needs: [ test, build ]
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - uses: actions/setup-go@v2
        with:
          go-version: 1.15
      - uses: goreleaser/goreleaser-action@v2
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Релизный workflow чуть больше, и запускается он при push событиях в release ветке. Этот workflow также включает в себя выполнение линтеров и тестов. Но также тут есть еще две задачи - build и goreleaser.

В задаче build выполняется сборка и публикация Docker образа. Обратите внимание, что используются секреты, которые предварительно нужно указать в разделе Secrets, в настройках Github репозитория.

В задаче goreleaser выполняется публикация релиза в разделе Releases репозитория. Настройки goreleaser определим позже. Здесь также используется секрет GITHUB_TOKEN его указывать нигде не нужно, он создается автоматически для нужд workflow.

Goreleaser

Последний шаг это публикация релиза и дополнительная сборка пакетов. Кроме Docker образов существуют способы распространения с помощью пакетных менеджеров. Наиболее распространенные это deb и rpm пакеты. Есть и другие варианты, но для меня они экзотичны
и их я рассматривать не буду. Для всего этого нам потребуется goreleaser который и сделает всю работу по сборке. Настройки определяются в .goreleaser.yml

before:
  hooks:
  - make dep

builds:
  - binary: pgcenter
    main: ./cmd
    goarch:
      - amd64
    goos:
      - linux
    env:
      - CGO_ENABLED=0
    ldflags:
      - -a -installsuffix cgo
      - -X main.gitTag={{.Tag}} -X main.gitCommit={{.Commit}} -X main.gitBranch={{.Branch}}

archives:
  - builds: [pgcenter]

changelog:
  sort: asc

nfpms:
  - vendor: pgcenter
    homepage: https://github.com/lesovsky/pgcenter
    maintainer: Alexey Lesovsky
    description: Command-line admin tool for observing and troubleshooting Postgres.
    license: BSD-3
    formats: [ deb, rpm ]

Важным моментом является то что goreleaser не имеет интеграции с Makefile и сборку нужно описывать отдельно в формате goreleaser правил. Поэтому важно описать сборку точно так же как это осуществляется в Makefile, т.е. указать все те же флаги, переменные окружения и т.п. В секции nfpms описываем метаданные пакета и указываем необходимые форматы пакетов.

Собственно на этом все. Можно фиксировать изменения, пушить в репозиторий, перейти в Actions и наблюдать за тем как выполняются workflow. При успешном выполнении, можем создать новый тег, и влить изменения в релизную ветвь и также понаблюдать за прогрессом в Actions.

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

Ссылки

Комментарии 9

    +3

    Тоже одно время использовал makefile, но местный двухмерный синтаксис иногда выводит из себя. В качестве альтернативы предложу https://magefile.org/ (описание задач прямо кодом) и https://taskfile.dev/ (модный ныне yaml).

      0
      Согласен, периодически от него подгорает, но пока в целом Makefile удовлетворяет мои скромные потребности. За альтернативы спасибо, а из этих двух сами к чему склоняетесь больше и почему?
        +5

        Лично сам в последнее время вообще отказался от такого, потребности вполне покрывает go generate, go build, pre-commit и github actions. А так, оба предложенных варианта используют плюс-минус в равной степени.

      0
      Таким двухэтапным образом можно упаковывать приложения которые не требуют внешних зависимостей типа библиотек, сертификатов, других программ или чего-то аналогичного.
      А если приложение требует, например, обязательного конфиг-файла рядом с собой или по указанному в аргументах пути, то что тогда делать разработчику/пользователю?
        +1
        Зависит от кейса:
        — если «что-то» что лежит в коде, то просто кладем в целевой контейнер через COPY/ADD
        — если «что-то» принадлежит другому пакету (корневые сертификаты), то это также можно через multi-stage сделать — установить пакет в одном контейнере и затем оттуда скопировать в целевой.
        — если «что-то» создается самим пользователем (конфиги например) — то это создается отдельно до запуска контейнера, а при запуске контейнера подкладывается через тома (volumes).
          0
          Или как вариант генерировать это «что-то» при запуске из переменных окружения, передаваемых контейнеру.
        +1
        объясните, для чего нужен makefile, если все описанное в нем делается через github actions/docker? Без прослойки в виде make
          0
          Лично мне — на случай если нужно локально что-то выполнить, например погнать тесты или собрать бинарник.

          Судя по вашему комментарию я так понимаю что вы знаете о GH Actions больше чем я, тогда вам встречный вопрос — нет ли у вас готовых примеров (с гитхаба) по сборке докер образов и их выкладке в registry. Я понимаю что описание можно найти в их документации, но хотелось бы увидеть как это устроено в реальных workflow.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое