name: Create Changelog Pull Request on: push: branches: ["main"] workflow_dispatch: jobs: update-changelog-pull-request: runs-on: ubuntu-latest env: CONFIG_PATH: .github/changelog-pr-config.json BRANCH_NAME: github-action-update-changelog AUTOMATED_PR_LABEL: "automated pr" permissions: contents: write pull-requests: write steps: - name: Generate a token id: generate-token uses: actions/create-github-app-token@v1 with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get latest dates in changelog run: | # Extract the latest and second latest dates from changelog DATES=$(grep '^## [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' CHANGELOG.md | head -n 2 | awk '{print $2}') LATEST_DATE=$(echo "$DATES" | sed -n '1p') SECOND_LATEST_DATE=$(echo "$DATES" | sed -n '2p') TODAY=$(date -u +%Y-%m-%d) echo "TODAY=$TODAY" >> $GITHUB_ENV if [ "$LATEST_DATE" == "$TODAY" ]; then echo "LATEST_DATE=$SECOND_LATEST_DATE" >> $GITHUB_ENV else echo "LATEST_DATE=$LATEST_DATE" >> $GITHUB_ENV fi - name: Get categorized pull requests id: get-categorized-prs uses: actions/github-script@v7 with: script: | const fs = require('fs').promises; const path = require('path'); const configPath = path.resolve(process.env.CONFIG_PATH); const fileContent = await fs.readFile(configPath, 'utf-8'); const changelogConfig = JSON.parse(fileContent); const categorizedPRs = changelogConfig.map((obj) => ({ ...obj, notes: [] })); const latestDateInChangelog = new Date(process.env.LATEST_DATE); latestDateInChangelog.setUTCHours(23,59,59,999); const { data: pulls } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, base: "main", state: "closed", sort: "updated", direction: "desc", per_page: 100, }); pulls.filter((pr) => pr.merged_at && new Date(pr.merged_at) > latestDateInChangelog ).forEach((pr) => { const prLabels = pr.labels.map((label) => label.name.toLowerCase()); const prNote = `- ${pr.title} [@${pr.user.login}](https://github.com/${pr.user.login}) ([#${pr.number}](${pr.html_url}))`; for (const { labels, notes } of categorizedPRs) { const prHasCategoryLabel = labels.some((label) => prLabels.includes(label)); const isUnlabelledCategory = labels.length === 0; const prShouldBeExcluded = prLabels.includes(process.env.AUTOMATED_PR_LABEL); if ((prHasCategoryLabel || isUnlabelledCategory) && !prShouldBeExcluded) { notes.push(prNote); break; } }; }); return categorizedPRs; - name: Update CHANGELOG.md uses: actions/github-script@v7 with: script: | const fs = require('fs').promises; const path = require('path'); const today = process.env.TODAY; const latestDateInChangelog = process.env.LATEST_DATE; const changelogPath = path.resolve('CHANGELOG.md'); const categorizedPRs = ${{ steps.get-categorized-prs.outputs.result }}; let newReleaseNotes = `## ${today}\n\n### Changed\n\n`; for (const { title, notes } of categorizedPRs) { if (notes.length > 0) { newReleaseNotes += `### ${title}\n\n${notes.join("\n")}\n\n`; } } const changelogContent = await fs.readFile(changelogPath, 'utf-8'); const changelogIncludesTodaysReleaseNotes = changelogContent.includes(`\n## ${today}`); // Replace todays release notes or insert release notes above previous release notes const regex = changelogIncludesTodaysReleaseNotes ? new RegExp(`## ${today}.*(?=## ${latestDateInChangelog})`, "gs") : new RegExp(`(?=## ${latestDateInChangelog})`, "gs"); const newChangelogContent = changelogContent.replace(regex, newReleaseNotes) await fs.writeFile(changelogPath, newChangelogContent); - name: Check if there are any changes id: verify-diff run: | git diff --quiet . || echo "changed=true" >> $GITHUB_OUTPUT - name: Commit and push changes to separate branch if: steps.verify-diff.outputs.changed == 'true' run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add CHANGELOG.md git commit -m "Update CHANGELOG.md" git checkout -b $BRANCH_NAME || git checkout $BRANCH_NAME git push origin $BRANCH_NAME --force - name: Create pull request if not exists if: steps.verify-diff.outputs.changed == 'true' env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | PR_EXISTS=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number') if [ -z "$PR_EXISTS" ]; then gh pr create --title "[Github Action] Update CHANGELOG.md" \ --body "This PR is auto-generated by a Github Action to update the CHANGELOG.md file." \ --head $BRANCH_NAME \ --base main \ --label "$AUTOMATED_PR_LABEL" fi - name: Approve pull request if: steps.verify-diff.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUMBER=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number') if [ -n "$PR_NUMBER" ]; then gh pr review $PR_NUMBER --approve fi - name: Re-approve pull request after update if: steps.verify-diff.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUMBER=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number') if [ -n "$PR_NUMBER" ]; then gh pr review $PR_NUMBER --approve fi