Release #13
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: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version-bump: | |
| description: 'Version bump type (major, minor, or patch)' | |
| required: true | |
| default: 'patch' | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| permissions: | |
| contents: write | |
| issues: write | |
| jobs: | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run all tests | |
| run: ./tests/run-all-tests.sh | |
| release: | |
| name: Create Release | |
| needs: test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Required for git-cliff | |
| - name: Set up Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Determine next version | |
| id: version | |
| run: | | |
| # Get the latest tag | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| latest_version=${latest_tag#v} | |
| # Split version into components | |
| IFS='.' read -r -a version_parts <<< "$latest_version" | |
| major=${version_parts[0]} | |
| minor=${version_parts[1]} | |
| patch=${version_parts[2]} | |
| # Bump version based on input | |
| case "${{ github.event.inputs.version-bump }}" in | |
| major) | |
| major=$((major + 1)) | |
| minor=0 | |
| patch=0 | |
| ;; | |
| minor) | |
| minor=$((minor + 1)) | |
| patch=0 | |
| ;; | |
| patch) | |
| patch=$((patch + 1)) | |
| ;; | |
| esac | |
| new_version="v$major.$minor.$patch" | |
| echo "new_version=$new_version" >> $GITHUB_OUTPUT | |
| - name: Generate release notes | |
| id: release_notes | |
| run: | | |
| # Get the latest tag | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| # Generate release notes from commits since last tag | |
| echo "## What's Changed" > release_notes.md | |
| echo "" >> release_notes.md | |
| # Get commits since last tag | |
| if [ "$latest_tag" != "v0.0.0" ]; then | |
| git log --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" ${latest_tag}..HEAD >> release_notes.md | |
| else | |
| git log --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" >> release_notes.md | |
| fi | |
| echo "" >> release_notes.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${latest_tag}...${{ steps.version.outputs.new_version }}" >> release_notes.md | |
| # Set output | |
| { | |
| echo 'content<<EOF' | |
| cat release_notes.md | |
| echo EOF | |
| } >> $GITHUB_OUTPUT | |
| - name: Generate and update CHANGELOG.md | |
| run: | | |
| #!/bin/bash | |
| set -euo pipefail | |
| # Get the latest tag for comparison | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| echo "Generating changelog for ${{ steps.version.outputs.new_version }} (since $latest_tag)" | |
| # Create temporary files for categorizing commits | |
| added_file=$(mktemp) | |
| changed_file=$(mktemp) | |
| fixed_file=$(mktemp) | |
| security_file=$(mktemp) | |
| deprecated_file=$(mktemp) | |
| removed_file=$(mktemp) | |
| other_file=$(mktemp) | |
| # Get commits since last tag | |
| if [ "$latest_tag" != "v0.0.0" ]; then | |
| commits=$(git log --pretty=format:"%s" ${latest_tag}..HEAD) | |
| else | |
| commits=$(git log --pretty=format:"%s") | |
| fi | |
| # Process each commit and categorize based on conventional commits | |
| echo "$commits" | while IFS= read -r commit; do | |
| case "$commit" in | |
| feat\(*|feat:*) | |
| echo "- ${commit#feat*: }" >> "$added_file" | |
| ;; | |
| fix\(*|fix:*) | |
| echo "- ${commit#fix*: }" >> "$fixed_file" | |
| ;; | |
| security\(*|security:*) | |
| echo "- ${commit#security*: }" >> "$security_file" | |
| ;; | |
| deprecate\(*|deprecate:*|deprecated\(*|deprecated:*) | |
| echo "- ${commit#deprecat*: }" >> "$deprecated_file" | |
| ;; | |
| remove\(*|remove:*|removed\(*|removed:*) | |
| echo "- ${commit#remov*: }" >> "$removed_file" | |
| ;; | |
| docs\(*|docs:*|chore\(*|chore:*|refactor\(*|refactor:*|perf\(*|perf:*|style\(*|style:*|test\(*|test:*) | |
| echo "- ${commit}" >> "$changed_file" | |
| ;; | |
| *) | |
| echo "- ${commit}" >> "$other_file" | |
| ;; | |
| esac | |
| done | |
| # Create the new changelog entry | |
| { | |
| echo "## [${{ steps.version.outputs.new_version }}] - $(date +%Y-%m-%d)" | |
| echo "" | |
| } > new_entry.md | |
| # Add categorized changes following Keep a Changelog format | |
| for category in "Added:$added_file" "Changed:$changed_file" "Deprecated:$deprecated_file" "Removed:$removed_file" "Fixed:$fixed_file" "Security:$security_file"; do | |
| category_name="${category%:*}" | |
| file_path="${category#*:}" | |
| if [ -s "$file_path" ]; then | |
| echo "### $category_name" >> new_entry.md | |
| echo "" >> new_entry.md | |
| cat "$file_path" >> new_entry.md | |
| echo "" >> new_entry.md | |
| fi | |
| done | |
| # Add other changes if any | |
| if [ -s "$other_file" ]; then | |
| echo "### Other Changes" >> new_entry.md | |
| echo "" >> new_entry.md | |
| cat "$other_file" >> new_entry.md | |
| echo "" >> new_entry.md | |
| fi | |
| # Create or update the complete changelog | |
| { | |
| echo "# Changelog" | |
| echo "" | |
| echo "All notable changes to this project will be documented in this file." | |
| echo "" | |
| echo "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)," | |
| echo "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)." | |
| echo "" | |
| } > CHANGELOG.md | |
| # Add the new entry | |
| cat new_entry.md >> CHANGELOG.md | |
| # Preserve existing changelog entries if they exist | |
| if git show HEAD:CHANGELOG.md >/dev/null 2>&1; then | |
| # Get existing entries (skip header and current version if it exists) | |
| git show HEAD:CHANGELOG.md | sed -n '/^## \[/,$p' | grep -v "^## \[${{ steps.version.outputs.new_version }}\]" >> CHANGELOG.md 2>/dev/null || true | |
| fi | |
| # Add version links at the bottom | |
| echo "" >> CHANGELOG.md | |
| # Get all tags for version links | |
| all_tags=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true) | |
| if [ -n "$all_tags" ]; then | |
| prev_tag="" | |
| echo "$all_tags" | while IFS= read -r tag; do | |
| version="${tag#v}" | |
| if [ -z "$prev_tag" ]; then | |
| # First tag (latest) - compare with HEAD | |
| echo "[$version]: https://github.com/${{ github.repository }}/compare/$tag...HEAD" >> CHANGELOG.md | |
| else | |
| # Compare with previous tag | |
| echo "[$version]: https://github.com/${{ github.repository }}/compare/$tag...$prev_tag" >> CHANGELOG.md | |
| fi | |
| prev_tag="$tag" | |
| done | |
| # Add link for the oldest version | |
| if [ -n "$prev_tag" ]; then | |
| oldest_version="${prev_tag#v}" | |
| echo "[$oldest_version]: https://github.com/${{ github.repository }}/releases/tag/$prev_tag" >> CHANGELOG.md | |
| fi | |
| fi | |
| # Cleanup temporary files | |
| rm -f "$added_file" "$changed_file" "$fixed_file" "$security_file" "$deprecated_file" "$removed_file" "$other_file" new_entry.md | |
| echo "CHANGELOG.md generated successfully" | |
| - name: Commit and push CHANGELOG.md | |
| run: | | |
| git add CHANGELOG.md | |
| git commit -m "docs: update CHANGELOG.md for ${{ steps.version.outputs.new_version }}" | |
| git push | |
| - name: Create GitHub Release | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: ${{ steps.version.outputs.new_version }} | |
| name: ${{ steps.version.outputs.new_version }} | |
| body: ${{ steps.release_notes.outputs.content }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Process and close issues | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_NOTES: ${{ steps.release_notes.outputs.content }} | |
| RELEASE_VERSION: ${{ steps.version.outputs.new_version }} | |
| run: | | |
| #!/bin/bash | |
| set -euo pipefail | |
| # Extract issue numbers from release notes (using basic grep instead of -P for compatibility) | |
| issue_numbers=$(echo "$RELEASE_NOTES" | grep -o '#[0-9]\+' | sed 's/#//' | sort -u || true) | |
| if [ -z "$issue_numbers" ]; then | |
| echo "No issues found in release notes to process." | |
| echo "This is normal for releases that don't reference specific issues." | |
| exit 0 | |
| fi | |
| echo "Found issues: $issue_numbers" | |
| for issue_number in $issue_numbers; do | |
| echo "Processing issue #$issue_number" | |
| # Check if issue exists and is open | |
| if ! gh issue view "$issue_number" --json state -q .state >/dev/null 2>&1; then | |
| echo "Issue #$issue_number does not exist, skipping." | |
| continue | |
| fi | |
| issue_state=$(gh issue view "$issue_number" --json state -q .state) | |
| if [ "$issue_state" != "OPEN" ]; then | |
| echo "Issue #$issue_number is not open, skipping." | |
| continue | |
| fi | |
| # Add label, comment and close issue | |
| gh issue edit "$issue_number" --add-label "released" || echo "Failed to add label to issue #$issue_number" | |
| gh issue comment "$issue_number" --body "🎉 This issue has been released in version $RELEASE_VERSION." || echo "Failed to comment on issue #$issue_number" | |
| gh issue close "$issue_number" || echo "Failed to close issue #$issue_number" | |
| echo "Processed issue #$issue_number" | |
| done | |
| echo "Issue processing completed." |