-
-
Notifications
You must be signed in to change notification settings - Fork 266
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
base: main
Are you sure you want to change the base?
Changes from all commits
d82cda1
959c828
1bcd40c
057789c
245dcb3
e98e8b0
cb7cb98
f20516b
1bcbfc9
04224ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }} | ||||
|
Member
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 seems to be unused
Suggested change
|
||||
| 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") | ||||
|
Member
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. I'm not following how this ref will help us find changelog updates. It looks like we're always getting the same diff we see in the PR here, so it would miss changes due to updates on the base branch. e.g. if I'm fixing a bug in the network-controller, then a release happens and moves that under a release header, we won't see that here. The
Member
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. It seems like what we want here is to compare the changelog in the PR with what it would be after merge. The "before" case here would be |
||||
| 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 | ||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| 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 | ||||
|
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 |
| 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 | ||
|
Member
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. Is this comment from an earlier draft when this was written in TypeScript? |
||
| // 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be unused