-
-
Notifications
You must be signed in to change notification settings - Fork 261
ci: Add action to validate changelog diffs after merging #7525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Mrtenz
wants to merge
10
commits into
main
Choose a base branch
from
mrtenz/validate-changelog-diffs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+216
−0
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d82cda1
Add action to validate changelog diffs after merging
Mrtenz 959c828
Fix getting PR number for pull_request trigger
Mrtenz 1bcd40c
Fix logic
Mrtenz 057789c
Fix logic again
Mrtenz 245dcb3
Use `context` instead of `github`
Mrtenz e98e8b0
Add missing `GH_TOKEN`
Mrtenz cb7cb98
Fix command
Mrtenz f20516b
Clean up logic and fix issues
Mrtenz 1bcbfc9
Only check unreleased section for main and PR changelog
Mrtenz 04224ca
Check if file exists on target ref and fix diff algorithm
Mrtenz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
100 changes: 100 additions & 0 deletions
100
.github/actions/check-merge-queue-changelogs/action.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| name: Check merge queue changelogs | ||
| description: Check if the changelog was incorrectly merged in a merge queue | ||
| pull request. | ||
|
|
||
| inputs: | ||
| github-token: | ||
| description: The GitHub token to use for authentication. | ||
| required: false | ||
| default: ${{ github.token }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Get pull request number | ||
| id: pr-number | ||
| uses: actions/github-script@v8 | ||
| env: | ||
| HEAD_REF: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} | ||
| with: | ||
| github-token: ${{ inputs.github-token }} | ||
| script: | | ||
| const { EVENT_NAME, HEAD_REF } = process.env; | ||
|
|
||
| if (context.eventName === 'pull_request') { | ||
| const prNumber = context.payload.pull_request.number; | ||
| return core.setOutput('pr-number', prNumber); | ||
| } | ||
|
|
||
| const match = HEAD_REF.match(/\/pr-([0-9]+)-/u); | ||
| if (!match) { | ||
| return core.setFailed(`Could not extract pull request number from head ref: "${HEAD_REF}".`); | ||
| } | ||
|
|
||
| const number = parseInt(match[1], 10); | ||
| core.setOutput('pr-number', number); | ||
|
|
||
| - name: Get pull request branch | ||
| id: pr-branch | ||
| shell: bash | ||
| env: | ||
| REPOSITORY: ${{ github.repository }} | ||
| PR_NUMBER: ${{ steps.pr-number.outputs.pr-number }} | ||
| GH_TOKEN: ${{ inputs.github-token }} | ||
| run: | | ||
| BRANCH=$(gh api "/repos/${REPOSITORY}/pulls/${PR_NUMBER}" --jq=.head.ref) | ||
| echo "pr-branch=$BRANCH" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check changelog changes | ||
| id: changelog-check | ||
| shell: bash | ||
| env: | ||
| HEAD_REF: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} | ||
| BASE_REF: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} | ||
| PR_BRANCH: ${{ steps.pr-branch.outputs.pr-branch }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # Strip invalid prefix from `BASE_REF` | ||
| # It comes prefixed with `refs/heads/`, but the branch is not checked out in this context | ||
| # We need to express it as a remote branch | ||
| PREFIXED_REF_REGEX='refs/heads/(.+)' | ||
| if [[ "$BASE_REF" =~ $PREFIXED_REF_REGEX ]]; then | ||
| BASE_REF="${BASH_REMATCH[1]}" | ||
| fi | ||
|
|
||
| TARGET_REF=$(git merge-base "origin/$BASE_REF" "origin/$PR_BRANCH") | ||
| git fetch origin "$TARGET_REF" | ||
|
|
||
| UPDATED_CHANGELOGS=$(git diff --name-only "$TARGET_REF" "origin/$PR_BRANCH" | grep -E 'CHANGELOG\.md$' || true) | ||
| if [ -n "$UPDATED_CHANGELOGS" ]; then | ||
| for FILE in $UPDATED_CHANGELOGS; do | ||
| if [ ! -f "$FILE" ]; then | ||
| echo "Changelog file \"$FILE\" was deleted in this PR. Skipping." | ||
| continue | ||
| fi | ||
|
|
||
| if ! git cat-file -e "$TARGET_REF":"$FILE" 2>/dev/null; then | ||
| echo "Changelog file \"$FILE\" is new in this PR. Skipping." | ||
| continue | ||
| fi | ||
|
|
||
| echo "Checking changelog file: $FILE" | ||
| git show "$TARGET_REF":"$FILE" > /tmp/base-changelog.md | ||
| git show origin/"$PR_BRANCH":"$FILE" > /tmp/pr-changelog.md | ||
|
|
||
| node "${ACTION_PATH}/check-changelog-diff.cjs" \ | ||
| /tmp/base-changelog.md \ | ||
| /tmp/pr-changelog.md \ | ||
| "$FILE" | ||
| done | ||
| else | ||
| echo "No CHANGELOG.md files were modified in this PR." | ||
| fi | ||
105 changes: 105 additions & 0 deletions
105
.github/actions/check-merge-queue-changelogs/check-changelog-diff.cjs
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a plain JS file so we can just call |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| // This script checks that any new changelog entries added in a PR | ||
| // remain in the [Unreleased] section after the PR is merged. | ||
|
|
||
| const fs = require('fs'); | ||
|
|
||
| if (process.argv.length < 5) { | ||
| console.error( | ||
| 'Usage: tsx check-changelog-diff.mts <base-file> <pr-file> <merged-file>', | ||
| ); | ||
|
|
||
| // eslint-disable-next-line n/no-process-exit | ||
| process.exit(1); | ||
| } | ||
|
|
||
| /* eslint-disable n/no-sync */ | ||
| // The type of these is inferred as `Buffer` when using "utf-8" directly instead | ||
| // of an options object. Even though it's a plain JavaScript file, it's nice to | ||
| // keep the types correct. | ||
| const baseContent = fs.readFileSync(process.argv[2], { | ||
| encoding: 'utf-8', | ||
| }); | ||
|
|
||
| const prContent = fs.readFileSync(process.argv[3], { | ||
| encoding: 'utf-8', | ||
| }); | ||
|
|
||
| const mergedContent = fs.readFileSync(process.argv[4], { | ||
| encoding: 'utf-8', | ||
| }); | ||
| /* eslint-enable n/no-sync */ | ||
|
|
||
| /** | ||
| * Extract the "[Unreleased]" section from the changelog content. | ||
| * | ||
| * This doesn't actually parse the Markdown, it just looks for the section | ||
| * header and collects lines until the next section header. | ||
| * | ||
| * @param {string} content - The changelog content. | ||
| * @returns {Set<string>} The lines in the "[Unreleased]" section as a | ||
| * {@link Set}. | ||
| */ | ||
| function getUnreleasedSection(content) { | ||
| const lines = content.split('\n'); | ||
|
|
||
| let inUnreleased = false; | ||
| const sectionLines = new Set(); | ||
|
|
||
| for (const line of lines) { | ||
| // Find unreleased header. | ||
| if (line.trim().match(/^##\s+\[Unreleased\]/u)) { | ||
| inUnreleased = true; | ||
| continue; | ||
| } | ||
|
|
||
| // Stop if we hit the next version header (## [x.x.x]). | ||
| if (inUnreleased && line.trim().match(/^##\s+\[/u)) { | ||
| break; | ||
| } | ||
|
|
||
| // If inside the unreleased header, add lines to the set. | ||
| if (inUnreleased) { | ||
| sectionLines.add(line.trim()); | ||
| } | ||
| } | ||
|
|
||
| return sectionLines; | ||
| } | ||
|
|
||
| /** | ||
| * Get the lines that were added in the PR content compared to the base content. | ||
| * | ||
| * @param {Set<string>} oldLines - The base changelog content. | ||
| * @param {Set<string>} newLines - The PR changelog content. | ||
| * @returns {string[]} The added lines as an array of strings. | ||
| */ | ||
| function getAddedLines(oldLines, newLines) { | ||
| return Array.from(newLines).filter( | ||
| (line) => | ||
| line.length > 0 && | ||
| !oldLines.has(line) && | ||
| !line.startsWith('#') && | ||
| !line.startsWith('['), | ||
| ); | ||
| } | ||
|
|
||
| const mergedUnreleased = getUnreleasedSection(mergedContent); | ||
| const addedLines = getAddedLines( | ||
| getUnreleasedSection(baseContent), | ||
| getUnreleasedSection(prContent), | ||
| ); | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const missingLines = []; | ||
| for (const line of addedLines) { | ||
| if (!mergedUnreleased.has(line)) { | ||
| missingLines.push(line); | ||
| } | ||
| } | ||
|
|
||
| if (missingLines.length > 0) { | ||
| console.error( | ||
| `The following lines added in the PR are missing from the "Unreleased" section after merge:\n\n ${missingLines.join('\n ')}\n\nPlease update your pull request and ensure that new changelog entries remain in the "Unreleased" section.`, | ||
| ); | ||
|
|
||
| process.exitCode = 1; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.