feat: implement comprehensive GitHub workflow and project management … #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: Changelog Management | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| types: [closed] | |
| jobs: | |
| update-changelog: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || (github.event.pull_request.merged == true) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20.x" | |
| - name: Generate changelog | |
| id: changelog | |
| run: | | |
| # Get the latest tag | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$latest_tag" ]; then | |
| echo "No previous tags found, generating full changelog" | |
| commit_range="HEAD" | |
| else | |
| echo "Latest tag: $latest_tag" | |
| commit_range="$latest_tag..HEAD" | |
| fi | |
| # Generate changelog content | |
| echo "# Changelog" > CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| echo "All notable changes to this project will be documented in this file." >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| echo "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)," >> CHANGELOG_NEW.md | |
| echo "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)." >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| # Get current version from package.json | |
| current_version=$(node -p "require('./package.json').version") | |
| echo "## [Unreleased]" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| # Parse commits since last tag | |
| features="" | |
| fixes="" | |
| docs="" | |
| chores="" | |
| breaking="" | |
| while IFS= read -r commit; do | |
| if [[ $commit == feat* ]]; then | |
| features="$features\n- ${commit#feat*: }" | |
| elif [[ $commit == fix* ]]; then | |
| fixes="$fixes\n- ${commit#fix*: }" | |
| elif [[ $commit == docs* ]]; then | |
| docs="$docs\n- ${commit#docs*: }" | |
| elif [[ $commit == *BREAKING* ]] || [[ $commit == *breaking* ]]; then | |
| breaking="$breaking\n- $commit" | |
| else | |
| chores="$chores\n- $commit" | |
| fi | |
| done < <(git log --format="%s" $commit_range) | |
| # Add sections to changelog | |
| if [ -n "$breaking" ]; then | |
| echo "### ⚠️ BREAKING CHANGES" >> CHANGELOG_NEW.md | |
| echo -e "$breaking" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| fi | |
| if [ -n "$features" ]; then | |
| echo "### ✨ Features" >> CHANGELOG_NEW.md | |
| echo -e "$features" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| fi | |
| if [ -n "$fixes" ]; then | |
| echo "### 🐛 Bug Fixes" >> CHANGELOG_NEW.md | |
| echo -e "$fixes" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| fi | |
| if [ -n "$docs" ]; then | |
| echo "### 📚 Documentation" >> CHANGELOG_NEW.md | |
| echo -e "$docs" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| fi | |
| if [ -n "$chores" ]; then | |
| echo "### 🔧 Maintenance" >> CHANGELOG_NEW.md | |
| echo -e "$chores" >> CHANGELOG_NEW.md | |
| echo "" >> CHANGELOG_NEW.md | |
| fi | |
| # Append existing changelog if it exists | |
| if [ -f CHANGELOG.md ]; then | |
| # Skip the header from existing changelog | |
| tail -n +8 CHANGELOG.md >> CHANGELOG_NEW.md | |
| fi | |
| mv CHANGELOG_NEW.md CHANGELOG.md | |
| echo "changelog_updated=true" >> $GITHUB_OUTPUT | |
| - name: Commit changelog | |
| if: steps.changelog.outputs.changelog_updated == 'true' | |
| run: | | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| git add CHANGELOG.md | |
| if git diff --staged --quiet; then | |
| echo "No changes to commit" | |
| else | |
| git commit -m "docs: update changelog [skip ci]" | |
| git push | |
| fi | |
| prepare-release: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| needs: update-changelog | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20.x" | |
| - name: Check if release is needed | |
| id: release_check | |
| run: | | |
| # Get current version | |
| current_version=$(node -p "require('./package.json').version") | |
| # Get latest tag | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| latest_version=${latest_tag#v} | |
| echo "Current version: $current_version" | |
| echo "Latest tag version: $latest_version" | |
| if [ "$current_version" != "$latest_version" ]; then | |
| echo "Version bump detected: $latest_version -> $current_version" | |
| echo "needs_release=true" >> $GITHUB_OUTPUT | |
| echo "new_version=$current_version" >> $GITHUB_OUTPUT | |
| else | |
| echo "No version bump detected" | |
| echo "needs_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create release PR | |
| if: steps.release_check.outputs.needs_release == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const version = '${{ steps.release_check.outputs.new_version }}'; | |
| // Check if release PR already exists | |
| const existingPRs = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| head: `release/v${version}`, | |
| state: 'open' | |
| }); | |
| if (existingPRs.data.length > 0) { | |
| console.log('Release PR already exists'); | |
| return; | |
| } | |
| // Create release branch | |
| const mainBranch = await github.rest.repos.getBranch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| branch: 'main' | |
| }); | |
| await github.rest.git.createRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `refs/heads/release/v${version}`, | |
| sha: mainBranch.data.commit.sha | |
| }); | |
| // Create PR | |
| const pr = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `Release v${version}`, | |
| head: `release/v${version}`, | |
| base: 'main', | |
| body: `## Release v${version} | |
| This PR prepares the release for version ${version}. | |
| ### Pre-release Checklist | |
| - [ ] All tests pass | |
| - [ ] Documentation is up to date | |
| - [ ] Changelog is accurate | |
| - [ ] Version number is correct | |
| - [ ] Templates are tested | |
| - [ ] Security audit passes | |
| ### Release Notes | |
| See CHANGELOG.md for detailed changes. | |
| **This PR will trigger the release workflow when merged.**`, | |
| draft: false | |
| }); | |
| // Add labels | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.data.number, | |
| labels: ['release', 'automated'] | |
| }); | |
| console.log(`Created release PR #${pr.data.number}`); | |
| validate-release: | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') | |
| steps: | |
| - name: Extract version from branch | |
| id: version | |
| run: | | |
| branch_name="${{ github.head_ref }}" | |
| version=${branch_name#release/v} | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| - name: Create and push tag | |
| run: | | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" | |
| git push origin "v${{ steps.version.outputs.version }}" | |
| - name: Trigger release workflow | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'release.yml', | |
| ref: 'main' | |
| }); |