Created
April 10, 2025 09:30
-
-
Save rmpel/ec3b6e8b92a3d64a25c5d270a5e9b605 to your computer and use it in GitHub Desktop.
Build a changelog from the previous tag to head in a composer based website
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# A script to generate a changelog by comparing composer packages and project commits. | |
# | |
# Disclaimer: | |
# - Using this script is at your own risk. This script only uses READ operations, but I still | |
# do not take any responsibility. | |
# | |
# License: | |
# - BSD 2-Clause — Free to use with attribution. No warranty provided. | |
# | |
# Requirements: | |
# - Must be run from within the git repository | |
# - Assumes the default branch is named 'master' or 'main' | |
# - Requires composer to be installed and available in PATH | |
# - Uses git to obtain tags and commit messages | |
# - Requires `git`, `jq`, `awk`, `diff` | |
# - Supports Bash 3.2 on MacOS, should support Bash 4.x on any *nix system, but not tested. | |
# | |
# Usage: | |
# just run the script inside a root-project clone directory. | |
# A file final_changelog.txt will remain after succesful execution. | |
# Exit on error and treat unset variables as errors. | |
set -euo pipefail | |
# ----------------------------- | |
# Step 1: Verify Environment | |
# ----------------------------- | |
# Ensure we are on the master branch. | |
current_branch=$(git rev-parse --abbrev-ref HEAD) | |
if [ "$current_branch" != "master" ] && [ "$current_branch" != "main" ]; then | |
echo "Error: You are on branch '$current_branch'. Switch to 'master' before running the script." | |
exit 1 | |
fi | |
[ -f final_changelog.txt ] && rm final_changelog.txt || true | |
# Ensure that the working tree is clean. | |
if ! git diff-index --quiet HEAD --; then | |
echo "Error: Working tree is not clean. Please commit or stash your changes before proceeding." | |
exit 1 | |
fi | |
# Determine if the running Bash supports associative arrays. | |
bash_major=$(echo "$BASH_VERSION" | cut -d. -f1) | |
if (( bash_major >= 4 )); then | |
USE_ASSOC=1 | |
else | |
USE_ASSOC=0 | |
fi | |
# Ensure that the local branch is up-to-date with remote. | |
echo "Fetching updates from remote..." | |
git fetch origin | |
LOCAL=$(git rev-parse @) | |
REMOTE=$(git rev-parse @{u}) | |
BASE=$(git merge-base @ @{u}) | |
if [ "$LOCAL" = "$REMOTE" ]; then | |
echo "Local branch is up-to-date with remote." | |
elif [ "$LOCAL" = "$BASE" ]; then | |
echo "Error: Your branch is behind remote. Please pull the latest changes." | |
exit 1 | |
else | |
echo "Error: Local and remote branches have diverged. Please resolve this before proceeding." | |
exit 1 | |
fi | |
# ----------------------------- | |
# Step 2: Get Tags and Most Recent Tag | |
# ----------------------------- | |
# List all tags (sorted by creation date). | |
echo "Retrieving latest tag from git..." | |
latest_tag=$(git describe --tags --abbrev=0) | |
echo "DEBUG: Most recent tag is '$latest_tag'" | |
# ----------------------------- | |
# Step 3: Capture Composer Packages (Before Checkout) | |
# ----------------------------- | |
# List installed composer packages and their revisions before ensuring HEAD is at master. | |
echo "Listing installed composer packages before confirming HEAD..." | |
git checkout $latest_tag | |
composer install | |
composer show > packages_before.txt | |
# ----------------------------- | |
# Step 4: Confirm HEAD on master and Capture Composer Packages (After) | |
# ----------------------------- | |
# Ensure we are at master HEAD (this is precautionary – you should already be there). | |
git checkout master | |
composer install | |
echo "Listing installed composer packages at HEAD..." | |
composer show > packages_after.txt | |
# ----------------------------- | |
# Step 5: Build the List of Differences | |
# ----------------------------- | |
echo "Building the differences between the package lists..." | |
# 'diff' returns non-zero if differences are found, so we use '|| true' to continue script execution. | |
diff packages_before.txt packages_after.txt > packages_diff.txt || true | |
# ----------------------------- | |
# Step 6: List All Commit Messages for the Project | |
# ----------------------------- | |
echo "Listing all project commit messages from tag '$latest_tag' to HEAD..." | |
git log --pretty=format:"%h - %s" ${latest_tag}..HEAD > project_commits.txt | |
# ----------------------------- | |
# Step 7: Iterate over Changed Packages | |
# ----------------------------- | |
## Generate package update log | |
## $1 = package name | |
## $2 = old version | |
## $3 = new version | |
package_update_log() { | |
local pkg="$1" | |
local old="$2" | |
local new="$3" | |
if [[ -z "$pkg" || -z "$old" || -z "$new" ]]; then | |
echo "Usage: package_update_log <package> <old-version> <new-version>" >&2 | |
return 1 | |
fi | |
local path | |
path=vendor/composer/$(jq -r '.packages[] | select(.name=="'$pkg'") | ."install-path"' vendor/composer/installed.json) | |
path=$(realpath $path) | |
echo $path $old $new >&2 | |
if [[ -z "$path" || ! -d "$path/.git" ]]; then | |
echo "Package '$pkg' is not a git checkout, cannot determine changelog." >&2 | |
else | |
echo "==> $pkg: $old → $new" | |
git -C "$path" log --oneline --no-merges "$old..$new" | |
fi | |
} | |
if [ "$USE_ASSOC" -eq 1 ]; then | |
# Bash 4+ : Use associative arrays. | |
declare -A old_versions | |
declare -A new_versions | |
# Read through packages_diff.txt and record old and new package information. | |
while IFS= read -r line; do | |
if [[ $line =~ ^\<\ ]]; then | |
# Remove the "< " prefix and extract package and version. | |
content="${line:2}" | |
pkg=$(echo "$content" | awk '{print $1}') | |
version=$(echo "$content" | awk '{ | |
tag = $2 | |
rev = ($3 ~ /^[0-9a-f]{7,}$/ ? $3 : "") | |
print (rev != "" ? rev : tag) | |
}') | |
old_versions["$pkg"]="$version" | |
elif [[ $line =~ ^\>\ ]]; then | |
content="${line:2}" | |
pkg=$(echo "$content" | awk '{print $1}') | |
version=$(echo "$content" | awk '{ | |
tag = $2 | |
rev = ($3 ~ /^[0-9a-f]{7,}$/ ? $3 : "") | |
print (rev != "" ? rev : tag) | |
}') | |
new_versions["$pkg"]="$version" | |
fi | |
done < packages_diff.txt | |
# Build a combined list of all packages that appear in either old or new. | |
declare -A all_packages | |
for pkg in "${!old_versions[@]}"; do | |
all_packages["$pkg"]=1 | |
done | |
for pkg in "${!new_versions[@]}"; do | |
all_packages["$pkg"]=1 | |
done | |
{ | |
for pkg in "${!all_packages[@]}"; do | |
if [ -n "${old_versions[$pkg]:-}" ] && [ -n "${new_versions[$pkg]:-}" ]; then | |
echo "Package update for $pkg: from ${old_versions[$pkg]} to ${new_versions[$pkg]}" | |
package_update_log $pkg ${old_versions[$pkg]} ${new_versions[$pkg]} | |
elif [ -n "${old_versions[$pkg]:-}" ]; then | |
echo "Package removed: $pkg (was ${old_versions[$pkg]})" | |
elif [ -n "${new_versions[$pkg]:-}" ]; then | |
echo "Package added: $pkg (version ${new_versions[$pkg]})" | |
fi | |
done | |
} >> package_changelogs.txt | |
else | |
# Bash 3.x: Simulate associative arrays using paired indexed arrays. | |
old_keys=() | |
old_values=() | |
new_keys=() | |
new_values=() | |
# Read through packages_diff.txt and record old and new package information. | |
while IFS= read -r line; do | |
if [[ $line =~ ^\<\ ]]; then | |
content="${line:2}" | |
pkg=$(echo "$content" | awk '{print $1}') | |
version=$(echo "$content" | awk '{ | |
tag = $2 | |
rev = ($3 ~ /^[0-9a-f]{7,}$/ ? $3 : "") | |
print (rev != "" ? rev : tag) | |
}') | |
old_keys+=("$pkg") | |
old_values+=("$version") | |
elif [[ $line =~ ^\>\ ]]; then | |
content="${line:2}" | |
pkg=$(echo "$content" | awk '{print $1}') | |
version=$(echo "$content" | awk '{ | |
tag = $2 | |
rev = ($3 ~ /^[0-9a-f]{7,}$/ ? $3 : "") | |
print (rev != "" ? rev : tag) | |
}') | |
new_keys+=("$pkg") | |
new_values+=("$version") | |
fi | |
done < packages_diff.txt | |
# Helper function: retrieve value for a given package from a keys/values pair. | |
get_value() { | |
local key="$1" | |
shift | |
local -a keys=("${!1}") | |
local -a values=("${!2}") | |
local i | |
for i in "${!keys[@]}"; do | |
if [ "${keys[$i]}" = "$key" ]; then | |
echo "${values[$i]}" | |
return | |
fi | |
done | |
echo "" | |
} | |
# Build a combined unique list of all packages. | |
all_packages=() | |
for key in "${old_keys[@]}"; do | |
all_packages+=("$key") | |
done | |
for key in "${new_keys[@]}"; do | |
found=0 | |
for existing in "${all_packages[@]}"; do | |
if [ "$existing" = "$key" ]; then | |
found=1 | |
break | |
fi | |
done | |
if [ $found -eq 0 ]; then | |
all_packages+=("$key") | |
fi | |
done | |
{ | |
for pkg in "${all_packages[@]}"; do | |
old_ver=$(get_value "$pkg" old_keys[@] old_values[@]) | |
new_ver=$(get_value "$pkg" new_keys[@] new_values[@]) | |
if [ -n "$old_ver" ] && [ -n "$new_ver" ]; then | |
echo "Package update for $pkg: from $old_ver to $new_ver" | |
package_update_log $pkg $old_ver $new_ver | |
elif [ -n "$old_ver" ]; then | |
echo "Package removed: $pkg (was $old_ver)" | |
elif [ -n "$new_ver" ]; then | |
echo "Package added: $pkg (version $new_ver)" | |
fi | |
done | |
} >> package_changelogs.txt | |
fi | |
# ----------------------------- | |
# Step 8: Combine All Outputs to Final Changelog | |
# ----------------------------- | |
final_changelog="final_changelog.txt" | |
{ | |
echo "===============================" | |
echo "Project Changelog" | |
echo "From tag: $latest_tag" | |
echo "To HEAD on branch: master" | |
echo "Generated on: $(date)" | |
echo "===============================" | |
echo "" | |
echo "### Project Commit Messages" | |
cat project_commits.txt | |
echo "" | |
echo "### Plugins/Themes changes" | |
cat package_changelogs.txt | |
echo "" | |
} > "$final_changelog" | |
echo "Changelog generation complete. See '$final_changelog' for details." | |
# ----------------------------- | |
# Step 9: Cleanup Intermediate Files | |
# ----------------------------- | |
echo "Cleaning up intermediate files..." | |
rm -f all_tags.txt packages_before.txt packages_after.txt packages_diff.txt project_commits.txt package_changelogs.txt | |
echo "Cleanup complete." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment