#!/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<> "$GITHUB_OUTPUT" fi