Create Draft Release #19
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 Draft Release | |
| on: | |
| workflow_dispatch: | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Fail if not on main branch | |
| if: ${{ github.ref != 'refs/heads/main' }} | |
| run: | | |
| echo "❌ Error: This workflow must be run on the main branch." | |
| echo "Current ref: $GITHUB_REF" | |
| exit 1 | |
| - name: Generate GitHub App Token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: ".nvmrc" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Configure Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Determine version bump from merged PRs | |
| id: version | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| # Get the latest tag, default to v0.0.0 if no tags exist | |
| LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LATEST_TAG" ]; then | |
| echo "No tags found, starting from version 0.0.0" | |
| LATEST_TAG="v0.0.0" | |
| # Get all commits in the repository | |
| COMMITS=$(git log --oneline) | |
| else | |
| echo "Latest tag: $LATEST_TAG" | |
| # Get all commits since the last tag | |
| COMMITS=$(git log ${LATEST_TAG}..HEAD --oneline) | |
| fi | |
| # Check if there are any commits to process | |
| if [ -z "$COMMITS" ]; then | |
| echo "❌ Error: No commits found since $LATEST_TAG" | |
| echo "No changes have been made since the last release." | |
| exit 1 | |
| fi | |
| # Initialize semver bump level (0=none, 1=patch, 2=minor, 3=major) | |
| MAX_BUMP=0 # Must be explicitly set by PR checkbox | |
| # Parse each commit to find semver selections | |
| while IFS= read -r line; do | |
| if [ -z "$line" ]; then | |
| continue | |
| fi | |
| COMMIT_HASH=$(echo "$line" | awk '{print $1}') | |
| COMMIT_MESSAGE=${line#* } | |
| # Extract PR number from squash merge format: "Title (#123)" | |
| PR_NUMBER=$(echo "$COMMIT_MESSAGE" | grep -oE '\(#[0-9]+\)$' | grep -oE '[0-9]+') | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "❌ Error: No PR number found in commit $COMMIT_HASH" | |
| echo "Commit message: $COMMIT_MESSAGE" | |
| echo "All commits must be from merged PRs with format 'Title (#123)'" | |
| exit 1 | |
| fi | |
| echo "Checking PR #$PR_NUMBER for commit $COMMIT_HASH" | |
| # Fetch PR body using GitHub CLI | |
| GH_OUTPUT=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>&1) | |
| GH_EXIT_CODE=$? | |
| if [ $GH_EXIT_CODE -ne 0 ] || [ -z "$GH_OUTPUT" ]; then | |
| echo "❌ Error: Could not fetch PR body for #$PR_NUMBER" | |
| echo "gh CLI error output:" | |
| echo "$GH_OUTPUT" | |
| exit 1 | |
| fi | |
| # Check for semver selections in PR body (look for checked boxes) | |
| PR_BODY="$GH_OUTPUT" | |
| if echo "$PR_BODY" | grep -q "\[[xX]\] \*\*MAJOR\*\*"; then | |
| echo "Found MAJOR bump in PR #$PR_NUMBER" | |
| MAX_BUMP=3 | |
| elif echo "$PR_BODY" | grep -q "\[[xX]\] \*\*MINOR\*\*"; then | |
| echo "Found MINOR bump in PR #$PR_NUMBER" | |
| if [ $MAX_BUMP -lt 2 ]; then | |
| MAX_BUMP=2 | |
| fi | |
| elif echo "$PR_BODY" | grep -q "\[[xX]\] \*\*PATCH\*\*"; then | |
| echo "Found PATCH bump in PR #$PR_NUMBER" | |
| if [ $MAX_BUMP -lt 1 ]; then | |
| MAX_BUMP=1 | |
| fi | |
| else | |
| echo "❌ Error: No version bump checkbox selected in PR #$PR_NUMBER" | |
| echo "Please select MAJOR, MINOR, or PATCH in the PR description" | |
| exit 1 | |
| fi | |
| done <<< "$COMMITS" | |
| # Determine bump type | |
| case $MAX_BUMP in | |
| 3) | |
| BUMP_TYPE="major" | |
| ;; | |
| 2) | |
| BUMP_TYPE="minor" | |
| ;; | |
| 1) | |
| BUMP_TYPE="patch" | |
| ;; | |
| esac | |
| echo "Bump type: $BUMP_TYPE" | |
| # Export for later steps | |
| echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT | |
| - name: Update version in package.json | |
| id: npm_version | |
| run: | | |
| # Use npm version to automatically bump the version | |
| npm version ${{ steps.version.outputs.bump_type }} --no-git-tag-version | |
| # Read the new version | |
| NEW_VERSION=$(node -p "require('./package.json').version") | |
| echo "New version: $NEW_VERSION" | |
| # Export for later steps | |
| echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| - name: Update CHANGELOG.md | |
| run: | | |
| VERSION="${{ steps.npm_version.outputs.version }}" | |
| DATE=$(date +%Y-%m-%d) | |
| # Create a temporary file | |
| TEMP_FILE=$(mktemp) | |
| # Read the changelog and update it | |
| awk -v version="$VERSION" -v date="$DATE" ' | |
| BEGIN { found_unreleased = 0; seen_content = 0 } | |
| /## \[Unreleased\]/ { | |
| found_unreleased = 1 | |
| print "## [Unreleased]" | |
| print "" | |
| print "## [" version "] - " date | |
| print "" | |
| next | |
| } | |
| # Skip empty lines immediately after [Unreleased] until we hit content or another ## | |
| found_unreleased && /^$/ && !seen_content { | |
| next | |
| } | |
| found_unreleased && /./ { | |
| seen_content = 1 | |
| } | |
| { print } | |
| ' docs/CHANGELOG.md > "$TEMP_FILE" | |
| # Replace the original file | |
| mv "$TEMP_FILE" docs/CHANGELOG.md | |
| echo "✓ CHANGELOG.md updated: Added version $VERSION section" | |
| - name: Commit changelog and version bump | |
| run: | | |
| git add docs/CHANGELOG.md package.json package-lock.json | |
| git commit -m "Release version ${{ steps.npm_version.outputs.version }}" | |
| git push origin main | |
| - name: Create and push tag | |
| run: | | |
| # Create the tag pointing to the commit with updated files | |
| git tag -a "v${{ steps.npm_version.outputs.version }}" -m "Release version ${{ steps.npm_version.outputs.version }}" | |
| git push origin "v${{ steps.npm_version.outputs.version }}" | |
| - name: Create Draft GitHub Release | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const tagName = 'v${{ steps.npm_version.outputs.version }}'; | |
| // Create a draft release with auto-generated notes for manual curation | |
| const release = await github.rest.repos.createRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag_name: tagName, | |
| name: `Release ${tagName}`, | |
| body: `Release version ${{ steps.npm_version.outputs.version }} (${{ steps.version.outputs.bump_type }} bump)\n\nSee [CHANGELOG.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/docs/CHANGELOG.md) for details.`, | |
| draft: true, | |
| prerelease: false, | |
| generate_release_notes: true | |
| }); | |
| console.log(`Draft release created: ${release.data.html_url}`); | |
| console.log('Please review and publish the release manually from the GitHub UI.'); |