generate-changelog/generate-changelog.sh
Felipe M. 048bc380fa
feat: add generate-changelog Forgejo action
Composite action that generates a changelog from git history between
two tags, grouped by conventional commit types. Supports customizable
categories via JSON input and auto-detects the previous tag.
2026-04-07 10:12:40 +02:00

157 lines
4.7 KiB
Shell
Executable file

#!/bin/bash
# Generate changelog from git history for CI releases
# Groups commits by conventional commit types
#
# Expected environment variables:
# INPUT_TAG - Git tag to generate changelog for (required)
# INPUT_PREVIOUS_TAG - Previous tag override (optional, auto-detected)
# INPUT_CATEGORIES - JSON mapping of prefixes to headers (optional)
# GITHUB_OUTPUT - Output file for action outputs (set by runner)
# GITEA_SERVER_URL - Forgejo server URL (set by runner)
# GITEA_REPOSITORY - Repository in owner/repo format (set by runner)
set -euo pipefail
TAG="${INPUT_TAG:?Error: tag input is required}"
PREV_TAG="${INPUT_PREVIOUS_TAG:-}"
CATEGORIES="${INPUT_CATEGORIES:-}"
# Default categories if not provided or empty
if [ -z "$CATEGORIES" ]; then
CATEGORIES='{"feat": "✨ Features", "fix": "🐛 Bug Fixes", "refactor": "♻️ Refactoring", "chore": "🔧 Chores"}'
fi
echo "Generating changelog for ${TAG}..."
# Determine previous tag
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
fi
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found. This is the first release."
COMMIT_RANGE="${TAG}"
else
echo "Generating changelog from ${PREV_TAG} to ${TAG}"
COMMIT_RANGE="${PREV_TAG}..${TAG}"
fi
# Parse category prefixes and headers from JSON using jq if available, else fallback
parse_categories() {
if command -v jq &>/dev/null; then
jq -r 'to_entries[] | "\(.key)|\(.value)"' <<< "$CATEGORIES"
else
# Pure bash fallback: parse simple flat JSON {"key": "value", ...}
local json="${CATEGORIES//\{/}"
json="${json//\}/}"
local IFS=','
for pair in $json; do
local key value
key=$(echo "$pair" | sed 's/.*"\([^"]*\)"\s*:.*/\1/')
value=$(echo "$pair" | sed 's/.*:\s*"\([^"]*\)".*/\1/')
echo "${key}|${value}"
done
fi
}
# Read categories into arrays
CATEGORY_PREFIXES=()
CATEGORY_HEADERS=()
while IFS='|' read -r prefix header; do
[ -z "$prefix" ] && continue
CATEGORY_PREFIXES+=("$prefix")
CATEGORY_HEADERS+=("$header")
done < <(parse_categories)
NUM_CATEGORIES=${#CATEGORY_PREFIXES[@]}
# Initialize arrays to hold commits per category (+ 1 for "other")
declare -a CATEGORY_COMMITS
for ((i = 0; i <= NUM_CATEGORIES; i++)); do
CATEGORY_COMMITS[$i]=""
done
# Get all commits in range
COMMITS=$(git log "${COMMIT_RANGE}" --pretty=format:"%H|%s" 2>/dev/null || echo "")
if [ -n "$COMMITS" ]; then
while IFS='|' read -r hash message; do
[ -z "$hash" ] && continue
matched=false
for ((i = 0; i < NUM_CATEGORIES; i++)); do
prefix="${CATEGORY_PREFIXES[$i]}"
# Match "prefix: msg" or "prefix(scope): msg"
if [[ "$message" == "${prefix}:"* ]] || [[ "$message" == "${prefix}("* ]]; then
# Strip prefix from message
clean_msg="${message#"${prefix}"}"
# Strip optional (scope): prefix
if [[ "$clean_msg" == "("* ]]; then
clean_msg="${clean_msg#*): }"
clean_msg="${clean_msg#*):}"
else
clean_msg="${clean_msg#: }"
clean_msg="${clean_msg#:}"
fi
CATEGORY_COMMITS[$i]+="- ${clean_msg} (${hash:0:7})"$'\n'
matched=true
break
fi
done
if [ "$matched" = false ]; then
CATEGORY_COMMITS[$NUM_CATEGORIES]+="- ${message} (${hash:0:7})"$'\n'
fi
done <<< "$COMMITS"
fi
# Build changelog
CHANGELOG="# Release ${TAG}"$'\n'$'\n'
if [ -z "$PREV_TAG" ] && [ -z "$INPUT_PREVIOUS_TAG" ]; then
CHANGELOG+="First release! 🎉"$'\n'$'\n'
fi
if [ -z "$COMMITS" ]; then
CHANGELOG+="No commits found in range"$'\n'
else
# Write categorized sections
for ((i = 0; i < NUM_CATEGORIES; i++)); do
if [ -n "${CATEGORY_COMMITS[$i]}" ]; then
CHANGELOG+="## ${CATEGORY_HEADERS[$i]}"$'\n'$'\n'
CHANGELOG+="${CATEGORY_COMMITS[$i]}"$'\n'
fi
done
# Other changes
if [ -n "${CATEGORY_COMMITS[$NUM_CATEGORIES]}" ]; then
CHANGELOG+="## 📝 Other Changes"$'\n'$'\n'
CHANGELOG+="${CATEGORY_COMMITS[$NUM_CATEGORIES]}"$'\n'
fi
fi
CHANGELOG+="---"$'\n'
# Add compare link using Forgejo env vars
if [ -n "${GITEA_SERVER_URL:-}" ] && [ -n "${GITEA_REPOSITORY:-}" ]; then
COMPARE_BASE="${PREV_TAG:-$(git rev-list --max-parents=0 HEAD | head -1)}"
CHANGELOG+="**Full Changelog**: ${GITEA_SERVER_URL}/${GITEA_REPOSITORY}/compare/${COMPARE_BASE}...${TAG}"$'\n'
elif [ -n "$PREV_TAG" ]; then
CHANGELOG+="**Commits**: \`git log ${PREV_TAG}..${TAG}\`"$'\n'
else
CHANGELOG+="**First Release**"$'\n'
fi
echo ""
echo "=== Generated Changelog ==="
echo "$CHANGELOG"
echo "==========================="
# Write to GITHUB_OUTPUT using multiline delimiter
if [ -n "${GITHUB_OUTPUT:-}" ]; then
{
echo "changelog<<CHANGELOG_EOF"
echo "$CHANGELOG"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
fi