Skip to content

Commit 86ad4eb

Browse files
jwaldripclaude
andcommitted
feat(ci): add automatic version bump and changelog pipeline
Replicates han's auto-bump pattern for the ai-dlc root plugin. On push to main, determines bump type from conventional commit prefix (breaking→major, feat→minor, else→patch), updates both plugin.json and marketplace.json versions, generates changelog, and commits with [skip ci] to prevent loops. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d9b811 commit 86ad4eb

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Script to generate CHANGELOG.md based on git commit history
5+
# Usage: ./generate-changelog.sh <path> <new_version> <old_version>
6+
#
7+
# Arguments:
8+
# path: Path to the directory (e.g., "." for root plugin)
9+
# new_version: The new version being released (e.g., "1.2.3")
10+
# old_version: The previous version (e.g., "1.2.2") - optional, will auto-detect from plugin.json
11+
12+
PATH_DIR="$1"
13+
NEW_VERSION="$2"
14+
OLD_VERSION="$3"
15+
16+
if [ -z "$PATH_DIR" ] || [ -z "$NEW_VERSION" ]; then
17+
echo "Usage: $0 <path> <new_version> [old_version]"
18+
echo "Example: $0 . 1.2.3"
19+
exit 1
20+
fi
21+
22+
CHANGELOG_FILE="$PATH_DIR/CHANGELOG.md"
23+
TEMP_FILE=$(mktemp)
24+
25+
# Determine the previous version if not provided
26+
if [ -z "$OLD_VERSION" ]; then
27+
if [ -f "$PATH_DIR/.claude-plugin/plugin.json" ]; then
28+
OLD_VERSION=$(jq -r '.version' "$PATH_DIR/.claude-plugin/plugin.json" 2>/dev/null || echo "")
29+
fi
30+
fi
31+
32+
# Determine git range for commits
33+
# Get the last 50 commits and filter later
34+
GIT_RANGE="HEAD~50..HEAD"
35+
36+
# Get commits for this path, excluding version bump commits and website changes
37+
COMMITS=$(git log $GIT_RANGE --pretty=format:"%h|%s|%an|%ad" --date=short -- "$PATH_DIR" ':!website' 2>/dev/null | grep -v "\[skip ci\]" | grep -v "chore(release):" | grep -v "chore(plugin): bump" || true)
38+
39+
# If we still don't have commits, try without range limit
40+
if [ -z "$COMMITS" ]; then
41+
COMMITS=$(git log --all --pretty=format:"%h|%s|%an|%ad" --date=short -- "$PATH_DIR" ':!website' 2>/dev/null | grep -v "\[skip ci\]" | grep -v "chore(release):" | grep -v "chore(plugin): bump" | head -20 || true)
42+
fi
43+
44+
if [ -z "$COMMITS" ]; then
45+
echo "No commits found for $PATH_DIR in range $GIT_RANGE"
46+
exit 0
47+
fi
48+
49+
# Parse commits into categories
50+
FEATURES=""
51+
FIXES=""
52+
REFACTORS=""
53+
CHORES=""
54+
BREAKING=""
55+
OTHER=""
56+
57+
while IFS='|' read -r hash subject _author _date; do
58+
# Skip empty lines
59+
[ -z "$hash" ] && continue
60+
61+
# Remove scope from subject for cleaner display
62+
CLEAN_SUBJECT=$(echo "$subject" | sed -E 's/^[a-z]+(\([^)]+\))?!?: //')
63+
64+
# Format entry
65+
ENTRY="- $CLEAN_SUBJECT ([$hash](../../commit/$hash))"
66+
67+
# Categorize commit (use newline only between entries, not before first)
68+
if echo "$subject" | grep -qE '^[a-z]+(\([^)]+\))?!:' || echo "$subject" | grep -q 'BREAKING CHANGE'; then
69+
[ -n "$BREAKING" ] && BREAKING="$BREAKING\n$ENTRY" || BREAKING="$ENTRY"
70+
elif echo "$subject" | grep -qE '^feat(\([^)]+\))?:'; then
71+
[ -n "$FEATURES" ] && FEATURES="$FEATURES\n$ENTRY" || FEATURES="$ENTRY"
72+
elif echo "$subject" | grep -qE '^fix(\([^)]+\))?:'; then
73+
[ -n "$FIXES" ] && FIXES="$FIXES\n$ENTRY" || FIXES="$ENTRY"
74+
elif echo "$subject" | grep -qE '^refactor(\([^)]+\))?:'; then
75+
[ -n "$REFACTORS" ] && REFACTORS="$REFACTORS\n$ENTRY" || REFACTORS="$ENTRY"
76+
elif echo "$subject" | grep -qE '^chore(\([^)]+\))?:'; then
77+
[ -n "$CHORES" ] && CHORES="$CHORES\n$ENTRY" || CHORES="$ENTRY"
78+
else
79+
[ -n "$OTHER" ] && OTHER="$OTHER\n$ENTRY" || OTHER="$ENTRY"
80+
fi
81+
done <<<"$COMMITS"
82+
83+
# Generate changelog header
84+
{
85+
echo "# Changelog"
86+
echo ""
87+
echo "All notable changes to this project will be documented in this file."
88+
echo ""
89+
echo "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),"
90+
echo "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)."
91+
echo ""
92+
echo "## [$NEW_VERSION] - $(date +%Y-%m-%d)"
93+
echo ""
94+
} >"$TEMP_FILE"
95+
96+
# Add breaking changes section if any
97+
if [ -n "$BREAKING" ]; then
98+
{
99+
echo "### BREAKING CHANGES"
100+
echo ""
101+
echo -e "$BREAKING"
102+
echo ""
103+
} >>"$TEMP_FILE"
104+
fi
105+
106+
# Add features section if any
107+
if [ -n "$FEATURES" ]; then
108+
{
109+
echo "### Added"
110+
echo ""
111+
echo -e "$FEATURES"
112+
echo ""
113+
} >>"$TEMP_FILE"
114+
fi
115+
116+
# Add fixes section if any
117+
if [ -n "$FIXES" ]; then
118+
{
119+
echo "### Fixed"
120+
echo ""
121+
echo -e "$FIXES"
122+
echo ""
123+
} >>"$TEMP_FILE"
124+
fi
125+
126+
# Add refactors section if any
127+
if [ -n "$REFACTORS" ]; then
128+
{
129+
echo "### Changed"
130+
echo ""
131+
echo -e "$REFACTORS"
132+
echo ""
133+
} >>"$TEMP_FILE"
134+
fi
135+
136+
# Add other changes if any
137+
if [ -n "$OTHER" ]; then
138+
{
139+
echo "### Other"
140+
echo ""
141+
echo -e "$OTHER"
142+
echo ""
143+
} >>"$TEMP_FILE"
144+
fi
145+
146+
# If existing changelog exists, append old entries (excluding the header)
147+
if [ -f "$CHANGELOG_FILE" ]; then
148+
# Skip the first 6 lines (header) and append the rest
149+
tail -n +7 "$CHANGELOG_FILE" >>"$TEMP_FILE" 2>/dev/null || true
150+
fi
151+
152+
# Remove consecutive blank lines and ensure single trailing newline
153+
perl -i -0777 -pe 's/\n\n\n+/\n\n/g' "$TEMP_FILE"
154+
printf '%s\n' "$(cat "$TEMP_FILE")" >"$TEMP_FILE"
155+
156+
# Move temp file to final location
157+
mv "$TEMP_FILE" "$CHANGELOG_FILE"
158+
159+
echo "Changelog generated at $CHANGELOG_FILE"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Bump Plugin Version
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths-ignore:
8+
- ".claude-plugin/plugin.json"
9+
- ".claude-plugin/marketplace.json"
10+
- "CHANGELOG.md"
11+
- "website/**"
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
bump-version:
18+
runs-on: ubuntu-latest
19+
# Skip if commit is from bot (prevent loops) or if it's a version bump commit
20+
if: |
21+
github.actor != 'github-actions[bot]' &&
22+
!contains(github.event.head_commit.message, '[skip ci]')
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
token: ${{ secrets.GITHUB_TOKEN }}
29+
30+
- name: Pull latest changes
31+
run: |
32+
git config user.name "github-actions[bot]"
33+
git config user.email "github-actions[bot]@users.noreply.github.com"
34+
git pull --rebase origin main
35+
36+
- name: Determine version bump type
37+
id: bump-type
38+
env:
39+
COMMIT_MSG: ${{ github.event.head_commit.message }}
40+
run: |
41+
# Check for breaking changes (major bump)
42+
if echo "$COMMIT_MSG" | grep -qE '^[a-z]+(\(.+\))?!:|BREAKING CHANGE:'; then
43+
echo "type=major" >> $GITHUB_OUTPUT
44+
echo "Detected: MAJOR (breaking change)"
45+
# Check for features (minor bump)
46+
elif echo "$COMMIT_MSG" | grep -qE '^feat(\(.+\))?:'; then
47+
echo "type=minor" >> $GITHUB_OUTPUT
48+
echo "Detected: MINOR (new feature)"
49+
# Everything else (patch bump) - fix, chore, docs, refactor, etc.
50+
else
51+
echo "type=patch" >> $GITHUB_OUTPUT
52+
echo "Detected: PATCH (fix/chore/other)"
53+
fi
54+
55+
- name: Bump version
56+
id: bump-version
57+
run: |
58+
BUMP_TYPE="${{ steps.bump-type.outputs.type }}"
59+
PLUGIN_JSON=".claude-plugin/plugin.json"
60+
MARKETPLACE_JSON=".claude-plugin/marketplace.json"
61+
62+
# Get current version
63+
CURRENT_VERSION=$(jq -r '.version' "$PLUGIN_JSON")
64+
echo "Current version: $CURRENT_VERSION"
65+
66+
# Parse version components
67+
MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
68+
MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
69+
PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3)
70+
71+
# Bump based on type
72+
case "$BUMP_TYPE" in
73+
major)
74+
NEW_VERSION="$((MAJOR + 1)).0.0"
75+
;;
76+
minor)
77+
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
78+
;;
79+
patch)
80+
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
81+
;;
82+
esac
83+
84+
echo "New version: $NEW_VERSION"
85+
86+
# Update plugin.json
87+
jq --arg version "$NEW_VERSION" '.version = $version' "$PLUGIN_JSON" > "$PLUGIN_JSON.tmp"
88+
mv "$PLUGIN_JSON.tmp" "$PLUGIN_JSON"
89+
90+
# Update marketplace.json metadata.version
91+
jq --arg version "$NEW_VERSION" '.metadata.version = $version' "$MARKETPLACE_JSON" > "$MARKETPLACE_JSON.tmp"
92+
mv "$MARKETPLACE_JSON.tmp" "$MARKETPLACE_JSON"
93+
94+
# Generate changelog
95+
chmod +x .github/scripts/generate-changelog.sh
96+
.github/scripts/generate-changelog.sh "." "$NEW_VERSION" "$CURRENT_VERSION"
97+
98+
echo "old_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
99+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
100+
101+
- name: Commit version bump
102+
run: |
103+
# Check if there are any changes to commit
104+
if git diff --quiet; then
105+
echo "No version changes to commit"
106+
exit 0
107+
fi
108+
109+
# Add version files and changelog
110+
git add .claude-plugin/plugin.json .claude-plugin/marketplace.json CHANGELOG.md
111+
112+
# Create commit message
113+
COMMIT_MSG="chore(plugin): bump version ${{ steps.bump-version.outputs.old_version }} -> ${{ steps.bump-version.outputs.new_version }} [skip ci]"
114+
115+
git commit -m "$COMMIT_MSG"
116+
117+
# Pull latest and rebase, handling conflicts gracefully
118+
if ! git pull --rebase origin main; then
119+
echo "Rebase conflict detected - another workflow likely pushed first"
120+
echo "Aborting rebase and exiting cleanly (next push will retry)"
121+
git rebase --abort 2>/dev/null || true
122+
exit 0
123+
fi
124+
125+
# Push the rebased commit
126+
git push origin main

0 commit comments

Comments
 (0)