#!/usr/bin/env bash # Ensure we're running in bash if [ -z "$BASH_VERSION" ]; then echo "This script requires bash. Please run with: bash $0" >&2 exit 1 fi # Ensure we're using bash 4.0 or later for associative arrays if ((BASH_VERSINFO[0] < 4)); then echo "This script requires bash version 4 or later" >&2 echo "Current bash version: $BASH_VERSION" >&2 exit 1 fi # Set default values for required environment variables if not in GitHub Actions if [ -z "$GITHUB_ACTIONS" ]; then : "${GITHUB_SERVER_URL:=https://github.com}" : "${GITHUB_REPOSITORY:=stackblitz-labs/bolt.diy}" : "${GITHUB_OUTPUT:=/tmp/github_output}" touch "$GITHUB_OUTPUT" # Running locally echo "Running locally - checking for upstream remote..." MAIN_REMOTE="origin" if git remote -v | grep -q "upstream"; then MAIN_REMOTE="upstream" fi MAIN_BRANCH="main" # or "master" depending on your repository # Ensure we have latest tags git fetch ${MAIN_REMOTE} --tags # Use the remote reference for git log GITLOG_REF="${MAIN_REMOTE}/${MAIN_BRANCH}" else # Running in GitHub Actions GITLOG_REF="HEAD" fi # Get the latest tag LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") # Start changelog file echo "# ๐Ÿš€ Release v${NEW_VERSION}" > changelog.md echo "" >> changelog.md echo "## What's Changed ๐ŸŒŸ" >> changelog.md echo "" >> changelog.md if [ -z "$LATEST_TAG" ]; then echo "### ๐ŸŽ‰ First Release" >> changelog.md echo "" >> changelog.md echo "Exciting times! This marks our first release. Thanks to everyone who contributed! ๐Ÿ™Œ" >> changelog.md echo "" >> changelog.md COMPARE_BASE="$(git rev-list --max-parents=0 HEAD)" else echo "### ๐Ÿ”„ Changes since $LATEST_TAG" >> changelog.md echo "" >> changelog.md COMPARE_BASE="$LATEST_TAG" fi # Function to extract conventional commit type and associated emoji get_commit_type() { local msg="$1" if [[ $msg =~ ^feat(\(.+\))?:|^feature(\(.+\))?: ]]; then echo "โœจ Features" elif [[ $msg =~ ^fix(\(.+\))?: ]]; then echo "๐Ÿ› Bug Fixes" elif [[ $msg =~ ^docs(\(.+\))?: ]]; then echo "๐Ÿ“š Documentation" elif [[ $msg =~ ^style(\(.+\))?: ]]; then echo "๐Ÿ’Ž Styles" elif [[ $msg =~ ^refactor(\(.+\))?: ]]; then echo "โ™ป๏ธ Code Refactoring" elif [[ $msg =~ ^perf(\(.+\))?: ]]; then echo "โšก Performance Improvements" elif [[ $msg =~ ^test(\(.+\))?: ]]; then echo "๐Ÿงช Tests" elif [[ $msg =~ ^build(\(.+\))?: ]]; then echo "๐Ÿ› ๏ธ Build System" elif [[ $msg =~ ^ci(\(.+\))?: ]]; then echo "โš™๏ธ CI" elif [[ $msg =~ ^chore(\(.+\))?: ]]; then echo "" # Skip chore commits else echo "๐Ÿ” Other Changes" # Default category with emoji fi } # Initialize associative arrays declare -A CATEGORIES declare -A COMMITS_BY_CATEGORY declare -A ALL_AUTHORS declare -A NEW_CONTRIBUTORS # Get all historical authors before the compare base while IFS= read -r author; do ALL_AUTHORS["$author"]=1 done < <(git log "${COMPARE_BASE}" --pretty=format:"%ae" | sort -u) # Process all commits since last tag while IFS= read -r commit_line; do if [[ ! $commit_line =~ ^[a-f0-9]+\| ]]; then echo "WARNING: Skipping invalid commit line format: $commit_line" >&2 continue fi HASH=$(echo "$commit_line" | cut -d'|' -f1) COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2) BODY=$(echo "$commit_line" | cut -d'|' -f3) # Skip if hash doesn't match the expected format if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then continue fi HASH=$(echo "$commit_line" | cut -d'|' -f1) COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2) BODY=$(echo "$commit_line" | cut -d'|' -f3) # Validate hash format if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then echo "WARNING: Invalid commit hash format: $HASH" >&2 continue fi # Check if it's a merge commit if [[ $COMMIT_MSG =~ Merge\ pull\ request\ #([0-9]+) ]]; then # echo "Processing as merge commit" >&2 PR_NUM="${BASH_REMATCH[1]}" # Extract the PR title from the merge commit body PR_TITLE=$(echo "$BODY" | grep -v "^Merge pull request" | head -n 1) # Only process if it follows conventional commit format CATEGORY=$(get_commit_type "$PR_TITLE") if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit # Get PR author's GitHub username GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login') if [ -n "$GITHUB_USERNAME" ]; then # Check if this is a first-time contributor AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH") if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1 ALL_AUTHORS["$AUTHOR_EMAIL"]=1 fi CATEGORIES["$CATEGORY"]=1 COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n' else COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n' fi fi # Check if it's a squash merge by looking for (#NUMBER) pattern elif [[ $COMMIT_MSG =~ \(#([0-9]+)\) ]]; then # echo "Processing as squash commit" >&2 PR_NUM="${BASH_REMATCH[1]}" # Only process if it follows conventional commit format CATEGORY=$(get_commit_type "$COMMIT_MSG") if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit # Get PR author's GitHub username GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login') if [ -n "$GITHUB_USERNAME" ]; then # Check if this is a first-time contributor AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH") if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1 ALL_AUTHORS["$AUTHOR_EMAIL"]=1 fi CATEGORIES["$CATEGORY"]=1 COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n' else COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n' fi fi else # echo "Processing as regular commit" >&2 # Process conventional commits without PR numbers CATEGORY=$(get_commit_type "$COMMIT_MSG") if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit # Get commit author info AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH") # Try to get GitHub username using gh api if [ -n "$GITHUB_ACTIONS" ] || command -v gh >/dev/null 2>&1; then GITHUB_USERNAME=$(gh api "/repos/${GITHUB_REPOSITORY}/commits/${HASH}" --jq '.author.login' 2>/dev/null) fi if [ -n "$GITHUB_USERNAME" ]; then # If we got GitHub username, use it if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1 ALL_AUTHORS["$AUTHOR_EMAIL"]=1 fi CATEGORIES["$CATEGORY"]=1 COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by @$GITHUB_USERNAME"$'\n' else # Fallback to git author name if no GitHub username found AUTHOR_NAME=$(git show -s --format='%an' "$HASH") if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then NEW_CONTRIBUTORS["$AUTHOR_NAME"]=1 ALL_AUTHORS["$AUTHOR_EMAIL"]=1 fi CATEGORIES["$CATEGORY"]=1 COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by $AUTHOR_NAME"$'\n' fi fi fi done < <(git log "${COMPARE_BASE}..${GITLOG_REF}" --pretty=format:"%H|%s|%b" --reverse --first-parent) # Write categorized commits to changelog with their emojis for category in "โœจ Features" "๐Ÿ› Bug Fixes" "๐Ÿ“š Documentation" "๐Ÿ’Ž Styles" "โ™ป๏ธ Code Refactoring" "โšก Performance Improvements" "๐Ÿงช Tests" "๐Ÿ› ๏ธ Build System" "โš™๏ธ CI" "๐Ÿ” Other Changes"; do if [ -n "${COMMITS_BY_CATEGORY[$category]}" ]; then echo "### $category" >> changelog.md echo "" >> changelog.md echo "${COMMITS_BY_CATEGORY[$category]}" >> changelog.md echo "" >> changelog.md fi done # Add first-time contributors section if there are any if [ ${#NEW_CONTRIBUTORS[@]} -gt 0 ]; then echo "## โœจ First-time Contributors" >> changelog.md echo "" >> changelog.md echo "A huge thank you to our amazing new contributors! Your first contribution marks the start of an exciting journey! ๐ŸŒŸ" >> changelog.md echo "" >> changelog.md # Use readarray to sort the keys readarray -t sorted_contributors < <(printf '%s\n' "${!NEW_CONTRIBUTORS[@]}" | sort) for github_username in "${sorted_contributors[@]}"; do echo "* ๐ŸŒŸ [@$github_username](https://github.com/$github_username)" >> changelog.md done echo "" >> changelog.md fi # Add compare link if not first release if [ -n "$LATEST_TAG" ]; then echo "## ๐Ÿ“ˆ Stats" >> changelog.md echo "" >> changelog.md echo "**Full Changelog**: [\`$LATEST_TAG..v${NEW_VERSION}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/$LATEST_TAG...v${NEW_VERSION})" >> changelog.md fi # Output the changelog content CHANGELOG_CONTENT=$(cat changelog.md) { echo "content<> "$GITHUB_OUTPUT" # Also print to stdout for local testing echo "Generated changelog:" echo "===================" cat changelog.md echo "==================="