# ══════════════════════════════════════════════════════════════════════════════ # CI/CD Pipeline: Сборка и публикация релиза GenAudioBookInfo # ══════════════════════════════════════════════════════════════════════════════ # # ЗАПУСК ТОЛЬКО ЧЕРЕЗ make — два способа: # # 1. make release-tag VERSION=2.1.0 # Создаёт аннотированный git-тег v2.1.0 и пушит его в origin. # Workflow запускается автоматически по событию push.tags. # # 2. make ci-release VERSION=2.1.0 GITEA_TOKEN=<токен> # Вызывает Gitea API (workflow_dispatch) — запускает вручную # без создания тега. Удобно для тестирования пайплайна. # # ────────────────────────────────────────────────────────────────────────────── # ЭТАПЫ ПАЙПЛАЙНА: # # [Этап 1] quality — Статический анализ (go vet) + Unit-тесты # Блокирует следующие этапы при ошибке. # # [Этап 2] build — Определение версии, кросс-компиляция 15 бинарников # для Linux / macOS / Windows / FreeBSD / OpenBSD / # NetBSD / MIPS / RISC-V, упаковка в архивы, # генерация SHA256 контрольных сумм. # # [Этап 3] publish — Создание Gitea Release через API, # загрузка всех архивов и checksums-sha256.txt. # # ────────────────────────────────────────────────────────────────────────────── # НЕОБХОДИМАЯ НАСТРОЙКА: # # В настройках репозитория Gitea → Settings → Secrets добавить: # GITEA_TOKEN — токен с правами write:repository и write:release # (обычно автоматически инжектируется runner'ом) # # ══════════════════════════════════════════════════════════════════════════════ name: "Release CI/CD" on: # ── Способ 1: запуск через make ci-release ──────────────────────────────── workflow_dispatch: inputs: version: description: "Версия релиза (например: 2.1.0). Без префикса 'v'." required: false default: "" prerelease: description: "Отметить как пре-релиз (alpha/beta)?" required: false default: "false" # ── Способ 2: запуск через make release-tag ─────────────────────────────── push: tags: - "v[0-9]+.[0-9]+.[0-9]*" # ────────────────────────────────────────────────────────────────────────────── # Глобальные переменные окружения (доступны во всех jobs) # ────────────────────────────────────────────────────────────────────────────── env: GO_VERSION: "1.24" # Версия Go для сборки APP_NAME: genaudiobookinfo BUILD_DIR: build # ══════════════════════════════════════════════════════════════════════════════ jobs: # ──────────────────────────────────────────────────────────────────────────── # ЭТАП 1: Проверка качества кода # ──────────────────────────────────────────────────────────────────────────── quality: name: "Этап 1 — Качество кода" runs-on: ubuntu-latest steps: - name: "Получение исходного кода" uses: actions/checkout@v4 - name: "Установка Go ${{ env.GO_VERSION }}" uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: "Загрузка зависимостей (go mod download)" run: make deps - name: "Статический анализ (go vet)" # Проверяет корректность кода без запуска: подозрительные # конструкции, неправильные форматные строки, ошибки типов. run: make vet - name: "Unit-тесты (go test)" # -count=1 — отключает кеш результатов (всегда свежий прогон) # -timeout 5m — предел времени выполнения тестов run: go test ./... -count=1 -timeout 5m # ──────────────────────────────────────────────────────────────────────────── # ЭТАП 2: Кросс-компиляция и подготовка артефактов # ──────────────────────────────────────────────────────────────────────────── build: name: "Этап 2 — Кросс-компиляция" runs-on: ubuntu-latest needs: quality # Запускается только после успешного прохождения Этапа 1 outputs: # Версия и тег передаются в Этап 3 через outputs version: ${{ steps.ver.outputs.version }} tag: ${{ steps.ver.outputs.tag }} steps: - name: "Получение исходного кода (полная история для тегов)" uses: actions/checkout@v4 with: fetch-depth: 0 # Нужна полная история для git describe - name: "Установка Go ${{ env.GO_VERSION }}" uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true # ── Определение версии ──────────────────────────────────────────────── # Приоритет: git-тег > ручной ввод (inputs.version) > значение в Makefile - name: "Определение версии релиза" id: ver run: | if [ "${{ github.event_name }}" = "push" ]; then # Запуск через make release-tag: версия берётся из имени тега TAG="${{ github.ref_name }}" # например: v2.0.0 VER="${TAG#v}" # например: 2.0.0 elif [ -n "${{ inputs.version }}" ]; then # Запуск через make ci-release с явной версией VER="${{ inputs.version }}" TAG="v${VER}" else # Версия не задана — берём из Makefile (строка VERSION := X.Y.Z) VER=$(grep '^VERSION' Makefile | head -1 | sed 's/.*:=\s*//' | tr -d ' ') TAG="v${VER}" fi echo "version=${VER}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "──────────────────────────────────" echo " Версия: ${VER}" echo " Git-тег: ${TAG}" echo " Коммит: ${{ github.sha }}" echo "──────────────────────────────────" - name: "Загрузка зависимостей" run: make deps # ── Кросс-компиляция ───────────────────────────────────────────────── # make build-all собирает 15 бинарников через GOOS/GOARCH # VERSION перезаписывает значение из Makefile (инжектируется в ldflags) - name: "Кросс-компиляция для всех платформ" run: | make build-all VERSION=${{ steps.ver.outputs.version }} echo "" echo "Собранные бинарники:" ls -lh ${{ env.BUILD_DIR }}/ # ── Упаковка в архивы ───────────────────────────────────────────────── # Windows → ZIP (стандарт для Windows-пользователей) # Всё остальное → tar.gz (стандарт для Unix-систем) - name: "Создание архивов (zip / tar.gz)" run: | cd ${{ env.BUILD_DIR }} mkdir -p archives echo "=== Windows → ZIP ===" for f in *-windows-*.exe; do [ -f "$f" ] || continue ARCNAME="${f%.exe}.zip" echo " ${f} → archives/${ARCNAME}" zip "archives/${ARCNAME}" "$f" done echo "=== Unix → TAR.GZ ===" for f in \ *-linux-* \ *-darwin-* \ *-freebsd-* \ *-openbsd-* \ *-netbsd-*; do [[ "$f" == *.exe ]] && continue [ -f "$f" ] || continue ARCNAME="${f}.tar.gz" echo " ${f} → archives/${ARCNAME}" tar -czf "archives/${ARCNAME}" "$f" done echo "" echo "Готовые архивы:" ls -lh archives/ # ── Контрольные суммы ───────────────────────────────────────────────── # SHA256 для каждого архива — пользователи могут проверить целостность - name: "Генерация контрольных сумм SHA256" run: | cd ${{ env.BUILD_DIR }}/archives sha256sum * | tee checksums-sha256.txt echo "" echo "checksums-sha256.txt создан ($(wc -l < checksums-sha256.txt) файлов)" # ── Передача артефактов в Этап 3 ───────────────────────────────────── - name: "Сохранение артефактов" uses: actions/upload-artifact@v4 with: name: release-assets-${{ steps.ver.outputs.tag }} path: ${{ env.BUILD_DIR }}/archives/ retention-days: 5 if-no-files-found: error # Ошибка если архивы не созданы # ──────────────────────────────────────────────────────────────────────────── # ЭТАП 3: Публикация релиза в Gitea # ──────────────────────────────────────────────────────────────────────────── publish: name: "Этап 3 — Публикация релиза" runs-on: ubuntu-latest needs: build # Запускается только после успешного Этапа 2 env: VERSION: ${{ needs.build.outputs.version }} TAG: ${{ needs.build.outputs.tag }} PRERELEASE: ${{ inputs.prerelease || 'false' }} steps: - name: "Скачивание артефактов из Этапа 2" uses: actions/download-artifact@v4 with: name: release-assets-${{ needs.build.outputs.tag }} path: ./artifacts - name: "Список артефактов для публикации" run: | echo "Артефакты ($(ls ./artifacts | wc -l) файлов):" ls -lh ./artifacts/ # ── Создание Gitea Release и загрузка файлов ───────────────────────── # Используется Gitea REST API v1: # POST /api/v1/repos/{owner}/{repo}/releases — создать релиз # POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets — загрузить файл - name: "Создание релиза в Gitea и загрузка файлов" env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | GITEA_URL="${{ github.server_url }}" REPO="${{ github.repository }}" COMMIT="${{ github.sha }}" DATE=$(date -u '+%Y-%m-%d %H:%M UTC') echo "──────────────────────────────────────────────" echo " Сервер: ${GITEA_URL}" echo " Репо: ${REPO}" echo " Версия: ${TAG}" echo " Коммит: ${COMMIT}" echo "──────────────────────────────────────────────" # ── Тело описания релиза (Markdown) ──────────────────────────── RELEASE_BODY="## GenAudioBookInfo ${TAG} ### Платформы | ОС | Архитектура | Файл | |---|---|---| | Windows | amd64 (64-бит) | \`genaudiobookinfo-windows-amd64.zip\` | | Windows | 386 (32-бит) | \`genaudiobookinfo-windows-386.zip\` | | Windows | arm64 | \`genaudiobookinfo-windows-arm64.zip\` | | Linux | amd64 | \`genaudiobookinfo-linux-amd64.tar.gz\` | | Linux | arm64 | \`genaudiobookinfo-linux-arm64.tar.gz\` | | Linux | armv7 | \`genaudiobookinfo-linux-armv7.tar.gz\` | | Linux | 386 | \`genaudiobookinfo-linux-386.tar.gz\` | | Linux | MIPS (big-endian) | \`genaudiobookinfo-linux-mips.tar.gz\` | | Linux | MIPS (little-endian) | \`genaudiobookinfo-linux-mipsle.tar.gz\` | | Linux | RISC-V 64 | \`genaudiobookinfo-linux-riscv64.tar.gz\` | | macOS | amd64 (Intel) | \`genaudiobookinfo-darwin-amd64.tar.gz\` | | macOS | arm64 (Apple Silicon) | \`genaudiobookinfo-darwin-arm64.tar.gz\` | | FreeBSD | amd64 | \`genaudiobookinfo-freebsd-amd64.tar.gz\` | | FreeBSD | arm64 | \`genaudiobookinfo-freebsd-arm64.tar.gz\` | | OpenBSD | amd64 | \`genaudiobookinfo-openbsd-amd64.tar.gz\` | | NetBSD | amd64 | \`genaudiobookinfo-netbsd-amd64.tar.gz\` | > Контрольные суммы файлов: \`checksums-sha256.txt\` ### Сборка - **Коммит:** \`${COMMIT}\` - **Дата:** ${DATE} - **Go:** ${{ env.GO_VERSION }}" # ── Создать релиз через API ───────────────────────────────────── echo "" echo "Создаём релиз ${TAG}..." RELEASE_JSON=$(curl -sf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases" \ -d "{ \"tag_name\": \"${TAG}\", \"target_commitish\": \"${COMMIT}\", \"name\": \"${{ env.APP_NAME }} ${TAG}\", \"body\": $(echo "$RELEASE_BODY" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'), \"draft\": false, \"prerelease\": ${PRERELEASE} }") RELEASE_ID=$(echo "$RELEASE_JSON" | \ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "None" ]; then echo "ОШИБКА: не удалось создать релиз!" echo "Ответ API:" echo "$RELEASE_JSON" | python3 -m json.tool 2>/dev/null || echo "$RELEASE_JSON" exit 1 fi echo "Релиз создан: ID=${RELEASE_ID}" # ── Загрузить каждый артефакт ──────────────────────────────────── echo "" echo "Загружаем артефакты..." UPLOAD_OK=0 UPLOAD_FAIL=0 for FILE in ./artifacts/*; do [ -f "$FILE" ] || continue FILENAME=$(basename "$FILE") FILESIZE=$(du -sh "$FILE" | cut -f1) printf " %-55s [%5s] " "${FILENAME}" "${FILESIZE}" HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/octet-stream" \ --data-binary @"${FILE}" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}") if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then echo "OK (HTTP ${HTTP_CODE})" UPLOAD_OK=$((UPLOAD_OK + 1)) else echo "ОШИБКА (HTTP ${HTTP_CODE})" UPLOAD_FAIL=$((UPLOAD_FAIL + 1)) fi done # ── Итог ───────────────────────────────────────────────────────── echo "" echo "══════════════════════════════════════════════" echo " Загружено успешно: ${UPLOAD_OK}" echo " Ошибок: ${UPLOAD_FAIL}" echo " Релиз: ${GITEA_URL}/${REPO}/releases/tag/${TAG}" echo "══════════════════════════════════════════════" if [ "$UPLOAD_FAIL" -gt 0 ]; then echo "Часть файлов не удалось загрузить!" exit 1 fi