From 9ece3fa2a7ae737d14127f0b8a24fc0d8d1bd1e1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 12 Jan 2026 17:11:25 +0100 Subject: [PATCH 1/3] ci: Add CI enforcement to prevent manual CHANGELOG.md edits Add a new workflow that checks PRs for CHANGELOG.md modifications and fails unless the PR title starts with `meta(changelog):`. When a violation is detected, posts a comment explaining the automated changelog policy and what action to take. Fixes #3072 Co-Authored-By: Claude --- .github/workflows/changelog-enforcement.yml | 80 +++++++++++++++++++++ .github/workflows/ci.yml | 9 ++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/changelog-enforcement.yml diff --git a/.github/workflows/changelog-enforcement.yml b/.github/workflows/changelog-enforcement.yml new file mode 100644 index 0000000000..d430ee345a --- /dev/null +++ b/.github/workflows/changelog-enforcement.yml @@ -0,0 +1,80 @@ +name: Changelog Enforcement + +on: + workflow_call: + +jobs: + check-changelog: + name: Check Changelog Changes + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'pull_request' }} + + permissions: + contents: read + pull-requests: write + + steps: + - name: Check for CHANGELOG.md modifications + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0 + with: + script: | + const prNumber = context.payload.pull_request.number; + const prTitle = context.payload.pull_request.title; + const isChangelogPR = prTitle.startsWith('meta(changelog):'); + + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + per_page: 100 + }); + + const changelogModified = files.some(file => file.filename === 'CHANGELOG.md'); + + console.log(`PR Title: ${prTitle}`); + console.log(`Is Changelog PR: ${isChangelogPR}`); + console.log(`CHANGELOG.md Modified: ${changelogModified}`); + + if (changelogModified && !isChangelogPR) { + const commentBody = `## :warning: Manual Changelog Changes Detected + + This PR modifies \`CHANGELOG.md\`, but the PR title does not start with \`meta(changelog):\`. + + **Changelogs in this repository are generated automatically during releases.** Manual edits to \`CHANGELOG.md\` are not allowed in regular PRs. + + ### What to do: + + 1. **If this change is intentional**: Create a separate PR with a title starting with \`meta(changelog):\` to make changelog edits. + + 2. **If this was unintentional**: Remove the \`CHANGELOG.md\` changes from this PR.`; + + // Avoid posting duplicate comments on re-runs + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Manual Changelog Changes Detected') + ); + + if (!botComment) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody + }); + console.log('Posted comment explaining changelog policy'); + } else { + console.log('Comment already exists, skipping'); + } + + core.setFailed('CHANGELOG.md changes are not allowed unless PR title starts with "meta(changelog):"'); + } else if (changelogModified && isChangelogPR) { + console.log('CHANGELOG.md changes allowed in meta(changelog): PR'); + } else { + console.log('No CHANGELOG.md changes detected'); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccaa740611..e9246f2c65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,10 +37,14 @@ jobs: name: Test Swift uses: ./.github/workflows/swift-test.yml + changelog: + name: Changelog + uses: ./.github/workflows/changelog-enforcement.yml + required: name: Check required jobs runs-on: ubuntu-24.04 - needs: [lint, test, test_node, test_swift] + needs: [lint, test, test_node, test_swift, changelog] if: always() steps: - name: Check for failure @@ -48,7 +52,8 @@ jobs: needs.lint.result != 'success' || needs.test.result != 'success' || needs.test_node.result != 'success' || - needs.test_swift.result != 'success' + needs.test_swift.result != 'success' || + (needs.changelog.result != 'success' && needs.changelog.result != 'skipped') }} run: | echo "One or more jobs failed" From 43813cdc2fb8d2df33aaac71560382440aab40bd Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 26 Jan 2026 15:32:08 +0100 Subject: [PATCH 2/3] fix(ci): Add pagination for large PRs in changelog enforcement Use github.paginate() for both listFiles and listComments API calls to handle PRs with >100 changed files or >30 comments. Without pagination, CHANGELOG.md changes could go undetected in very large PRs, and duplicate warning comments could be posted on PRs with many existing comments. Co-Authored-By: Claude --- .github/workflows/changelog-enforcement.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changelog-enforcement.yml b/.github/workflows/changelog-enforcement.yml index d430ee345a..26028018df 100644 --- a/.github/workflows/changelog-enforcement.yml +++ b/.github/workflows/changelog-enforcement.yml @@ -22,7 +22,8 @@ jobs: const prTitle = context.payload.pull_request.title; const isChangelogPR = prTitle.startsWith('meta(changelog):'); - const { data: files } = await github.rest.pulls.listFiles({ + // Paginate through all files to handle PRs with >100 changed files + const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, @@ -49,10 +50,12 @@ jobs: 2. **If this was unintentional**: Remove the \`CHANGELOG.md\` changes from this PR.`; // Avoid posting duplicate comments on re-runs - const { data: comments } = await github.rest.issues.listComments({ + // Paginate through all comments to handle PRs with many comments + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, - issue_number: prNumber + issue_number: prNumber, + per_page: 100 }); const botComment = comments.find(comment => From 6f8d5ac5d8bf9b2e8a8367900f5199c4a577cc45 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 26 Jan 2026 15:36:27 +0100 Subject: [PATCH 3/3] fix(ci): Delete violation comment when changelog issue is resolved Add an HTML marker to identify the bot comment, and delete it when the PR no longer violates the changelog policy (either by updating the title to meta(changelog): or removing CHANGELOG.md changes). This prevents stale warning comments from persisting after fixes. Co-Authored-By: Claude --- .github/workflows/changelog-enforcement.yml | 47 +++++++++++++-------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/.github/workflows/changelog-enforcement.yml b/.github/workflows/changelog-enforcement.yml index 26028018df..15f9401496 100644 --- a/.github/workflows/changelog-enforcement.yml +++ b/.github/workflows/changelog-enforcement.yml @@ -36,8 +36,9 @@ jobs: console.log(`Is Changelog PR: ${isChangelogPR}`); console.log(`CHANGELOG.md Modified: ${changelogModified}`); - if (changelogModified && !isChangelogPR) { - const commentBody = `## :warning: Manual Changelog Changes Detected + const commentMarker = ''; + const commentBody = `${commentMarker} + ## :warning: Manual Changelog Changes Detected This PR modifies \`CHANGELOG.md\`, but the PR title does not start with \`meta(changelog):\`. @@ -49,20 +50,20 @@ jobs: 2. **If this was unintentional**: Remove the \`CHANGELOG.md\` changes from this PR.`; - // Avoid posting duplicate comments on re-runs - // Paginate through all comments to handle PRs with many comments - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100 - }); + // Paginate through all comments to handle PRs with many comments + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100 + }); - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Manual Changelog Changes Detected') - ); + const botComment = comments.find(comment => + comment.body.includes(commentMarker) + ); + if (changelogModified && !isChangelogPR) { + // Violation: post comment if not already present if (!botComment) { await github.rest.issues.createComment({ owner: context.repo.owner, @@ -76,8 +77,20 @@ jobs: } core.setFailed('CHANGELOG.md changes are not allowed unless PR title starts with "meta(changelog):"'); - } else if (changelogModified && isChangelogPR) { - console.log('CHANGELOG.md changes allowed in meta(changelog): PR'); } else { - console.log('No CHANGELOG.md changes detected'); + // No violation: delete comment if it exists from a previous run + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id + }); + console.log('Deleted outdated violation comment'); + } + + if (changelogModified && isChangelogPR) { + console.log('CHANGELOG.md changes allowed in meta(changelog): PR'); + } else { + console.log('No CHANGELOG.md changes detected'); + } }