Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,55 @@ jobs:
--repo-url "$GITHUB_REPOSITORY" \
--config-file release-please-config.json \
--manifest-file .release-please-manifest.json

# release-please lists the same change twice whenever a pull request was
# merged with a GitHub merge commit. The merge commit carries the pull
# request title in its body:
#
# Merge pull request #171 from strapi-community/feat/cache-dashboard
#
# feat: add a cache statistics endpoint for the admin dashboard
#
# and release-please parses every conventional commit it finds in a
# message, so that body line becomes an entry attributed to the merge
# commit, while the commit on the branch underneath produces an identical
# one. There is no setting for it: BEGIN_COMMIT_OVERRIDE is keyed on the
# pull request, so it would suppress both copies, and `changelog-type:
# github` discards the conventional-commit sections.
#
# This runs on every release rather than being hand-fixed once, because
# release-please regenerates both the branch and the notes on every run.
# It is a no-op when nothing is duplicated, which is the normal case now
# that pull requests are squash-merged.
- name: Deduplicate the release changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BRANCH: release-please--branches--main
run: |
set -euo pipefail

# 1. The rolling release pull request.
if git ls-remote --exit-code --heads origin "$RELEASE_BRANCH" >/dev/null 2>&1; then
git fetch --quiet origin "$RELEASE_BRANCH"
git checkout --quiet "$RELEASE_BRANCH"
node scripts/dedupe-changelog.mjs CHANGELOG.md
if ! git diff --quiet -- CHANGELOG.md; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -q -m "chore: drop duplicate changelog entries" -- CHANGELOG.md
git push origin "$RELEASE_BRANCH"
fi
git checkout --quiet -
fi

# 2. Any draft release. Its notes are stored separately from
# CHANGELOG.md, so deduplicating one does not touch the other, and
# the draft is what a maintainer actually reads before pressing
# Publish.
notes=$(mktemp)
for tag in $(gh release list --limit 20 --json tagName,isDraft \
--jq '.[] | select(.isDraft) | .tagName'); do
gh release view "$tag" --json body --jq .body > "$notes"
node scripts/dedupe-changelog.mjs "$notes"
gh release edit "$tag" --notes-file "$notes"
done
107 changes: 107 additions & 0 deletions scripts/dedupe-changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env node

/**
* Removes duplicate entries from a generated changelog.
*
* GitHub's "Merge pull request" commits carry the pull request title in their
* body:
*
* Merge pull request #171 from strapi-community/feat/cache-dashboard
*
* feat: add a cache statistics endpoint for the admin dashboard
*
* release-please parses every conventional commit it finds in a message, so
* that body line becomes an entry attributed to the merge commit - and the
* commit on the branch underneath produces an identical one. The same change
* is listed twice under different shas. There is no config switch for it:
* BEGIN_COMMIT_OVERRIDE is keyed on the pull request, so it would suppress
* both, and `changelog-type: github` drops the conventional-commit sections.
*
* This is a one-off. Every pull request since is squash-merged, which produces
* a single commit and a single entry, and once this release lands
* last-release-sha sits past all 22 merge commits.
*
* Entries are matched on their text with the trailing ([sha](url)) link
* stripped, so the two shas do not defeat the comparison. The first occurrence
* wins, which keeps the original ordering.
*
* Usage:
* node scripts/dedupe-changelog.mjs CHANGELOG.md # rewrite in place
* node scripts/dedupe-changelog.mjs notes.md --stdout # print instead
* gh release view v5.1.0-beta --json body -q .body > n.md
* && node scripts/dedupe-changelog.mjs n.md --stdout
*/

import { readFileSync, writeFileSync } from 'node:fs';

const [, , file, ...flags] = process.argv;
if (!file) {
console.error('usage: dedupe-changelog.mjs <file> [--stdout]');
process.exit(1);
}

const toStdout = flags.includes('--stdout');
const input = readFileSync(file, 'utf8');

/**
* The text of an entry, with the trailing commit link removed.
*
* `* fix: a thing ([abc1234](https://github.com/o/r/commit/abc1234))`
* -> `fix: a thing`
*
* Issue links inside the text are deliberately kept: two entries differing
* only by which issue they cite are different entries.
*/
function entryKey(line) {
return line
.replace(/^\s*[*-]\s+/, '')
.replace(/\s*\(\[[0-9a-f]{7,40}\]\([^)]*\)\)\s*$/, '')
.trim()
.toLowerCase();
}

const isEntry = (line) => /^\s*[*-]\s+\S/.test(line);
// A heading starts a new scope: the same change legitimately appears under
// both Features and Bug Fixes in different releases, and this file may hold
// several releases.
const isHeading = (line) => /^#{1,6}\s/.test(line) || /^<details>/.test(line);

const lines = input.split('\n');
const out = [];
let seen = new Set();
let removed = 0;
const removedText = [];

for (const line of lines) {
if (isHeading(line)) {
seen = new Set();
out.push(line);
continue;
}

if (isEntry(line)) {
const key = entryKey(line);
if (key && seen.has(key)) {
removed += 1;
removedText.push(key);
continue;
}
seen.add(key);
}

out.push(line);
}

const result = out.join('\n');

if (toStdout) {
process.stdout.write(result);
} else {
writeFileSync(file, result);
}

const report = removed
? `removed ${removed} duplicate ${removed === 1 ? 'entry' : 'entries'}:\n` +
removedText.map((t) => ` - ${t}`).join('\n')
: 'no duplicates found';
console.error(report);
Loading