361 lines
14 KiB
YAML
361 lines
14 KiB
YAML
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
|
||
|
||
# Check if tag exists on the remote
|
||
TAG_EXISTS=false
|
||
if git ls-remote --exit-code --tags "${CLONE_URL}" "refs/tags/${TAG}" >/dev/null 2>&1; then
|
||
TAG_EXISTS=true
|
||
fi
|
||
|
||
if [ "${TAG_EXISTS}" = "true" ]; then
|
||
echo ">>> Тег ${TAG} найден, клонирование..."
|
||
git clone --branch "${TAG}" --depth 50 "${CLONE_URL}" . 2>&1 | grep -F -v "${TOKEN:-}" || true
|
||
else
|
||
echo ">>> Тег ${TAG} не найден, клонирование ветки master..."
|
||
git clone --branch master --depth 50 "${CLONE_URL}" . 2>&1 | grep -F -v "${TOKEN:-}" || true
|
||
fi
|
||
|
||
git fetch --tags --force 2>/dev/null || true
|
||
|
||
# If tag doesn't exist locally, create and push it
|
||
if [ "${TAG_EXISTS}" = "false" ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
echo ">>> Создание тега ${TAG} на текущем коммите ($(git rev-parse --short HEAD))..."
|
||
git tag "${TAG}"
|
||
git push origin "${TAG}" 2>&1 | grep -F -v "${TOKEN:-}" || true
|
||
echo ">>> Тег ${TAG} создан и отправлен"
|
||
fi
|
||
|
||
echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV"
|
||
echo ">>> Исходный код получен: $(git log --oneline -1 2>/dev/null || echo 'N/A')"
|
||
|
||
- 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
|
||
|
||
COMMITS=""
|
||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
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
|
||
else
|
||
echo ">>> Предупреждение: .git недоступен, список коммитов будет пропущен"
|
||
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 "- Недоступно (runner без .git)"
|
||
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 }}"
|
||
|
||
# Set up API URLs and auth header early (needed for tag check below)
|
||
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
|
||
|
||
TARGET_COMMITISH="${RELEASE_REF:-master}"
|
||
if [ -n "${GITHUB_SHA:-}" ]; then
|
||
TARGET_COMMITISH="${GITHUB_SHA}"
|
||
fi
|
||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
TARGET_COMMITISH="$(git rev-list -n 1 "${TAG}" 2>/dev/null || echo "${TARGET_COMMITISH}")"
|
||
fi
|
||
|
||
# Ensure the tag exists on the remote (Gitea requires it before creating a release)
|
||
if [ "${HOST}" != "github.com" ]; then
|
||
TAG_HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
|
||
-H "${AUTH_HEADER}" \
|
||
"${SERVER}/api/v1/repos/${REPO}/tags/${TAG}")
|
||
if [ "${TAG_HTTP_CODE}" != "200" ]; then
|
||
echo ">>> Тег ${TAG} не найден на сервере (HTTP ${TAG_HTTP_CODE}), создание через API..."
|
||
TAG_CREATE_RESP=$(curl -sS -X POST \
|
||
-H "${AUTH_HEADER}" \
|
||
-H "Content-Type: application/json" \
|
||
"${SERVER}/api/v1/repos/${REPO}/tags" \
|
||
-d "{\"tag_name\":\"${TAG}\",\"target\":\"${TARGET_COMMITISH}\"}")
|
||
echo ">>> Ответ создания тега: ${TAG_CREATE_RESP}"
|
||
else
|
||
echo ">>> Тег ${TAG} существует на сервере"
|
||
fi
|
||
fi
|
||
|
||
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
|
||
)
|
||
|
||
RELEASE_PAYLOAD="{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${TARGET_COMMITISH}\",\"name\":\"${TAG}\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":false}"
|
||
RELEASE_JSON=$(curl -sS -X POST \
|
||
-H "${AUTH_HEADER}" \
|
||
-H "Content-Type: application/json" \
|
||
"${EXTRA_HEADERS[@]}" \
|
||
"${CREATE_URL}" \
|
||
-d "${RELEASE_PAYLOAD}")
|
||
|
||
if ! printf '%s' "${RELEASE_JSON}" | python3 -c "import sys, json; j=json.load(sys.stdin); print(j.get('id',''));" >/tmp/release_id.txt 2>/dev/null; then
|
||
echo "ОШИБКА: API вернул не-JSON при создании релиза"
|
||
echo "Ответ: ${RELEASE_JSON}"
|
||
exit 1
|
||
fi
|
||
|
||
RELEASE_ID=$(cat /tmp/release_id.txt)
|
||
rm -f /tmp/release_id.txt
|
||
|
||
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}"
|