# ══════════════════════════════════════════════════════════════════════════════ # CI/CD Pipeline: Сборка и публикация релиза GenAudioBookInfo # ══════════════════════════════════════════════════════════════════════════════ # # ЗАПУСК: # make release VERSION=2.1.0 # Создаёт git-тег v2.1.0, пушит его → запускается этот workflow. # # ────────────────────────────────────────────────────────────────────────────── # ЕДИНЫЙ JOB (без передачи артефактов между jobs): # # 1. Качество кода: go vet + go test # 2. Кросс-компиляция: 16 бинарников для всех платформ # 3. Архивирование: Windows → ZIP, Unix → tar.gz, SHA256 # 4. Описание релиза: авто-changelog из git-коммитов на русском языке # 5. Публикация: Gitea Release + загрузка всех файлов # # ────────────────────────────────────────────────────────────────────────────── # НЕОБХОДИМАЯ НАСТРОЙКА: # # В настройках репозитория Gitea → Settings → Secrets: # GIT_TOKEN (или GITEA_TOKEN) — токен с правами write:repository # # ══════════════════════════════════════════════════════════════════════════════ name: "Release CI/CD" on: push: tags: - "v*" env: GO_VERSION: "1.24" APP_NAME: genaudiobookinfo BUILD_DIR: build # ══════════════════════════════════════════════════════════════════════════════ jobs: release: name: "Сборка и публикация релиза" runs-on: native steps: # ── Исходный код (полная история для changelog) ────────────────────── - name: "Получение исходного кода" shell: bash env: GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | TOKEN="${GIT_TOKEN:-$GITEA_TOKEN}" SERVER="${{ github.server_url }}" REPO="${{ github.repository }}" TAG="${GITHUB_REF_NAME}" # Убираем https:// для вставки токена HOST="${SERVER#https://}" HOST="${HOST#http://}" CLONE_URL="https://${TOKEN}@${HOST}/${REPO}.git" echo ">>> Клонирование ${REPO} (тег ${TAG})..." git clone --branch "${TAG}" "${CLONE_URL}" . 2>&1 | grep -v "${TOKEN}" || true git fetch --tags --force 2>&1 | grep -v "${TOKEN}" || true echo ">>> Исходный код получен: $(git log --oneline -1)" - name: "Проверка Go" shell: bash run: | # Добавляем стандартные пути установки Go for p in /usr/local/go/bin /usr/local/bin /snap/bin "$HOME/go/bin" "$HOME/.go/bin" /opt/go/bin; do [ -d "$p" ] && export PATH="$p:$PATH" done # Сохраняем PATH для всех последующих шагов echo "PATH=$PATH" >> "$GITHUB_ENV" if ! command -v go &>/dev/null; then echo "ОШИБКА: Go не установлен на раннере." echo "Установите Go ${{ env.GO_VERSION }} на сервер раннера." echo "Проверенные пути: $PATH" exit 1 fi echo ">>> Go: $(go version)" echo ">>> GOPATH: $(go env GOPATH)" echo ">>> GOROOT: $(go env GOROOT)" # ── Определение версии ────────────────────────────────────────────── - name: "Определение версии и предыдущего тега" id: ver shell: bash run: | TAG="${GITHUB_REF_NAME}" VER="${TAG#v}" echo "version=${VER}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") echo "prev_tag=${PREV_TAG}" >> "$GITHUB_OUTPUT" echo "══════════════════════════════════════" echo " Версия: ${VER}" echo " Git-тег: ${TAG}" echo " Пред. тег: ${PREV_TAG:-<первый релиз>}" echo " Коммит: ${GITHUB_SHA:0:8}" echo "══════════════════════════════════════" # ── Качество кода ─────────────────────────────────────────────────── - name: "Проверка качества кода (vet + tests)" shell: bash run: | echo ">>> Загрузка зависимостей..." go mod download echo ">>> Статический анализ (go vet)..." go vet ./... echo ">>> Unit-тесты..." go test ./... -count=1 -timeout 5m echo ">>> Качество кода — ОК" # ── Кросс-компиляция ──────────────────────────────────────────────── - name: "Кросс-компиляция для всех платформ (16 бинарников)" shell: bash run: | VER="${{ steps.ver.outputs.version }}" LDFLAGS="-s -w -X main.version=${VER}" GOFLAGS="-trimpath" CMD="./cmd/genaudiobookinfo" OUT="${BUILD_DIR}" mkdir -p "${OUT}" build() { local goos=$1 goarch=$2 suffix=$3 extra_env="$4" local outfile="${OUT}/${APP_NAME}-${suffix}" echo ">>> ${goos}/${goarch} → ${APP_NAME}-${suffix}" env GOOS=${goos} GOARCH=${goarch} ${extra_env} \ go build ${GOFLAGS} -ldflags "${LDFLAGS}" -o "${outfile}" ${CMD} } # Linux build linux amd64 linux-amd64 build linux 386 linux-386 build linux arm64 linux-arm64 build linux arm linux-armv7 "GOARM=7" build linux mips linux-mips "GOMIPS=softfloat" build linux mipsle linux-mipsle "GOMIPS=softfloat" build linux riscv64 linux-riscv64 # macOS build darwin amd64 darwin-amd64 build darwin arm64 darwin-arm64 # Windows build windows amd64 windows-amd64.exe build windows 386 windows-386.exe build windows arm64 windows-arm64.exe # FreeBSD build freebsd amd64 freebsd-amd64 build freebsd arm64 freebsd-arm64 # OpenBSD build openbsd amd64 openbsd-amd64 # NetBSD build netbsd amd64 netbsd-amd64 echo "" echo "Собранные бинарники ($(ls ${OUT}/ | wc -l)):" ls -lh ${OUT}/ # ── Архивы + контрольные суммы ────────────────────────────────────── - name: "Создание архивов и контрольных сумм" shell: bash run: | cd "${BUILD_DIR}" mkdir -p archives echo "=== Создание архивов ===" for f in *; do [ -f "$f" ] || continue case "$f" in *.exe) ARCNAME="${f%.exe}.zip" echo " ${f} → archives/${ARCNAME}" if command -v zip &>/dev/null; then zip "archives/${ARCNAME}" "$f" else # Fallback: tar.gz вместо zip если zip не установлен ARCNAME="${f%.exe}.tar.gz" tar -czf "archives/${ARCNAME}" "$f" fi ;; ${APP_NAME}-*) ARCNAME="${f}.tar.gz" echo " ${f} → archives/${ARCNAME}" tar -czf "archives/${ARCNAME}" "$f" ;; esac done echo "" echo "=== Контрольные суммы SHA256 ===" cd archives sha256sum * > checksums-sha256.txt cat checksums-sha256.txt echo "" echo "Готово: $(ls | wc -l) файлов" # ── Генерация описания релиза на русском языке ────────────────────── - name: "Генерация описания релиза" shell: bash run: | TAG="${{ steps.ver.outputs.tag }}" VER="${{ steps.ver.outputs.version }}" PREV_TAG="${{ steps.ver.outputs.prev_tag }}" COMMIT="${{ github.sha }}" DATE=$(date -u '+%d.%m.%Y %H:%M UTC') # ── Changelog из git-коммитов ────────────────────────────────── if [ -n "${PREV_TAG}" ]; then RAW_LOG=$(git log --pretty=format:"%s" "${PREV_TAG}..${TAG}" --no-merges 2>/dev/null || echo "") else RAW_LOG=$(git log --pretty=format:"%s" --no-merges 2>/dev/null | head -50 || echo "") fi # Категоризация коммитов FEATURES="" FIXES="" REFACTOR="" OTHER="" while IFS= read -r line; do [ -z "$line" ] && continue line_lower=$(echo "$line" | tr '[:upper:]' '[:lower:]') case "$line_lower" in функция:*|feat:*|feature:*|добавлен*|реализован*|новое:*) FEATURES="${FEATURES} - ${line}" ;; исправлен*|fix:*|bugfix:*|баг:*|ошибка:*) FIXES="${FIXES} - ${line}" ;; рефакторинг:*|refactor:*|оптимизац*|улучшен*) REFACTOR="${REFACTOR} - ${line}" ;; ci:*|docs:*|ci/*|build:*) OTHER="${OTHER} - ${line}" ;; *) OTHER="${OTHER} - ${line}" ;; esac done <<< "$RAW_LOG" # ── Начинаем формировать тело релиза ─────────────────────────── { echo "## GenAudioBookInfo ${TAG}" echo "" # Пользовательские заметки (RELEASE_NOTES.md — приоритет) if [ -f "RELEASE_NOTES.md" ]; then cat RELEASE_NOTES.md echo "" fi # Автоматический changelog HAS_CHANGES=false if [ -n "${FEATURES}" ]; then HAS_CHANGES=true echo "### 🚀 Новые возможности" echo "${FEATURES}" echo "" fi if [ -n "${FIXES}" ]; then HAS_CHANGES=true echo "### 🐛 Исправления" echo "${FIXES}" echo "" fi if [ -n "${REFACTOR}" ]; then HAS_CHANGES=true echo "### ♻️ Рефакторинг и оптимизация" echo "${REFACTOR}" echo "" fi if [ -n "${OTHER}" ]; then HAS_CHANGES=true echo "### 📝 Прочие изменения" echo "${OTHER}" echo "" fi if [ "${HAS_CHANGES}" = "false" ]; then echo "- Первый релиз" echo "" fi echo "---" echo "" echo "### Поддерживаемые платформы" echo "" echo "| ОС | Архитектура | Файл |" echo "|---|---|---|" echo "| Windows | amd64 (64-бит) | \`${APP_NAME}-windows-amd64.zip\` |" echo "| Windows | 386 (32-бит) | \`${APP_NAME}-windows-386.zip\` |" echo "| Windows | arm64 | \`${APP_NAME}-windows-arm64.zip\` |" echo "| Linux | amd64 | \`${APP_NAME}-linux-amd64.tar.gz\` |" echo "| Linux | arm64 | \`${APP_NAME}-linux-arm64.tar.gz\` |" echo "| Linux | armv7 | \`${APP_NAME}-linux-armv7.tar.gz\` |" echo "| Linux | 386 | \`${APP_NAME}-linux-386.tar.gz\` |" echo "| Linux | MIPS | \`${APP_NAME}-linux-mips.tar.gz\` |" echo "| Linux | MIPSle | \`${APP_NAME}-linux-mipsle.tar.gz\` |" echo "| Linux | RISC-V 64 | \`${APP_NAME}-linux-riscv64.tar.gz\` |" echo "| macOS | amd64 (Intel) | \`${APP_NAME}-darwin-amd64.tar.gz\` |" echo "| macOS | arm64 (Apple Silicon) | \`${APP_NAME}-darwin-arm64.tar.gz\` |" echo "| FreeBSD | amd64 | \`${APP_NAME}-freebsd-amd64.tar.gz\` |" echo "| FreeBSD | arm64 | \`${APP_NAME}-freebsd-arm64.tar.gz\` |" echo "| OpenBSD | amd64 | \`${APP_NAME}-openbsd-amd64.tar.gz\` |" echo "| NetBSD | amd64 | \`${APP_NAME}-netbsd-amd64.tar.gz\` |" echo "" echo "> 🔒 Контрольные суммы: \`checksums-sha256.txt\`" echo "" echo "### Информация о сборке" echo "" echo "| Параметр | Значение |" echo "|---|---|" echo "| Коммит | \`${COMMIT}\` |" echo "| Дата сборки | ${DATE} |" echo "| Go | ${{ env.GO_VERSION }} |" } > /tmp/release_body.md echo ">>> Описание релиза сформировано:" echo "──────────────────────────────────────" cat /tmp/release_body.md echo "──────────────────────────────────────" # ── Публикация в DFGit ────────────────────────────────────────────── - name: "Создание релиза в DFGit и загрузка файлов" shell: bash env: GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | TOKEN="${GIT_TOKEN:-$GITEA_TOKEN}" if [ -z "${TOKEN}" ]; then echo "ОШИБКА: не задан секрет GIT_TOKEN (или GITEA_TOKEN)." echo "Добавьте токен в Settings → Secrets репозитория." exit 1 fi TAG="${{ steps.ver.outputs.tag }}" COMMIT="${{ github.sha }}" GITEA_URL="${{ github.server_url }}" REPO="${{ github.repository }}" echo "══════════════════════════════════════" echo " Сервер: ${GITEA_URL}" echo " Репо: ${REPO}" echo " Тег: ${TAG}" echo " Коммит: ${COMMIT:0:8}" echo "══════════════════════════════════════" # ── JSON-кодирование тела релиза ─────────────────────────────── # JSON-кодирование: python3 или чистый bash if command -v python3 &>/dev/null; then BODY_JSON=$(python3 -c " import json with open('/tmp/release_body.md', 'r') as f: print(json.dumps(f.read())) ") else # Чистый bash: экранируем для JSON BODY_RAW=$(cat /tmp/release_body.md) BODY_JSON=$(printf '%s' "$BODY_RAW" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') BODY_JSON="\"${BODY_JSON}\"" fi # ── Создание релиза через Gitea API ──────────────────────────── echo "" echo ">>> Создаём релиз ${TAG}..." RELEASE_JSON=$(curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases" \ -d "{ \"tag_name\": \"${TAG}\", \"target_commitish\": \"${COMMIT}\", \"name\": \"${APP_NAME} ${TAG}\", \"body\": ${BODY_JSON}, \"draft\": false, \"prerelease\": false }") # Извлекаем ID релиза: python3 или grep/sed if command -v python3 &>/dev/null; then RELEASE_ID=$(echo "$RELEASE_JSON" | \ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) else RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') fi if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "None" ]; then echo "ОШИБКА: не удалось создать релиз!" echo "Ответ API:" echo "$RELEASE_JSON" exit 1 fi echo ">>> Релиз создан: ID=${RELEASE_ID}" # ── Загрузка артефактов ──────────────────────────────────────── echo "" echo ">>> Загрузка артефактов..." UPLOAD_OK=0 UPLOAD_FAIL=0 for FILE in ./${BUILD_DIR}/archives/*; 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 ${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 "✓ (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 echo ">>> Релиз ${TAG} опубликован успешно."