release.sh 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # QMD Release Script
  4. #
  5. # Renames the [Unreleased] section in CHANGELOG.md to the new version,
  6. # bumps package.json, commits, and creates a tag. The actual publish
  7. # happens via GitHub Actions when the tag is pushed.
  8. #
  9. # Usage: ./scripts/release.sh [patch|minor|major|<version>]
  10. # Examples:
  11. # ./scripts/release.sh patch # 0.9.0 -> 0.9.1
  12. # ./scripts/release.sh minor # 0.9.0 -> 0.10.0
  13. # ./scripts/release.sh major # 0.9.0 -> 1.0.0
  14. # ./scripts/release.sh 1.0.0 # explicit version
  15. BUMP="${1:?Usage: release.sh [patch|minor|major|<version>]}"
  16. # Ensure we're on main and clean
  17. BRANCH=$(git branch --show-current)
  18. if [[ "$BRANCH" != "main" ]]; then
  19. echo "Error: must be on main branch (currently on $BRANCH)" >&2
  20. exit 1
  21. fi
  22. if [[ -n "$(git status --porcelain)" ]]; then
  23. echo "Error: working directory not clean" >&2
  24. git status --short
  25. exit 1
  26. fi
  27. # Verify bun.lock is in sync with package.json
  28. if ! bun install --frozen-lockfile &>/dev/null; then
  29. echo "Error: bun.lock is out of sync with package.json" >&2
  30. echo "Run 'bun install' and commit the updated lockfile." >&2
  31. exit 1
  32. fi
  33. echo "bun.lock: in sync ✓"
  34. # Read current version
  35. CURRENT=$(jq -r .version package.json)
  36. echo "Current version: $CURRENT"
  37. # Calculate new version
  38. bump_version() {
  39. local current="$1" type="$2"
  40. IFS='.' read -r major minor patch <<< "$current"
  41. case "$type" in
  42. major) echo "$((major + 1)).0.0" ;;
  43. minor) echo "$major.$((minor + 1)).0" ;;
  44. patch) echo "$major.$minor.$((patch + 1))" ;;
  45. *) echo "$type" ;; # explicit version
  46. esac
  47. }
  48. NEW=$(bump_version "$CURRENT" "$BUMP")
  49. DATE=$(date +%Y-%m-%d)
  50. echo "New version: $NEW"
  51. echo ""
  52. # --- Validate CHANGELOG.md ---
  53. if [[ ! -f CHANGELOG.md ]]; then
  54. echo "Error: CHANGELOG.md not found" >&2
  55. exit 1
  56. fi
  57. # The [Unreleased] section must have content
  58. if ! grep -q "^## \[Unreleased\]" CHANGELOG.md; then
  59. echo "Error: no [Unreleased] section in CHANGELOG.md" >&2
  60. echo "" >&2
  61. echo "Add your changes under an [Unreleased] heading first:" >&2
  62. echo "" >&2
  63. echo " ## [Unreleased]" >&2
  64. echo "" >&2
  65. echo " ### Changes" >&2
  66. echo " - Your change here" >&2
  67. exit 1
  68. fi
  69. # --- Preview release notes ---
  70. echo "--- Release notes (will appear on GitHub) ---"
  71. ./scripts/extract-changelog.sh "$NEW"
  72. echo "--- End ---"
  73. echo ""
  74. # --- Confirm ---
  75. read -p "Release v$NEW? [y/N] " -n 1 -r
  76. echo ""
  77. [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; }
  78. # --- Rename [Unreleased] -> [X.Y.Z] - date, add fresh [Unreleased] ---
  79. sed -i '' "s/^## \[Unreleased\].*/## [$NEW] - $DATE/" CHANGELOG.md
  80. # Insert a new empty [Unreleased] section after the header
  81. awk '
  82. /^## \['"$NEW"'\]/ && !done {
  83. print "## [Unreleased]\n"
  84. done = 1
  85. }
  86. { print }
  87. ' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
  88. # --- Bump version and commit ---
  89. jq --arg v "$NEW" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
  90. git add package.json CHANGELOG.md
  91. git commit -m "release: v$NEW"
  92. git tag -a "v$NEW" -m "v$NEW"
  93. echo ""
  94. echo "Created commit and tag v$NEW"
  95. echo ""
  96. echo "Next: push to trigger the publish workflow"
  97. echo ""
  98. echo " git push origin main --tags"