#!/usr/bin/env bash # # Sprint 21: генератор release-notes между двумя тэгами. # # Парсит `git log ..`, группирует коммиты по prefix: # feat: → ## Новые возможности # fix: → ## Исправления # perf: → ## Производительность # docs: → ## Документация # test: → ## Тесты (свёрнуто) # chore/refactor/build: → ## Внутренние изменения (свёрнуто) # # Вывод — markdown, дополнительно сохраняет в: # docs/release-notes/.md # Используется при создании git-тега и в /whats-new. # # Usage: # deploy/generate-release-notes.sh [--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 [--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 "
🧪 Тесты" print "" printf "%s", bucket["test"] print "" print "
" } if (bucket["refactor"] || bucket["chore"] || bucket["build"] || bucket["style"]) { print "" print "
🔧 Внутренние изменения" print "" for (k in bucket) if (k == "refactor" || k == "chore" || k == "build" || k == "style") printf "%s", bucket[k] print "" print "
" } if (bucket["other"]) { print "" print "
📦 Прочее" print "" printf "%s", bucket["other"] print "" print "
" } } ') 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