name: Create Changelog Pull Request on: push: branches: ["main"] workflow_dispatch: jobs: update-changelog-pull-request: runs-on: runner-cluster-htl-set 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: | DATES=$(grep -E '^## [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: [], subCategories: obj.subCategories ?? ( obj.labels.includes("update script") ? [ { title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] }, { title: "✨ New Features", labels: ["feature"], notes: [] }, { title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] } ] : obj.labels.includes("maintenance") ? [ { title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] }, { title: "✨ New Features", labels: ["feature"], notes: [] }, { title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] }, { title: "📡 API", labels: ["api"], notes: [] }, { title: "Github", labels: ["github"], notes: [] } ] : obj.labels.includes("website") ? [ { title: "🐞 Bug Fixes", labels: ["bugfix"], notes: [] }, { title: "✨ New Features", labels: ["feature"], notes: [] }, { title: "💥 Breaking Changes", labels: ["breaking change"], notes: [] }, { title: "Script Information", labels: ["json"], 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 && !pr.labels.some(label => ["invalid", "wontdo", process.env.AUTOMATED_PR_LABEL].includes(label.name.toLowerCase()) ) ).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}))`; const updateScriptsCategory = categorizedPRs.find(category => category.labels.some(label => prLabels.includes(label)) ); if (updateScriptsCategory) { const subCategory = updateScriptsCategory.subCategories.find(sub => sub.labels.some(label => prLabels.includes(label)) ); if (subCategory) { subCategory.notes.push(prNote); } else { updateScriptsCategory.notes.push(prNote); } } }); console.log(JSON.stringify(categorizedPRs, null, 2)); 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 }}; console.log(JSON.stringify(categorizedPRs, null, 2)); let newReleaseNotes = `## ${today}\n\n`; for (const { title, notes, subCategories } of categorizedPRs) { const hasSubcategories = subCategories && subCategories.length > 0; const hasMainNotes = notes.length > 0; const hasSubNotes = hasSubcategories && subCategories.some(sub => sub.notes && sub.notes.length > 0); if (hasMainNotes || hasSubNotes) { newReleaseNotes += `### ${title}\n\n`; } if (hasMainNotes) { newReleaseNotes += ` ${notes.join("\n")}\n\n`; } if (hasSubcategories) { for (const { title: subTitle, notes: subNotes } of subCategories) { if (subNotes && subNotes.length > 0) { newReleaseNotes += ` - #### ${subTitle}\n\n`; newReleaseNotes += ` ${subNotes.join("\n ")}\n\n`; } } } } const changelogContent = await fs.readFile(changelogPath, 'utf-8'); const changelogIncludesTodaysReleaseNotes = changelogContent.includes(`\n## ${today}`); 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 for changes id: verify-diff run: | git diff --quiet . || echo "changed=true" >> $GITHUB_ENV - name: Commit and push changes if: env.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: env.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: env.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: env.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