diff --git a/.grenrc.cjs b/.grenrc.cjs index 1ca10b24..69fcf4d5 100644 --- a/.grenrc.cjs +++ b/.grenrc.cjs @@ -1,8 +1,11 @@ module.exports = { - dataSource: "commits", + dataSource: "prs", tags: "all", prefix: "v", + // 5.0.0 was released on the 7th, and the cutoff is exclusive. + frozenBefore: "2026-09-08", ignoreIssuesWith: ["duplicate", "wontfix", "invalid", "help wanted"], + ignoreCommitsWith: ["^Release \\d"], username: "cjbarth", repo: "github-release-notes", template: { @@ -11,5 +14,7 @@ module.exports = { groupBy: { "Enhancements:": ["enhancement", "internal"], "Bug Fixes:": ["bug"], + // Without a catch-all, a pull request whose labels match no group is dropped in silence. + "Other:": ["..."], }, }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f8d2de..49926b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ ## v4.3.0 (2025-04-17) -- [Release 4.3.0](https://github.com/cjbarth/github-release-notes/commit/10fffd5e0fcf16215d2c151f7926b76269467057) - @cjbarth - [Rework sorting to account for parents and date (#12)](https://github.com/cjbarth/github-release-notes/commit/ba1fc4bf49722950f9476facd725940b1150ac02) - @cjbarth - [Sort commits by parent, then date (#11)](https://github.com/cjbarth/github-release-notes/commit/b55a29a783e3c0f69299a0b45563c081e618a63e) - @cjbarth - [Update dependencies (#10)](https://github.com/cjbarth/github-release-notes/commit/84cc103cf23ca8244b80a024f0fc27c674983aa0) - @cjbarth @@ -20,7 +19,6 @@ ## v4.2.0 (2023-09-05) -- [Release 4.2.0](https://github.com/cjbarth/github-release-notes/commit/fa9e966b56872cee12980922be4e0ed94ae0eabb) - @cjbarth - [Format using prettier (#9)](https://github.com/cjbarth/github-release-notes/commit/b8fd1b6a01ef30ba0f2458a86ebd56991a911e44) - @cjbarth - [Update README (#8)](https://github.com/cjbarth/github-release-notes/commit/bb476de6b36057ef694b55002e3877b88f93b0ab) - @cjbarth - [Use application version number for head of changelog (#7)](https://github.com/cjbarth/github-release-notes/commit/aaae7e0b07fa2078dec4096074a061aaf40a0439) - @cjbarth @@ -30,7 +28,6 @@ ## v4.1.0 (2023-07-14) -- [Release 4.1.0](https://github.com/cjbarth/github-release-notes/commit/c09f024a189c2a3494c21a85cce9f04e4b986995) - @cjbarth - [Add basic workflow for PR testing before merge (#5)](https://github.com/cjbarth/github-release-notes/commit/346d5ff6d90bbcde5d3a6264e63d250168daeebb) - @cjbarth - [Adjust tests to account for default branch (#4)](https://github.com/cjbarth/github-release-notes/commit/fdc118304f58692165a95b31de8fed299479f3ae) - @cjbarth - [Use current branch as default branch (#3)](https://github.com/cjbarth/github-release-notes/commit/16b30918eec4a2e73e4164880904034f9a457d50) - @cjbarth @@ -40,7 +37,6 @@ ## v4.0.0 (2023-04-12) -- [Release 4.0.0](https://github.com/cjbarth/github-release-notes/commit/934e728b5e87e941a5f5f2bcef457e47e423f41b) - @cjbarth - [Build changelog as part of release process](https://github.com/cjbarth/github-release-notes/commit/3e87029e162fdaa1a7e542e906152e4da5ac0d7b) - @cjbarth - [Update to release-it@15](https://github.com/cjbarth/github-release-notes/commit/cb70224198557df28fb122766131bf84608dcfdc) - @cjbarth - [Update all semver-minor dependencies](https://github.com/cjbarth/github-release-notes/commit/a372f19c67bfec2993c3740f9e386771852ced73) - @cjbarth @@ -53,7 +49,6 @@ ## v3.0.1 (2023-04-11) -- [Release 3.0.1](https://github.com/cjbarth/github-release-notes/commit/9a1c586c10982040379c2c13a5b41822cc9c3bc0) - @cjbarth - [Ensure that PRs are sorted by merge date](https://github.com/cjbarth/github-release-notes/commit/18007c9a6e3a7c480ac325bb77ffbb81f1b0744f) - @cjbarth - [Ensure that PRs are sorted by merge date](https://github.com/cjbarth/github-release-notes/commit/6990df20ca9fa1277787ff60a50941d3fdfde7c2) - @cjbarth @@ -61,14 +56,12 @@ ## v3.0.0 (2022-10-13) -- [Release 3.0.0](https://github.com/cjbarth/github-release-notes/commit/e5b88259c599fbeed648464d4977ff7e0b314b40) - @cjbarth - [Stably sort commits](https://github.com/cjbarth/github-release-notes/commit/51ac5a1835cbb28ebecbf2913b6d8c3e52010dfc) - @cjbarth --- ## v2.1.0 (2022-10-12) -- [Release 2.1.0](https://github.com/cjbarth/github-release-notes/commit/ebfdcaf83493c8124a04764bbc224f7a56796025) - @cjbarth - [Update changelog](https://github.com/cjbarth/github-release-notes/commit/3cee2ddaa27be24cba1cb439f165b3ad03265b1d) - @cjbarth - [Adjust tests to support new features](https://github.com/cjbarth/github-release-notes/commit/4cd37e9163785570c59de913fb9ecca95f9ddeab) - @cjbarth - [Update changelog](https://github.com/cjbarth/github-release-notes/commit/c2d01f485bc725c0f5b98b142771591cc842b0f7) - @cjbarth @@ -78,7 +71,6 @@ ## v2.0.0 (2022-08-26) -- [Release 2.0.0](https://github.com/cjbarth/github-release-notes/commit/7024505d127242a55822a8cd76b36ea6ba4829f8) - @cjbarth - [Update changelog](https://github.com/cjbarth/github-release-notes/commit/2ba7ee5d15e69085234e3b112191a966ed32ad7b) - @cjbarth - [Add option to get commits since last tag; don't skip any commits](https://github.com/cjbarth/github-release-notes/commit/73ba41a7dd926bc9e65e379ae5916736fd78ad18) - @cjbarth - [Update tests for fork and new features](https://github.com/cjbarth/github-release-notes/commit/59f222ac706c5cf0c1737158552cd8482aeb7ea4) - @cjbarth @@ -92,7 +84,6 @@ ## v1.0.1 (2022-04-01) -- [Release 1.0.1](https://github.com/cjbarth/github-release-notes/commit/8bd16d19cbc26bd77a73983423a20556df88f519) - @cjbarth - [Fix broken build by rolling back eslint updates](https://github.com/cjbarth/github-release-notes/commit/0d0336f6d285f97c2003b6cfe0813542802b64d7) - @cjbarth - [Update README for new project name](https://github.com/cjbarth/github-release-notes/commit/dc1a6d5285d4921cdf3b63891ee69a7a413655d0) - @cjbarth - [Update release-it to auto-build](https://github.com/cjbarth/github-release-notes/commit/dbdcfd9a27a260d88952c2fc8319fcf2d3116367) - @cjbarth @@ -101,7 +92,6 @@ ## v1.0.0 (2022-04-01) -- [Release 1.0.0](https://github.com/cjbarth/github-release-notes/commit/25698b89794422434caf8667dbee156e4fa25275) - @cjbarth - [Update packages, semver-major](https://github.com/cjbarth/github-release-notes/commit/ec3c201f6bacc0d98da7046d6107b2deccfb2812) - @cjbarth - [Update packages, semver-minor](https://github.com/cjbarth/github-release-notes/commit/bd75ecfeb3d21eb3dcf81faf6a4595c4755a136e) - @cjbarth @@ -109,7 +99,6 @@ ## v0.18.0 (2021-09-25) -- [Release 0.18.0](https://github.com/cjbarth/github-release-notes/commit/2f4719d4a9997ae8d38db4cf1fe1e71183f35f59) - @cjbarth - [Update package.json](https://github.com/cjbarth/github-release-notes/commit/f2fea79abb3300381744faed0d11ef539ddf676c) - @cjbarth - [Add release-it](https://github.com/cjbarth/github-release-notes/commit/5f4b37dcf1c3bb7a02f5a69b29f989cae803757c) - @cjbarth - [Fix tests](https://github.com/cjbarth/github-release-notes/commit/ecceb7ca1d1e80a5ba88b4fbf9863dd673b05bde) - @cjbarth @@ -370,29 +359,19 @@ _No changelog for this release._ --- -## v0.3.3 (2017-03-14) - -_No changelog for this release._ +## v0.6.3 (2017-03-14) ---- +### Bug Fixes -## v0.5.0 (2017-03-14) +- [#48](https://github.com/github-tools/github-release-notes/issues/48) Fix multiple repo information -### Enhancements +--- -- [#20](https://github.com/github-tools/github-release-notes/issues/20) Specify which tag to build -- [#18](https://github.com/github-tools/github-release-notes/issues/18) Create global version of the module -- [#16](https://github.com/github-tools/github-release-notes/issues/16) Update the documentation -- [#14](https://github.com/github-tools/github-release-notes/issues/14) Add the chance to override the latest release body -- [#13](https://github.com/github-tools/github-release-notes/issues/13) Check the network -- [#11](https://github.com/github-tools/github-release-notes/issues/11) Add tests -- [#10](https://github.com/github-tools/github-release-notes/issues/10) Use the issues as data source -- [#9](https://github.com/github-tools/github-release-notes/issues/9) Get the information from the local git config -- [#7](https://github.com/github-tools/github-release-notes/issues/7) Add the possibility to create a CHANGELOG file +## v0.6.2 (2017-03-14) ### Bug Fixes -- [#15](https://github.com/github-tools/github-release-notes/issues/15) Manage the scenario where there is only one tag +- [#45](https://github.com/github-tools/github-release-notes/issues/45) Remove unused option user.name --- @@ -408,19 +387,40 @@ _No changelog for this release._ --- -## v0.6.2 (2017-03-14) +## v0.6.0 (2017-03-14) + +### Enhancements + +- [#32](https://github.com/github-tools/github-release-notes/issues/32) Unwrap github-api promises +- [#26](https://github.com/github-tools/github-release-notes/issues/26) Use external config file +- [#23](https://github.com/github-tools/github-release-notes/issues/23) Introduce templates for the issues +- [#19](https://github.com/github-tools/github-release-notes/issues/19) Add an "ignore label" flag +- [#12](https://github.com/github-tools/github-release-notes/issues/12) Add the chance to rebuild the history of release notes ### Bug Fixes -- [#45](https://github.com/github-tools/github-release-notes/issues/45) Remove unused option user.name +- [#29](https://github.com/github-tools/github-release-notes/issues/29) Remove escaping character on regex +- [#24](https://github.com/github-tools/github-release-notes/issues/24) The changelog action doesn't compile latest release --- -## v0.6.3 (2017-03-14) +## v0.5.0 (2017-03-14) + +### Enhancements + +- [#20](https://github.com/github-tools/github-release-notes/issues/20) Specify which tag to build +- [#18](https://github.com/github-tools/github-release-notes/issues/18) Create global version of the module +- [#16](https://github.com/github-tools/github-release-notes/issues/16) Update the documentation +- [#14](https://github.com/github-tools/github-release-notes/issues/14) Add the chance to override the latest release body +- [#13](https://github.com/github-tools/github-release-notes/issues/13) Check the network +- [#11](https://github.com/github-tools/github-release-notes/issues/11) Add tests +- [#10](https://github.com/github-tools/github-release-notes/issues/10) Use the issues as data source +- [#9](https://github.com/github-tools/github-release-notes/issues/9) Get the information from the local git config +- [#7](https://github.com/github-tools/github-release-notes/issues/7) Add the possibility to create a CHANGELOG file ### Bug Fixes -- [#48](https://github.com/github-tools/github-release-notes/issues/48) Fix multiple repo information +- [#15](https://github.com/github-tools/github-release-notes/issues/15) Manage the scenario where there is only one tag --- @@ -432,41 +432,30 @@ _No changelog for this release._ --- -## v0.6.0 (2017-03-14) - -### Enhancements - -- [#32](https://github.com/github-tools/github-release-notes/issues/32) Unwrap github-api promises -- [#26](https://github.com/github-tools/github-release-notes/issues/26) Use external config file -- [#23](https://github.com/github-tools/github-release-notes/issues/23) Introduce templates for the issues -- [#19](https://github.com/github-tools/github-release-notes/issues/19) Add an "ignore label" flag -- [#12](https://github.com/github-tools/github-release-notes/issues/12) Add the chance to rebuild the history of release notes - -### Bug Fixes +## v0.3.3 (2017-03-14) -- [#29](https://github.com/github-tools/github-release-notes/issues/29) Remove escaping character on regex -- [#24](https://github.com/github-tools/github-release-notes/issues/24) The changelog action doesn't compile latest release +_No changelog for this release._ --- -## v0.2.2 (2017-03-10) +## v0.3.2 (2017-03-10) _No changelog for this release._ --- -## v0.3.0 (2017-03-10) +## v0.3.1 (2017-03-10) _No changelog for this release._ --- -## v0.3.1 (2017-03-10) +## v0.3.0 (2017-03-10) _No changelog for this release._ --- -## v0.3.2 (2017-03-10) +## v0.2.2 (2017-03-10) _No changelog for this release._ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb3ccb5e..b6fb9774 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,7 @@ It's easier to review the PR, if you rebase your commits so that every commit re e.g. -``` +```text Closes #123 Here any further informations needed to the reviewer, e.g. run yarn before reviewing. diff --git a/README.md b/README.md index aff69ceb..c08d4db0 100644 --- a/README.md +++ b/README.md @@ -54,20 +54,22 @@ The process, [as explained here](https://help.github.com/articles/creating-relea Let `gren` take care of that for you. It automates this process and also writes release notes for you, creating something like this: -> ## v0.6.0 (14/03/2017) -> -> #### Framework Enhancements -> -> - [#32](https://github.com/github-tools/github-release-notes/issues/32) Unwrap github-api promises -> - [#26](https://github.com/github-tools/github-release-notes/issues/26) Use external config file -> - [#23](https://github.com/github-tools/github-release-notes/issues/23) Introduce templates for the issues -> - [#19](https://github.com/github-tools/github-release-notes/issues/19) Add an "ignore label" flag -> - [#12](https://github.com/github-tools/github-release-notes/issues/12) Add the chance to rebuild the history of release notes -> -> #### Bug Fixes -> -> - [#29](https://github.com/github-tools/github-release-notes/issues/29) Remove escaping character on regex -> - [#24](https://github.com/github-tools/github-release-notes/issues/24) The changelog action doesn't compile latest release +```markdown +## v0.6.0 (14/03/2017) + +#### Framework Enhancements + +- [#32](https://github.com/github-tools/github-release-notes/issues/32) Unwrap github-api promises +- [#26](https://github.com/github-tools/github-release-notes/issues/26) Use external config file +- [#23](https://github.com/github-tools/github-release-notes/issues/23) Introduce templates for the issues +- [#19](https://github.com/github-tools/github-release-notes/issues/19) Add an "ignore label" flag +- [#12](https://github.com/github-tools/github-release-notes/issues/12) Add the chance to rebuild the history of release notes + +#### Bug Fixes + +- [#29](https://github.com/github-tools/github-release-notes/issues/29) Remove escaping character on regex +- [#24](https://github.com/github-tools/github-release-notes/issues/24) The changelog action doesn't compile latest release +``` _(yes, this is one of_ 🤖 _'s actual releases)_ @@ -102,15 +104,17 @@ Even though it doesn't require a machine-readable commit, it is still better to The output then uses commit messages (title + description) to look something like: -> ## v0.9.0 (17/05/2017) -> -> - Filter milestones (#75) -> - Create milestones data-source option -> - Add documentation for the milestones option -> - Support GitHub enterprise (#73) -> - Support GitHub enterprise -> - Add api-url to options documentation -> - Update CHANGELOG.md +```markdown +## v0.9.0 (17/05/2017) + +- Filter milestones (#75) + - Create milestones data-source option + - Add documentation for the milestones option +- Support GitHub enterprise (#73) + - Support GitHub enterprise + - Add api-url to options documentation +- Update CHANGELOG.md +``` #### Help 🤖 to write wonderful stuff (commits) @@ -227,7 +231,7 @@ export GREN_GITHUB_TOKEN=your_token_here Show the internet that you use gren for automating your release notes -> [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github.com/cjbarth/github-release-notes) -``` +```markdown [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github.com/cjbarth/github-release-notes) ``` @@ -320,7 +324,7 @@ pick one explicitly. A config hosted remotely (via the `gren` field in If you need help to create the configuration file, you can run the following command and follow the instructions -``` +```shell gren init ``` diff --git a/lib/src/Gren.js b/lib/src/Gren.js index d3cf1615..787b742e 100644 --- a/lib/src/Gren.js +++ b/lib/src/Gren.js @@ -817,7 +817,8 @@ class Gren { return generate( { sha, - message: message.split("\n")[0], + // A stray space at either end would land inside the link text and read as [ text ]. + message: message.split("\n")[0].trim(), url: html_url, author: author && author.login, @@ -1004,7 +1005,7 @@ class Gren { name: this.options.prefix + range[0].name, release: range[0].name, published_at: range[0].date, - body: this._generateCommitsBody(range[2]) + "\n", + body: this._templateBody(this._generateCommitsBody(range[2]).split("\n").filter(Boolean)), })); loaded(`Commit ranges loaded: ${ranges.length}`); @@ -1693,7 +1694,10 @@ class Gren { totalPages = this._getLastPage(response.headers.link); page++; allCommits.push(...response.data); - } while (page < totalPages); + // page has already been moved on to the next one to ask for, so the last page GitHub + // reports is still to come. Stopping short of it drops the oldest commits on the branch, + // and with them every tag that points at one. + } while (page <= totalPages); return allCommits; } @@ -1862,6 +1866,12 @@ class Gren { const cutoff = existingSections && this._getFrozenCutoff(); const generated = selected.filter((release) => !(cutoff && Date.parse(release.date) < cutoff)); + const unreleased = selected.find(({ ref }) => ref === this.options.head); + + if (unreleased && !(cutoff && Date.parse(unreleased.date) < cutoff)) { + await this._validateHeadIsPushed(this.options.head); + } + // A shallow clone has every tag but not the commits between them, which would leave // pull requests out of the release notes without any way to tell. if (generated.length && git.isShallow()) { @@ -2068,6 +2078,39 @@ class Gren { return releases.concat(latest ? [latest] : []); } + /** + * Check the branch being released is where GitHub has it + * + * Tags are checked against GitHub, and the commits of a release are read from the local + * repository, so a branch that has not been pushed would put work in the release notes that + * nobody else can see. + * + * @private + * + * @param {string} branch + */ + async _validateHeadIsPushed(branch) { + let remote; + + try { + const { data } = await this.octokit.rest.repos.getBranch({ ...this.repoParams, branch }); + + remote = data.commit.sha; + } catch (error) { + throw chalk.red( + `\nCould not read the branch "${branch}" from GitHub: ${error.message}. ` + + "The release notes are written for the branch as it is there.", + ); + } + + if (remote !== git.commitSha(branch)) { + throw chalk.red( + `\nThe branch "${branch}" is at a different commit here than on GitHub. ` + + "Push it, or fetch, so that the release notes describe what everyone else sees.", + ); + } + } + /** * Get the date before which releases are frozen * diff --git a/lib/src/_git.js b/lib/src/_git.js index fcb55772..0d7b1a53 100644 --- a/lib/src/_git.js +++ b/lib/src/_git.js @@ -144,6 +144,17 @@ function isShallow() { * * @return {boolean} */ +/** + * Get the commit a ref points to + * + * @param {string} ref A tag, branch or SHA + * + * @return {string} The full SHA + */ +function commitSha(ref) { + return git(["rev-parse", `${ref}^{commit}`]); +} + function refExists(ref) { try { git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]); @@ -161,4 +172,5 @@ export { commitDate, isShallow, refExists, + commitSha, }; diff --git a/test/Gren.membership.spec.js b/test/Gren.membership.spec.js index 6e44cf2a..55e9f6b6 100644 --- a/test/Gren.membership.spec.js +++ b/test/Gren.membership.spec.js @@ -41,7 +41,7 @@ describe("Gren release membership", () => { * * @return {Gren} */ - const createGren = ({ remoteTags = ALL_TAGS, ...options } = {}) => { + const createGren = ({ remoteTags = ALL_TAGS, headSha, ...options } = {}) => { const gren = new Gren({ token: "test-token", username: "owner", @@ -63,6 +63,9 @@ describe("Gren release membership", () => { rest: { repos: { listReleases: async () => ({ headers: {}, data: [] }), + getBranch: async ({ branch }) => ({ + data: { commit: { sha: headSha ?? git.commitSha(branch) } }, + }), listTags: async () => ({ data: remoteTags.map((name) => ({ name, @@ -509,6 +512,30 @@ describe("Gren release membership", () => { } }); + it("Should stop when the branch is at a different commit than on GitHub", async () => { + const gren = createGren({ headSha: "0".repeat(40) }); + + try { + await gren._getReleaseBlocks(); + assert.fail("An unpushed branch should stop the changelog"); + } catch (error) { + assert.include(String(error), 'The branch "master" is at a different commit'); + } + }); + + it("Should not ask about the branch when the version is already tagged", async () => { + // Nothing is read from the branch, so it does not matter where GitHub has it. + const blocks = await createGren({ + headSha: "0".repeat(40), + version: "2.0.0", + })._getReleaseBlocks(); + + assert.deepEqual( + blocks.map(({ release }) => release), + ["v2.0.0", "v1.2.1", "v1.2.0", "v1.1.1", "v1.1.0", "v1.0.0"], + ); + }); + it("Should stop when the local repository has a truncated history", async () => { const shallow = fs.mkdtempSync(path.join(os.tmpdir(), "gren-shallow-")); diff --git a/test/Gren.pagination.spec.js b/test/Gren.pagination.spec.js new file mode 100644 index 00000000..4ebd0262 --- /dev/null +++ b/test/Gren.pagination.spec.js @@ -0,0 +1,62 @@ +import { assert } from "chai"; +import Gren from "../lib/src/Gren.js"; + +describe("Gren pagination", () => { + /** + * Create a Gren whose GitHub lists a branch's commits over the given number of pages + * + * @param {number} pages + * + * @return {Object} { gren, requested }, where requested collects the pages asked for + */ + const createGren = (pages) => { + const gren = new Gren({ + token: "test-token", + username: "owner", + repo: "current", + head: "master", + version: "1.0.0", + quiet: true, + }); + const requested = []; + + gren.octokit = { + rest: { + repos: { + listCommits: async ({ page }) => { + requested.push(page); + + return { + headers: + pages > 1 ? { link: `; rel="last"` } : {}, + data: [{ sha: `commit-on-page-${page}` }], + }; + }, + }, + }, + }; + + return { gren, requested }; + }; + + describe("_getAllCommitsForBranch", () => { + it("Should read every page GitHub reports", async () => { + const { gren, requested } = createGren(4); + const commits = await gren._getAllCommitsForBranch("master"); + + assert.deepEqual(requested, [1, 2, 3, 4], "Stopping early hides the oldest commits"); + assert.deepEqual( + commits.map(({ sha }) => sha), + ["commit-on-page-1", "commit-on-page-2", "commit-on-page-3", "commit-on-page-4"], + ); + }); + + it("Should read one page when that is all there is", async () => { + const { gren, requested } = createGren(1); + + await gren._getAllCommitsForBranch("master"); + + assert.deepEqual(requested, [1]); + }); + }); +});