name: "Релиз: архив исходников" on: push: tags: - "v*" workflow_dispatch: inputs: tag: description: "Тег релиза (например: v2.2.0)" required: true permissions: contents: write env: DIST_DIR: dist jobs: release: name: "Упаковка и публикация релиза" runs-on: native steps: - name: "Получение исходного кода" shell: bash env: GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail TOKEN="${GIT_TOKEN:-$GITEA_TOKEN}" SERVER="${{ github.server_url }}" REPO="${{ github.repository }}" HOST="${SERVER#https://}" HOST="${HOST#http://}" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then TAG="${{ github.event.inputs.tag }}" else TAG="${GITHUB_REF_NAME}" fi if [ -z "${TAG}" ]; then echo "ОШИБКА: не удалось определить тег релиза." exit 1 fi echo ">>> Клонирование ${REPO} (тег: ${TAG})" if [ -n "${TOKEN}" ]; then if [ "${HOST}" = "github.com" ]; then CLONE_URL="https://x-access-token:${TOKEN}@${HOST}/${REPO}.git" else CLONE_URL="https://${TOKEN}@${HOST}/${REPO}.git" fi else CLONE_URL="${SERVER}/${REPO}.git" fi git clone --branch "${TAG}" --depth 1 "${CLONE_URL}" . 2>&1 | grep -F -v "${TOKEN:-}" || true git fetch --tags --force 2>/dev/null || true echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV" echo ">>> Исходный код получен: $(git log --oneline -1)" - name: "Подготовка метаданных релиза" id: meta shell: bash run: | set -euo pipefail TAG="${RELEASE_TAG}" VERSION="${TAG#v}" PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true) echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "prev_tag=${PREV_TAG}" >> "$GITHUB_OUTPUT" echo "══════════════════════════════════════" echo " Тег: ${TAG}" echo " Версия: ${VERSION}" echo " Пред. тег: ${PREV_TAG:-<первый релиз>}" echo " Коммит: ${GITHUB_SHA:0:8}" echo "══════════════════════════════════════" - name: "Создание архивов исходного кода" shell: bash run: | set -euo pipefail TAG="${{ steps.meta.outputs.tag }}" REPO_NAME="${GITHUB_REPOSITORY##*/}" ARCHIVE_BASE="${REPO_NAME}-${TAG}" rm -rf .release-work "${DIST_DIR}" mkdir -p .release-work "${DIST_DIR}" if command -v rsync >/dev/null 2>&1; then rsync -a \ --exclude '.git/' \ --exclude '.src-repo/' \ --exclude '.wiki-repo/' \ --exclude '.release-work/' \ --exclude "${DIST_DIR}/" \ ./ ".release-work/${ARCHIVE_BASE}/" else cp -a . ".release-work/${ARCHIVE_BASE}" rm -rf \ ".release-work/${ARCHIVE_BASE}/.git" \ ".release-work/${ARCHIVE_BASE}/.src-repo" \ ".release-work/${ARCHIVE_BASE}/.wiki-repo" \ ".release-work/${ARCHIVE_BASE}/.release-work" \ ".release-work/${ARCHIVE_BASE}/${DIST_DIR}" fi tar -czf "${DIST_DIR}/${ARCHIVE_BASE}.tar.gz" -C .release-work "${ARCHIVE_BASE}" if command -v zip >/dev/null 2>&1; then ( cd .release-work zip -qr "../${DIST_DIR}/${ARCHIVE_BASE}.zip" "${ARCHIVE_BASE}" ) else ARCHIVE_BASE="${ARCHIVE_BASE}" DIST_DIR="${DIST_DIR}" python3 - <<'PY' import os import zipfile base = os.environ["ARCHIVE_BASE"] src = os.path.join(".release-work", base) dst = os.path.join(os.environ["DIST_DIR"], f"{base}.zip") with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zf: for root, _, files in os.walk(src): for file_name in files: full = os.path.join(root, file_name) arc = os.path.relpath(full, ".release-work") zf.write(full, arc) PY fi sha256sum "${DIST_DIR}"/* > "${DIST_DIR}/checksums-sha256.txt" echo "ARCHIVE_BASE=${ARCHIVE_BASE}" >> "$GITHUB_ENV" echo ">>> Артефакты релиза:" ls -lh "${DIST_DIR}" - name: "Генерация описания релиза" shell: bash run: | set -euo pipefail TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" PREV_TAG="${{ steps.meta.outputs.prev_tag }}" DATE_UTC=$(date -u '+%Y-%m-%d %H:%M UTC') CHANGELOG_FILE="" if [ -f "docs/ru/CHANGELOG.md" ]; then CHANGELOG_FILE="docs/ru/CHANGELOG.md" elif [ -f "docs/en/CHANGELOG_EN.md" ]; then CHANGELOG_FILE="docs/en/CHANGELOG_EN.md" fi CHANGELOG_SECTION="" if [ -n "${CHANGELOG_FILE}" ]; then CHANGELOG_SECTION=$(awk -v ver="${VERSION}" ' $0 ~ "^## \\[" ver "\\]" { in_section=1; next } /^## \[/ && in_section { exit } in_section { print } ' "${CHANGELOG_FILE}") fi if [ -n "${PREV_TAG}" ]; then COMMITS=$(git log --pretty=format:'- %s (%h)' "${PREV_TAG}..${TAG}" --no-merges | head -50) else COMMITS=$(git log --pretty=format:'- %s (%h)' --no-merges | head -50) fi { echo "## ${TAG}" echo "" echo "Релиз скриптового проекта без компиляции бинарников." echo "" echo "### Что входит в релиз" echo "- Исходный код в форматах: ${ARCHIVE_BASE}.tar.gz, ${ARCHIVE_BASE}.zip" echo "- Контрольные суммы: checksums-sha256.txt" echo "" if [ -n "${CHANGELOG_SECTION}" ]; then echo "### Что сделано (из CHANGELOG)" echo "${CHANGELOG_SECTION}" echo "" fi echo "### Коммиты релиза" if [ -n "${COMMITS}" ]; then echo "${COMMITS}" else echo "- Изменения не найдены" fi echo "" echo "---" echo "" echo "- Тег: ${TAG}" echo "- Коммит: ${GITHUB_SHA}" echo "- Дата (UTC): ${DATE_UTC}" } > /tmp/release_body.md echo ">>> Сформировано описание релиза: /tmp/release_body.md" - name: "Создание релиза и загрузка архивов" shell: bash env: GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail TOKEN="${GIT_TOKEN:-$GITEA_TOKEN}" if [ -z "${TOKEN}" ]; then echo "ОШИБКА: не задан секрет GIT_TOKEN (или GITEA_TOKEN)." exit 1 fi SERVER="${{ github.server_url }}" REPO="${{ github.repository }}" HOST="${SERVER#https://}" HOST="${HOST#http://}" TAG="${{ steps.meta.outputs.tag }}" BODY_JSON=$(python3 - <<'PY' import json with open('/tmp/release_body.md', 'r', encoding='utf-8') as f: print(json.dumps(f.read(), ensure_ascii=False)) PY ) if [ "${HOST}" = "github.com" ]; then CREATE_URL="https://api.github.com/repos/${REPO}/releases" AUTH_HEADER="Authorization: Bearer ${TOKEN}" EXTRA_HEADERS=(-H "Accept: application/vnd.github+json") else CREATE_URL="${SERVER}/api/v1/repos/${REPO}/releases" AUTH_HEADER="Authorization: token ${TOKEN}" EXTRA_HEADERS=() fi RELEASE_JSON=$(curl -sf -X POST \ -H "${AUTH_HEADER}" \ -H "Content-Type: application/json" \ "${EXTRA_HEADERS[@]}" \ "${CREATE_URL}" \ -d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${GITHUB_SHA}\",\"name\":\"${TAG}\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":false}") RELEASE_ID=$(printf '%s' "${RELEASE_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") if [ -z "${RELEASE_ID}" ]; then echo "ОШИБКА: не удалось получить ID релиза" echo "${RELEASE_JSON}" exit 1 fi if [ "${HOST}" = "github.com" ]; then UPLOAD_BASE=$(printf '%s' "${RELEASE_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('upload_url','').split('{')[0])") RELEASE_URL="https://github.com/${REPO}/releases/tag/${TAG}" else UPLOAD_BASE="${SERVER}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" RELEASE_URL="${SERVER}/${REPO}/releases/tag/${TAG}" fi echo ">>> Загрузка файлов в релиз ${TAG}" for file in "${DIST_DIR}"/*; do [ -f "${file}" ] || continue name=$(basename "${file}") if [ "${HOST}" = "github.com" ]; then curl -sf -X POST \ -H "${AUTH_HEADER}" \ -H "Content-Type: application/octet-stream" \ "${EXTRA_HEADERS[@]}" \ --data-binary @"${file}" \ "${UPLOAD_BASE}?name=${name}" >/dev/null else curl -sf -X POST \ -H "${AUTH_HEADER}" \ -H "Content-Type: application/octet-stream" \ --data-binary @"${file}" \ "${UPLOAD_BASE}?name=${name}" >/dev/null fi echo " ✓ ${name}" done echo "" echo ">>> Релиз опубликован: ${RELEASE_URL}"