diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 46c491b3..04a81975 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,11 +41,12 @@ jobs: - name: Commit changes run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git remote set-url origin https://x-access-token:${{ secrets.GH_PAT }}@github.com/forntoh/LcdMenu.git git add . git commit -m "Update version to $UPDATED_VERSION" - git push origin master + git push origin master --force - name: Create Tag if: ${{ env.UPDATED_VERSION != '' }} @@ -66,6 +67,25 @@ jobs: git tag -d ${{ env.UPDATED_VERSION }} git push origin :refs/tags/${{ env.UPDATED_VERSION }} + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + + - name: Install @actions/github + run: npm install @actions/github + + - name: Generate Release Notes + id: generate_release_notes + uses: actions/github-script@v6 + env: + CURRENT_TAG: ${{ env.UPDATED_VERSION }} + with: + script: | + const generateReleaseNotes = require('.scripts/release_notes.js'); + const releaseNotes = await generateReleaseNotes(github, context); + core.setOutput('release_notes', releaseNotes); + - name: Create GitHub Release if: success() uses: actions/create-release@v1 @@ -74,5 +94,6 @@ jobs: with: tag_name: ${{ env.UPDATED_VERSION }} release_name: LcdMenu v${{ env.UPDATED_VERSION }} - draft: true - prerelease: false \ No newline at end of file + draft: false + prerelease: false + body: ${{ steps.generate_release_notes.outputs.result }} diff --git a/.scripts/release_notes.js b/.scripts/release_notes.js new file mode 100644 index 00000000..2ad6b3f9 --- /dev/null +++ b/.scripts/release_notes.js @@ -0,0 +1,123 @@ +function escapeSpecialChars(str) { + const specialChars = /[\\`*_{}\[\]()#+\-!:.]/g; + return str.replace(specialChars, "\\$&"); +} + +async function generateReleaseNotes(github, context) { + const { owner, repo } = context.repo; + const currentTag = process.env.CURRENT_TAG; + + // Fetch all tags + const { data: tags } = await github.rest.repos.listTags({ + owner, + repo, + per_page: 100, + }); + + // Find the previous tag + const currentTagIndex = tags.findIndex((tag) => tag.name === currentTag); + const previousTag = + currentTagIndex < tags.length - 1 ? tags[currentTagIndex + 1].name : null; + + console.log(`Current Tag: ${currentTag}`); + console.log(`Previous Tag: ${previousTag}`); + + const getCommitDate = async (github, owner, repo, sha) => { + const { data: commit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: sha, + }); + return new Date(commit.commit.committer.date); + }; + + const previousTagDate = previousTag + ? await getCommitDate( + github, + owner, + repo, + tags.find((tag) => tag.name === previousTag).commit.sha + ) + : new Date(0); + + const currentTagDate = await getCommitDate( + github, + owner, + repo, + tags.find((tag) => tag.name === currentTag).commit.sha + ); + + // Fetch PRs merged between previousTag and currentTag + const { data: pulls } = await github.rest.pulls.list({ + owner, + repo, + state: "closed", + sort: "updated", + direction: "desc", + per_page: 100, + }); + + const categoryNames = { + feature: "New Features", + enhancement: "Enhancements", + bugfix: "Bug Fixes", + chore: "Chore Updates", + documentation: "Documentation Updates", + }; + + const categories = { + feature: [], + enhancement: [], + bugfix: [], + chore: [], + documentation: [], + }; + + let hasBreakingChanges = false; + + pulls + .filter((pr) => pr.merged_at) + .filter((pr) => { + const mergedAt = new Date(pr.merged_at); + return mergedAt > previousTagDate && mergedAt <= currentTagDate; + }) + .forEach((pr) => { + const prEntry = `* ${escapeSpecialChars(pr.title)} by @${ + pr.user.login + } in ${pr.html_url}`; + + pr.labels.forEach((label) => { + if (label.name === "breaking-change") { + hasBreakingChanges = true; + } + if (categories[label.name]) { + categories[label.name].push(prEntry); + } else if (!categories[label.name] && label.name === pr.labels[pr.labels.length - 1].name) { + categories["chore"].push(prEntry); + } + }); + }); + + console.log(`Categories: ${JSON.stringify(categories, null, 2)}`); + + const releaseNotes = Object.entries(categories) + .filter(([_, notes]) => notes.length > 0) + .map( + ([category, notes]) => + `### ${ + categoryNames[category] || + category.charAt(0).toUpperCase() + category.slice(1) + }\n${notes.join("\n")}` + ) + .join("\n\n"); + + const breakingChangesSection = hasBreakingChanges + ? `### Breaking Changes\n\n- This release introduces breaking changes. Please review [the migration guide](https://lcdmenu.forntoh.dev/reference/migration/index.html) for details on how to update your code.\n\n` + : ""; + + const repoUrl = pulls.length > 0 ? pulls[0].base.repo.html_url : ""; + const fullChangelog = `**Full Changelog**: ${repoUrl}/compare/${previousTag}...${currentTag}`; + return `${releaseNotes}\n\n${breakingChangesSection}\n\n\n${fullChangelog}`; +} + +module.exports = generateReleaseNotes;