1. deploy/check-prod-readiness.sh — pre-deploy gating: backup<60min, disk≥5GB на /opt+/var/lib/docker, /health/ready=Healthy, .env required-keys без placeholder'ов. --ssh-host для удалённой проверки. 2. deploy/prod-deploy.sh <api-tag> <web-tag> — blue-green release: pull → green-контейнер на :8088 → migrations (auto) → smoke (/health/ready + /api/me с тест-токеном) → nginx upstream switch → swap → docker compose up -d с обновлённым тэгом. Failure → удаление green, blue остаётся. --skip-web флаг. 3. deploy/prod-rollback.sh <to-tag> — docker pull (если нужно) → docker compose up -d --force-recreate с указанным tag'ом → wait /health/ready до 120с. --dry-run + --skip-web. 4. deploy/post-deploy-smoke.sh — 10 шагов (signup → login → /api/me → list products/counterparties/stores/stock → create+delete product → logout-via-session). JSON парсится через python3 (не grep — споткнулись на пробеле перед `:` в access_token). Telegram-alert через FM_TG_TOKEN/CHAT при провале. Stage-тест: 10/10 ✓. 5. deploy/db-schema-diff.sh — pg_dump --schema-only с обоих хостов через ssh+docker exec, нормализация (sed), diff -u. Exit: 0=идентичны, 1=разница, 2=ошибка. 6. deploy/generate-release-notes.sh <from-tag> <to-tag> — git log group by prefix через awk: feat→✨, fix→🐛, perf→⚡, docs→📚, test/refactor/chore→<details>. Сохраняет docs/release-notes/<tag>.md. 7. .forgejo/workflows/auto-tag.yml — на push в main: если HEAD не помечен → создаёт v<YYYYMMDD>.<N> annotated tag, push в origin, генерирует release-notes для будущего деплоя. Все скрипты идемпотентные, поддерживают --dry-run, не трогают прод. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
4 KiB
Bash
Executable file
130 lines
4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# Sprint 21: генератор release-notes между двумя тэгами.
|
||
#
|
||
# Парсит `git log <from>..<to>`, группирует коммиты по prefix:
|
||
# feat: → ## Новые возможности
|
||
# fix: → ## Исправления
|
||
# perf: → ## Производительность
|
||
# docs: → ## Документация
|
||
# test: → ## Тесты (свёрнуто)
|
||
# chore/refactor/build: → ## Внутренние изменения (свёрнуто)
|
||
#
|
||
# Вывод — markdown, дополнительно сохраняет в:
|
||
# docs/release-notes/<to-tag>.md
|
||
# Используется при создании git-тега и в /whats-new.
|
||
#
|
||
# Usage:
|
||
# deploy/generate-release-notes.sh <from-tag> <to-tag> [--dry-run]
|
||
# deploy/generate-release-notes.sh v20260606.1 v20260607.3 > release.md
|
||
|
||
set -uo pipefail
|
||
|
||
FROM="${1:-}"
|
||
TO="${2:-HEAD}"
|
||
DRY_RUN=0
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--dry-run) DRY_RUN=1; shift ;;
|
||
--help|-h) grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;;
|
||
-*) echo "Unknown: $1" >&2; exit 2 ;;
|
||
*) shift ;;
|
||
esac
|
||
done
|
||
|
||
if [[ -z "$FROM" ]]; then
|
||
echo "Usage: $0 <from-tag> <to-tag> [--dry-run]" >&2
|
||
exit 2
|
||
fi
|
||
|
||
cd "$(dirname "$0")/.."
|
||
REPO_ROOT="$(pwd)"
|
||
|
||
# Валидация тэгов: должны существовать в git.
|
||
git rev-parse --verify "$FROM" >/dev/null 2>&1 || { echo "FAIL: тэг $FROM не найден"; exit 1; }
|
||
git rev-parse --verify "$TO" >/dev/null 2>&1 || { echo "FAIL: тэг $TO не найден"; exit 1; }
|
||
|
||
# Собираем коммиты в формате `prefix|subject|short-sha`.
|
||
# `grep -v Merge` исключает merge-коммиты.
|
||
COMMITS=$(git log "$FROM..$TO" --pretty=format:'%s|%h' --no-merges)
|
||
|
||
if [[ -z "$COMMITS" ]]; then
|
||
echo "Нет коммитов между $FROM и $TO"
|
||
exit 0
|
||
fi
|
||
|
||
# Группируем через awk. Префикс: feat/fix/perf/docs/test/chore/refactor/build/style.
|
||
RENDERED=$(echo "$COMMITS" | awk -F'|' '
|
||
function head(label) {
|
||
if (!printed[label]) {
|
||
print ""
|
||
print label
|
||
print ""
|
||
printed[label] = 1
|
||
}
|
||
}
|
||
{
|
||
subject = $1
|
||
sha = $2
|
||
type = "other"
|
||
text = subject
|
||
if (match(subject, /^(feat|fix|perf|docs|test|chore|refactor|build|style)(\([^)]+\))?:[[:space:]]*/, m)) {
|
||
type = m[1]
|
||
scope = m[2]
|
||
text = substr(subject, RLENGTH + 1)
|
||
}
|
||
line = "- " text " (`" sha "`)"
|
||
bucket[type] = bucket[type] line "\n"
|
||
}
|
||
END {
|
||
if (bucket["feat"]) { head("## ✨ Новые возможности"); printf "%s", bucket["feat"] }
|
||
if (bucket["fix"]) { head("## 🐛 Исправления"); printf "%s", bucket["fix"] }
|
||
if (bucket["perf"]) { head("## ⚡ Производительность"); printf "%s", bucket["perf"] }
|
||
if (bucket["docs"]) { head("## 📚 Документация"); printf "%s", bucket["docs"] }
|
||
if (bucket["test"]) {
|
||
print ""
|
||
print "<details><summary>🧪 Тесты</summary>"
|
||
print ""
|
||
printf "%s", bucket["test"]
|
||
print ""
|
||
print "</details>"
|
||
}
|
||
if (bucket["refactor"] || bucket["chore"] || bucket["build"] || bucket["style"]) {
|
||
print ""
|
||
print "<details><summary>🔧 Внутренние изменения</summary>"
|
||
print ""
|
||
for (k in bucket) if (k == "refactor" || k == "chore" || k == "build" || k == "style") printf "%s", bucket[k]
|
||
print ""
|
||
print "</details>"
|
||
}
|
||
if (bucket["other"]) {
|
||
print ""
|
||
print "<details><summary>📦 Прочее</summary>"
|
||
print ""
|
||
printf "%s", bucket["other"]
|
||
print ""
|
||
print "</details>"
|
||
}
|
||
}
|
||
')
|
||
|
||
DATE=$(date -u +%Y-%m-%d)
|
||
COUNT=$(echo "$COMMITS" | wc -l)
|
||
HEADER="# Release $TO
|
||
|
||
Дата: $DATE · Коммитов: $COUNT · С: $FROM"
|
||
|
||
OUTPUT="$HEADER
|
||
$RENDERED"
|
||
|
||
echo "$OUTPUT"
|
||
|
||
if [[ $DRY_RUN -eq 0 ]]; then
|
||
TARGET="$REPO_ROOT/docs/release-notes/$TO.md"
|
||
mkdir -p "$(dirname "$TARGET")"
|
||
echo "$OUTPUT" > "$TARGET"
|
||
echo "" >&2
|
||
echo "[saved] $TARGET" >&2
|
||
fi
|