| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- #!/usr/bin/env bash
- set -euo pipefail
- # QMD Release Script
- # Usage: ./scripts/release.sh [patch|minor|major|<version>]
- # Examples:
- # ./scripts/release.sh patch # 0.9.0 -> 0.9.1
- # ./scripts/release.sh minor # 0.9.0 -> 0.10.0
- # ./scripts/release.sh major # 0.9.0 -> 1.0.0
- # ./scripts/release.sh 1.0.0 # explicit version
- BUMP="${1:?Usage: release.sh [patch|minor|major|<version>]}"
- # Ensure we're on main and clean
- BRANCH=$(git branch --show-current)
- if [[ "$BRANCH" != "main" ]]; then
- echo "Error: must be on main branch (currently on $BRANCH)" >&2
- exit 1
- fi
- if [[ -n "$(git status --porcelain)" ]]; then
- echo "Error: working directory not clean" >&2
- git status --short
- exit 1
- fi
- # Read current version
- CURRENT=$(jq -r .version package.json)
- echo "Current version: $CURRENT"
- # Calculate new version
- bump_version() {
- local current="$1" type="$2"
- IFS='.' read -r major minor patch <<< "$current"
- case "$type" in
- major) echo "$((major + 1)).0.0" ;;
- minor) echo "$major.$((minor + 1)).0" ;;
- patch) echo "$major.$minor.$((patch + 1))" ;;
- *) echo "$type" ;; # explicit version
- esac
- }
- NEW=$(bump_version "$CURRENT" "$BUMP")
- echo "New version: $NEW"
- echo ""
- # Confirm
- read -p "Release v$NEW? [y/N] " -n 1 -r
- echo ""
- [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; }
- # Gather commits since last tag (or all if no tags)
- LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
- if [[ -n "$LAST_TAG" ]]; then
- RANGE="$LAST_TAG..HEAD"
- else
- RANGE="HEAD"
- fi
- echo ""
- echo "Commits since ${LAST_TAG:-beginning}:"
- git log "$RANGE" --oneline --no-decorate
- echo ""
- # Generate changelog entry
- DATE=$(date +%Y-%m-%d)
- ENTRY="## [$NEW] - $DATE"$'\n'$'\n'
- # Collect conventional commits
- FEATS=$(git log "$RANGE" --oneline --no-decorate --grep="^feat" | sed 's/^[a-f0-9]* feat[:(]/- /' | sed 's/)$//' || true)
- FIXES=$(git log "$RANGE" --oneline --no-decorate --grep="^fix" | sed 's/^[a-f0-9]* fix[:(]/- /' | sed 's/)$//' || true)
- OTHER=$(git log "$RANGE" --oneline --no-decorate --grep="^feat" --grep="^fix" --grep="^docs" --grep="^chore" --grep="^refactor" --invert-grep | sed 's/^[a-f0-9]* /- /' || true)
- if [[ -n "$FEATS" ]]; then
- ENTRY+="### Features"$'\n'$'\n'"$FEATS"$'\n'$'\n'
- fi
- if [[ -n "$FIXES" ]]; then
- ENTRY+="### Fixes"$'\n'$'\n'"$FIXES"$'\n'$'\n'
- fi
- if [[ -n "$OTHER" ]]; then
- ENTRY+="### Other"$'\n'$'\n'"$OTHER"$'\n'$'\n'
- fi
- # Add link reference
- LINK="[$NEW]: https://github.com/tobi/qmd/compare/v$CURRENT...v$NEW"
- # Show what will be added
- echo "--- Changelog entry ---"
- echo "$ENTRY"
- echo "$LINK"
- echo "--- End ---"
- echo ""
- read -p "Looks good? [y/N] " -n 1 -r
- echo ""
- [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; }
- # Update package.json version
- jq --arg v "$NEW" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
- # Prepend changelog entry (after the header line)
- if [[ -f CHANGELOG.md ]]; then
- # Insert after "# Changelog" header and any blank lines
- awk -v entry="$ENTRY$LINK" '
- /^# Changelog/ { print; getline; print; print ""; print entry; print ""; next }
- { print }
- ' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
- else
- echo "# Changelog"$'\n'$'\n'"$ENTRY$LINK" > CHANGELOG.md
- fi
- # Commit and tag
- git add package.json CHANGELOG.md
- git commit -m "release: v$NEW"
- git tag -a "v$NEW" -m "v$NEW"
- echo ""
- echo "Created commit and tag v$NEW"
- echo ""
- echo "Next steps:"
- echo " git push origin main --tags # push to GitHub"
- echo " npm publish # publish to npm"
- echo ""
- echo "Or both at once:"
- echo " git push origin main --tags && npm publish"
|