Create Release PR #1
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
| name: Create Release PR | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version_type: | |
| description: 'Version bump type' | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| - auto | |
| default: 'auto' | |
| custom_version: | |
| description: 'Custom version (overrides version_type if set, e.g., 1.2.3)' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| create-release-pr: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout main branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Get current version | |
| id: current | |
| run: | | |
| CURRENT_VERSION=$(jq -r '.version' package.json) | |
| echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| echo "Current version: $CURRENT_VERSION" | |
| - name: Determine version bump from commits | |
| id: analyze | |
| if: inputs.version_type == 'auto' && inputs.custom_version == '' | |
| run: | | |
| # Get commits since last tag | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LAST_TAG" ]; then | |
| COMMITS=$(git log --oneline) | |
| else | |
| COMMITS=$(git log --oneline ${LAST_TAG}..HEAD) | |
| fi | |
| echo "Commits since last tag:" | |
| echo "$COMMITS" | |
| # Analyze commits for version bump (conventional commits) | |
| BUMP="patch" | |
| if echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?!:|BREAKING CHANGE"; then | |
| BUMP="major" | |
| elif echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?:"; then | |
| BUMP="minor" | |
| fi | |
| echo "bump=$BUMP" >> $GITHUB_OUTPUT | |
| echo "Determined bump type: $BUMP" | |
| - name: Calculate new version | |
| id: version | |
| run: | | |
| CURRENT="${{ steps.current.outputs.version }}" | |
| # Use custom version if provided | |
| if [ -n "${{ inputs.custom_version }}" ]; then | |
| CUSTOM="${{ inputs.custom_version }}" | |
| # Strip optional 'v' prefix and validate strict semver (X.Y.Z) | |
| CUSTOM="${CUSTOM#v}" | |
| if [[ ! "$CUSTOM" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Invalid custom version '${{ inputs.custom_version }}'. Must be semver (e.g., 1.2.3 or v1.2.3)." | |
| exit 1 | |
| fi | |
| NEW_VERSION="$CUSTOM" | |
| else | |
| # Determine bump type | |
| if [ "${{ inputs.version_type }}" == "auto" ]; then | |
| BUMP="${{ steps.analyze.outputs.bump }}" | |
| else | |
| BUMP="${{ inputs.version_type }}" | |
| fi | |
| # Split version into parts | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" | |
| # Validate no prerelease suffix (e.g., 1.0.0-alpha.1) | |
| if [[ ! "$MAJOR" =~ ^[0-9]+$ ]] || [[ ! "$MINOR" =~ ^[0-9]+$ ]] || [[ ! "$PATCH" =~ ^[0-9]+$ ]]; then | |
| echo "::error::Prerelease versions not supported (got: $CURRENT). Use explicit version input instead." | |
| exit 1 | |
| fi | |
| case "$BUMP" in | |
| major) | |
| NEW_VERSION="$((MAJOR + 1)).0.0" | |
| ;; | |
| minor) | |
| NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" | |
| ;; | |
| patch) | |
| NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" | |
| ;; | |
| esac | |
| fi | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "New version: $NEW_VERSION" | |
| - name: Create release branch | |
| run: | | |
| BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}" | |
| git checkout -b "$BRANCH_NAME" | |
| echo "Created branch: $BRANCH_NAME" | |
| - name: Update package.json version | |
| run: | | |
| NEW_VERSION="${{ steps.version.outputs.new_version }}" | |
| jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp | |
| mv package.json.tmp package.json | |
| echo "Updated package.json to version $NEW_VERSION" | |
| - name: Generate changelog entry | |
| id: changelog | |
| run: | | |
| NEW_VERSION="${{ steps.version.outputs.new_version }}" | |
| TODAY=$(date +%Y-%m-%d) | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| # Get commits grouped by type | |
| if [ -z "$LAST_TAG" ]; then | |
| COMMITS=$(git log --pretty=format:"%s" HEAD) | |
| else | |
| COMMITS=$(git log --pretty=format:"%s" ${LAST_TAG}..HEAD) | |
| fi | |
| # Build changelog entry | |
| ENTRY="## [$NEW_VERSION] - $TODAY"$'\n\n' | |
| # Features (normalize case before sed to handle Feat/FEAT variants) | |
| FEATURES=$(echo "$COMMITS" | grep -iE "^feat(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^feat(\([^)]*\)): /- **\1**: /' | sed 's/^feat: /- /' || true) | |
| if [ -n "$FEATURES" ]; then | |
| ENTRY+="### Added"$'\n'"$FEATURES"$'\n\n' | |
| fi | |
| # Fixes (normalize case before sed to handle Fix/FIX variants) | |
| FIXES=$(echo "$COMMITS" | grep -iE "^fix(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^fix(\([^)]*\)): /- **\1**: /' | sed 's/^fix: /- /' || true) | |
| if [ -n "$FIXES" ]; then | |
| ENTRY+="### Fixed"$'\n'"$FIXES"$'\n\n' | |
| fi | |
| # Other changes (refactor, perf, style, or non-conventional commits) | |
| OTHERS=$(echo "$COMMITS" | grep -ivE "^(feat|fix|chore|ci|docs|test)(\(.+\))?:" | sed 's/^/- /' || true) | |
| if [ -n "$OTHERS" ]; then | |
| ENTRY+="### Changed"$'\n'"$OTHERS"$'\n\n' | |
| fi | |
| # Save entry to file for use in PR body | |
| echo "$ENTRY" > /tmp/changelog_entry.md | |
| # Prepend to CHANGELOG.md if it exists | |
| # Handles [Unreleased] section: insert new version AFTER it | |
| if [ -f "CHANGELOG.md" ]; then | |
| # Check for [Unreleased] section (case-insensitive) | |
| UNRELEASED_LINE=$(grep -in '^## \[Unreleased\]' CHANGELOG.md | head -1 | cut -d: -f1) | |
| # Find first versioned section (## [x.y.z]) | |
| FIRST_VERSION_LINE=$(grep -n '^## \[[0-9]' CHANGELOG.md | head -1 | cut -d: -f1) | |
| if [ -n "$UNRELEASED_LINE" ]; then | |
| # Has [Unreleased] section - insert new version after it | |
| head -n "$UNRELEASED_LINE" CHANGELOG.md > /tmp/changelog_new.md | |
| echo "" >> /tmp/changelog_new.md | |
| cat /tmp/changelog_entry.md >> /tmp/changelog_new.md | |
| tail -n +"$((UNRELEASED_LINE + 1))" CHANGELOG.md >> /tmp/changelog_new.md | |
| elif [ -n "$FIRST_VERSION_LINE" ]; then | |
| # No [Unreleased], insert before first version section | |
| head -n "$((FIRST_VERSION_LINE - 1))" CHANGELOG.md > /tmp/changelog_new.md | |
| echo "" >> /tmp/changelog_new.md | |
| cat /tmp/changelog_entry.md >> /tmp/changelog_new.md | |
| tail -n +"$FIRST_VERSION_LINE" CHANGELOG.md >> /tmp/changelog_new.md | |
| else | |
| # No version sections found, append to end | |
| cat CHANGELOG.md > /tmp/changelog_new.md | |
| echo "" >> /tmp/changelog_new.md | |
| cat /tmp/changelog_entry.md >> /tmp/changelog_new.md | |
| fi | |
| mv /tmp/changelog_new.md CHANGELOG.md | |
| echo "Updated CHANGELOG.md" | |
| fi | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Commit changes | |
| run: | | |
| NEW_VERSION="${{ steps.version.outputs.new_version }}" | |
| git add package.json | |
| [ -f CHANGELOG.md ] && git add CHANGELOG.md | |
| git commit -m "chore(release): prepare v$NEW_VERSION" | |
| - name: Push branch | |
| run: | | |
| BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}" | |
| git push -u origin "$BRANCH_NAME" | |
| - name: Create Pull Request | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| NEW_VERSION="${{ steps.version.outputs.new_version }}" | |
| CURRENT_VERSION="${{ steps.current.outputs.version }}" | |
| # Create PR body | |
| cat << 'EOF' > /tmp/pr_body.md | |
| ## Release v$NEW_VERSION | |
| This PR prepares the release of version **$NEW_VERSION** (from $CURRENT_VERSION). | |
| ### Changes included | |
| EOF | |
| # Append changelog entry if exists | |
| if [ -f /tmp/changelog_entry.md ]; then | |
| cat /tmp/changelog_entry.md >> /tmp/pr_body.md | |
| fi | |
| cat << 'EOF' >> /tmp/pr_body.md | |
| --- | |
| ### Checklist | |
| - [ ] Version bump is correct | |
| - [ ] CHANGELOG.md is accurate | |
| - [ ] All tests pass | |
| ### After merge | |
| When this PR is merged: | |
| 1. A git tag `v$NEW_VERSION` will be created automatically | |
| 2. A GitHub release will be published | |
| 3. The package will be published to npm | |
| EOF | |
| # Replace version placeholders | |
| sed -i "s/\$NEW_VERSION/$NEW_VERSION/g" /tmp/pr_body.md | |
| sed -i "s/\$CURRENT_VERSION/$CURRENT_VERSION/g" /tmp/pr_body.md | |
| gh pr create \ | |
| --title "Release v$NEW_VERSION" \ | |
| --body-file /tmp/pr_body.md \ | |
| --base main \ | |
| --label "release" \ | |
| --label "automated" |