diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..64658b2 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,16 @@ +name: Prepare Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + pull-requests: write + +jobs: + call-prepare: + uses: leoweyr/github-release-workflow/.github/workflows/reusable-prepare-release.yml@develop + secrets: + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..472348f --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,14 @@ +name: Publish Release + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +jobs: + call-publish: + uses: leoweyr/github-release-workflow/.github/workflows/reusable-publish-release.yml@develop + secrets: + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-prepare-release.yml b/.github/workflows/reusable-prepare-release.yml new file mode 100644 index 0000000..3adbdc9 --- /dev/null +++ b/.github/workflows/reusable-prepare-release.yml @@ -0,0 +1,94 @@ +name: Prepare Release + +on: + workflow_call: + inputs: + node-verions: + description: 'Node.js version to use.' + required: false + type: string + default: '20' + commit-user-name: + description: 'Git user name for commit' + required: false + type: string + default: 'github-actions[bot]' + commit-user-email: + description: 'Git user email for commit.' + required: false + type: string + default: 'github-actions[bot]@users.noreply.github.com' + secrets: + ACCESS_TOKEN: + required: true + description: 'GitHub Token for authentication.' + +jobs: + prepare-release: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Checkout Config + uses: actions/checkout@v4 + with: + repository: 'leoweyr/github-release-workflow' + path: .change-log-config + sparse-checkout: src/cliff.toml + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-verions }} + + - name: Set Environment Variables + run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Create Release Branch + run: | + git config --global user.name "${{ inputs.commit-user-name }}" + git config --global user.email "${{ inputs.commit-user-email }}" + git checkout -b release/${{ env.TAG_NAME }} + + - name: Generate Changelog Content for PR Body + env: + GITHUB_REPO: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + npx git-cliff --config .change-log-config/src/cliff.toml --verbose --latest --strip all > pr_body_raw.md + + - name: Save PR Body to File + run: | + cat pr_body_raw.md | tail -n +2 > pr_body_cleaned.md + + - name: Update CHANGELOG.md File + env: + GITHUB_REPO: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + if [ -f "CHANGELOG.md" ]; then + # File exists: Prepend new changes (git-cliff intelligently handles headers). + npx git-cliff --config .change-log-config/src/cliff.toml --verbose --latest --prepend CHANGELOG.md + else + # File missing: Create new with full history and header. + npx git-cliff --config .change-log-config/src/cliff.toml --verbose --output CHANGELOG.md + fi + + - name: Commit and Push + run: | + git add CHANGELOG.md + git commit -m "release: ${{ env.TAG_NAME }}" + git push origin release/${{ env.TAG_NAME }} + + - name: Create Pull Request + env: + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + gh pr create \ + --title "release: ${{ env.TAG_NAME }}" \ + --body-file pr_body_cleaned.md \ + --base master \ + --head release/${{ env.TAG_NAME }} diff --git a/.github/workflows/reusable-publish-release.yml b/.github/workflows/reusable-publish-release.yml new file mode 100644 index 0000000..a49fb3d --- /dev/null +++ b/.github/workflows/reusable-publish-release.yml @@ -0,0 +1,41 @@ +name: Publish Release + +on: + workflow_call: + secrets: + ACCESS_TOKEN: + required: true + +jobs: + publish-release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.title, 'release:') + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract Tag Name + id: extract_tag + run: | + TITLE="${{ github.event.pull_request.title }}" + TAG_NAME=$(echo "$TITLE" | sed 's/release: //') + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV + VERSION_TITLE=${TAG_NAME#v} + echo "VERSION_TITLE=$VERSION_TITLE" >> $GITHUB_ENV + + - name: Create Release Body File + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: echo "$PR_BODY" > release_body.md + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + gh release create ${{ env.TAG_NAME }} \ + --title "${{ env.VERSION_TITLE }}" \ + --notes-file release_body.md \ + --verify-tag diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af3270b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +# [1.0.0] (2026-03-20) +### Bug Fixes + +* move reusable workflows back to .github/workflows directory ([0b0676e](https://github.com/leoweyr/github-release-workflow/commit/0b0676e413f95b39d33c79d64ffc0259a3783206)) [@leoweyr](https://github.com/leoweyr) +* add missing reusable release workflows ([456d0c9](https://github.com/leoweyr/github-release-workflow/commit/456d0c9348aad3d41e1d4b61aca8f7cd50194261)) [@leoweyr](https://github.com/leoweyr) +* rename reserved GITHUB_TOKEN secret in reusable workflows ([8d2afaa](https://github.com/leoweyr/github-release-workflow/commit/8d2afaa30e97c3c735bfe39b1d822879d834d4de)) [@leoweyr](https://github.com/leoweyr) +* correct git-cliff config path to prevent fallback to default ([7aef3e8](https://github.com/leoweyr/github-release-workflow/commit/7aef3e84c53bacded51601db6dcab4b9e765634e)) [@leoweyr](https://github.com/leoweyr) + + +### Features + +* add prepare release workflow ([4bec90f](https://github.com/leoweyr/github-release-workflow/commit/4bec90f7d44eda66efce0e48bec8079a5c98430c)) [@leoweyr](https://github.com/leoweyr) +* add publish release workflow ([20daca8](https://github.com/leoweyr/github-release-workflow/commit/20daca810acd33ee6071d7e5668fca95e203e797)) [@leoweyr](https://github.com/leoweyr) +* add user-facing workflows ([047bf99](https://github.com/leoweyr/github-release-workflow/commit/047bf996a00bb3fae423b17cb3d31a9392184b65)) [@leoweyr](https://github.com/leoweyr) +* add change log config ([4b667a0](https://github.com/leoweyr/github-release-workflow/commit/4b667a058ca7d14aa9960b7834262f5d7ee43a15)) [@leoweyr](https://github.com/leoweyr) + + +### Documentation + +* update readme with usage instructions ([7e15921](https://github.com/leoweyr/github-release-workflow/commit/7e15921622d59b01fa1f571a707ca4ffad7d5a94)) [@leoweyr](https://github.com/leoweyr) +* **readme:** add banner ([e81214e](https://github.com/leoweyr/github-release-workflow/commit/e81214ea480b1bbd1c63ea78cb7c4c32af085513)) [@leoweyr](https://github.com/leoweyr) + + +### Miscellaneous Tasks + +* move reusable workflows to src ([e48851c](https://github.com/leoweyr/github-release-workflow/commit/e48851ca6f5f92f075fae900cfc1332b7a507a20)) [@leoweyr](https://github.com/leoweyr) +* add icon ([04e730f](https://github.com/leoweyr/github-release-workflow/commit/04e730fc0a68c554c8b40f65ec949cdc4763ee73)) [@leoweyr](https://github.com/leoweyr) +* correct eye perspective in icon ([2ad0a73](https://github.com/leoweyr/github-release-workflow/commit/2ad0a7312fc8b75b7945679a71845ca68efbe43b)) [@leoweyr](https://github.com/leoweyr) + + + + diff --git a/README.md b/README.md index 80e9be2..9c376bd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,38 @@ -# GitHub Release Workflow +![github-release-workflow](https://socialify.git.ci/leoweyr/github-release-workflow/image?description=1&font=KoHo&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Fleoweyr%2Fgithub-release-workflow%2Frefs%2Fheads%2Fdevelop%2Fassets%2Ficon.svg&name=1&owner=1&pattern=Formal+Invitation&pulls=1&stargazers=1&theme=Light) -Streamline your software delivery with a production-ready engineering workflow. Automated semantic versioning, changelog generation (via git-cliff), and GitHub release publishing. +> [!IMPORTANT] +> To ensure changelogs are generated correctly, all git commit messages must follow the **[Conventional Commits](https://www.conventionalcommits.org/)** specification. +> +> Also, you must go to your repository **Settings > Actions > General > Workflow permissions** and enable **"Allow GitHub Actions to create and approve pull requests"**, otherwise the automated release process will fail. + +## 🚀 Instant Magic for Your Repository!!! + +Add professional release automation to your personal project with a single step: + +**Copy the `prepare-release.yml` and `publish-release.yml` files from `.github/workflows` into your project's `.github/workflows` directory.** + +✨ That's it! Your repository is now enchanted. + +## ⚙ How It Works + +This workflow streamlines your release process into a few simple steps: + +1. **Tag Your Release**: On your development branch (separate from `master` or `main`), create a git tag with a `v` prefix (e.g., `v1.0.0`). + ```bash + git tag v1.0.0 + ``` + +2. **Push the Tag**: Push the tag to GitHub. + ```bash + git push origin v1.0.0 + ``` + +3. **Automated Magic**: GitHub Actions will automatically: + * Generate a changelog based on your conventional commits. + * Create a specific release branch. + * Open a Pull Request to your default branch (e.g., `master`). + +4. **Review and Merge**: Review the Pull Request created by the bot. + * **Do not modify the Pull Request title or body**, as they are used for the release metadata. + * Merge the Pull Request. + * The workflow will automatically create a GitHub Release for you. diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..5facd38 --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cliff.toml b/src/cliff.toml new file mode 100644 index 0000000..976cbe6 --- /dev/null +++ b/src/cliff.toml @@ -0,0 +1,85 @@ +[changelog] +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" + +body = """ +{% if version %}\ + {% if previous.version %}\ + # [{{ version | trim_start_matches(pat="v") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/compare/{{ previous.version }}...{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }}) + {% else %}\ + # [{{ version | trim_start_matches(pat="v") }}] ({{ timestamp | date(format="%Y-%m-%d") }}) + {% endif %}\ +{% else %}\ + # [unreleased] +{% endif %}\ + +{% macro group_commits(group_name) %}\ + {% for group, commits in commits | group_by(attribute="group") %}\ + {% if group == group_name %}\ + ### {{ group | upper_first }} + + {% for commit in commits %}\ + * {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }})) \ + [@{{ commit.author.name }}](https://github.com/{{ commit.author.name }}) + {% endfor %} + + {% endif %}\ + {% endfor %}\ +{% endmacro %}\ + +{{ self::group_commits(group_name="Bug Fixes") }}\ +{{ self::group_commits(group_name="Features") }}\ +{{ self::group_commits(group_name="Performance") }}\ +{{ self::group_commits(group_name="Refactor") }}\ +{{ self::group_commits(group_name="Testing") }}\ +{{ self::group_commits(group_name="DevOps") }}\ +{{ self::group_commits(group_name="Documentation") }}\ +{{ self::group_commits(group_name="Styling") }}\ +{{ self::group_commits(group_name="Miscellaneous Tasks") }}\ +{{ self::group_commits(group_name="Security") }}\ +{{ self::group_commits(group_name="Revert") }}\ +\n +""" + +footer = """ + +""" + +trim = true +postprocessors = [] + + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_preprocessors = [ + { pattern = '^Revert "(.+)"$', replace = "revert $1" }, +] + +commit_parsers = [ + { message = "^fix", group = "Bug Fixes" }, + { message = "^feat", group = "Features" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "DevOps" }, + { message = "^doc", group = "Documentation" }, + { message = "^style", group = "Styling" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore", group = "Miscellaneous Tasks" }, + { body = ".*security", group = "Security" }, + { message = "^[Rr]evert", group = "Revert" }, +] + +protect_breaking_commits = false +filter_commits = false +topo_order = false +sort_commits = "oldest"