Spaces:
Runtime error
Runtime error
# 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<<EOF" | |
echo "$CHANGELOG_CONTENT" | |
echo "EOF" | |
} >> "$GITHUB_OUTPUT" | |
# Also print to stdout for local testing | |
echo "Generated changelog:" | |
echo "===================" | |
cat changelog.md | |
echo "===================" |