From 92915a230c6ae23ecbe95b8310970cae02c9c64d Mon Sep 17 00:00:00 2001 From: Nicolas Busseneau Date: Tue, 2 Jul 2024 19:38:30 +0200 Subject: [PATCH] add release-notes output Add a new `release-notes` output to the action containing the release notes for the newly released versions, allowing consumers to leverage it in their workflows (e.g. by passing it down to the GitHub Release API). --- CHANGELOG.md | 4 ++ .../empty_release/release-notes.expected.md | 0 .../first_release/release-notes.expected.md | 3 + .../release-notes.expected.md | 0 .../standard/release-notes.expected.md | 3 + .../tag_on_tag/release-notes.expected.md | 3 + .../tag_release/release-notes.expected.md | 3 + __tests__/getReleaseNotes.test.ts | 35 ++++++++++++ action.yml | 3 + src/getReleaseNotes.ts | 55 +++++++++++++++++++ src/index.ts | 6 +- 11 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 __tests__/fixtures/empty_release/release-notes.expected.md create mode 100644 __tests__/fixtures/first_release/release-notes.expected.md create mode 100644 __tests__/fixtures/lowercase_link_reference/release-notes.expected.md create mode 100644 __tests__/fixtures/standard/release-notes.expected.md create mode 100644 __tests__/fixtures/tag_on_tag/release-notes.expected.md create mode 100644 __tests__/fixtures/tag_release/release-notes.expected.md create mode 100644 __tests__/getReleaseNotes.test.ts create mode 100644 src/getReleaseNotes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dba8966..ec96ee80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a new `release-notes` output to the action containing the release notes for the newly released versions. + ## [3.0.0] - 2024-04-08 ### Fixed diff --git a/__tests__/fixtures/empty_release/release-notes.expected.md b/__tests__/fixtures/empty_release/release-notes.expected.md new file mode 100644 index 00000000..e69de29b diff --git a/__tests__/fixtures/first_release/release-notes.expected.md b/__tests__/fixtures/first_release/release-notes.expected.md new file mode 100644 index 00000000..346fdaac --- /dev/null +++ b/__tests__/fixtures/first_release/release-notes.expected.md @@ -0,0 +1,3 @@ +### Added + +- Everything since the beginning! \ No newline at end of file diff --git a/__tests__/fixtures/lowercase_link_reference/release-notes.expected.md b/__tests__/fixtures/lowercase_link_reference/release-notes.expected.md new file mode 100644 index 00000000..e69de29b diff --git a/__tests__/fixtures/standard/release-notes.expected.md b/__tests__/fixtures/standard/release-notes.expected.md new file mode 100644 index 00000000..aa2e99a7 --- /dev/null +++ b/__tests__/fixtures/standard/release-notes.expected.md @@ -0,0 +1,3 @@ +### Changed + +- Our main theme is now blue instead of red. \ No newline at end of file diff --git a/__tests__/fixtures/tag_on_tag/release-notes.expected.md b/__tests__/fixtures/tag_on_tag/release-notes.expected.md new file mode 100644 index 00000000..aa2e99a7 --- /dev/null +++ b/__tests__/fixtures/tag_on_tag/release-notes.expected.md @@ -0,0 +1,3 @@ +### Changed + +- Our main theme is now blue instead of red. \ No newline at end of file diff --git a/__tests__/fixtures/tag_release/release-notes.expected.md b/__tests__/fixtures/tag_release/release-notes.expected.md new file mode 100644 index 00000000..aa2e99a7 --- /dev/null +++ b/__tests__/fixtures/tag_release/release-notes.expected.md @@ -0,0 +1,3 @@ +### Changed + +- Our main theme is now blue instead of red. \ No newline at end of file diff --git a/__tests__/getReleaseNotes.test.ts b/__tests__/getReleaseNotes.test.ts new file mode 100644 index 00000000..297b38a9 --- /dev/null +++ b/__tests__/getReleaseNotes.test.ts @@ -0,0 +1,35 @@ +import getReleaseNotes from "../src/getReleaseNotes"; +import { read } from "to-vfile"; + +interface Fixture { + tag: string; + version: string; + date: string; + genesisHash: string; + owner: string; + repo: string; +} + +it.each(["empty_release", "standard", "first_release", "lowercase_link_reference", "tag_release", "tag_on_tag"])( + `should extract %s release-notes output`, + async function(testcase) { + const expectedChangelog = await read( + `./__tests__/fixtures/${testcase}/CHANGELOG.expected.md`, + { + encoding: "utf-8" + } + ); + const release: Fixture = await import( + `./fixtures/${testcase}/fixture` + ).then(module => module.default); + + const expectedReleaseNotes = await read( + `./__tests__/fixtures/${testcase}/release-notes.expected.md`, + { + encoding: "utf-8" + } + ).then(expected => expected.toString("utf-8")); + const actualReleaseNotes = getReleaseNotes(expectedChangelog, release.version); + expect(actualReleaseNotes).toEqual(expectedReleaseNotes); + } +); diff --git a/action.yml b/action.yml index e0a9f0f6..b6bab9ef 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: changelogPath: description: 'The path to the changelog file. Defaults to `./CHANGELOG.md`' required: false +outputs: + release-notes: + description: 'The release notes of the newly released version' runs: using: 'node20' main: 'dist/index.js' diff --git a/src/getReleaseNotes.ts b/src/getReleaseNotes.ts new file mode 100644 index 00000000..8e6f149d --- /dev/null +++ b/src/getReleaseNotes.ts @@ -0,0 +1,55 @@ +import unified, { Transformer } from "unified"; +import markdown from "remark-parse"; +import stringify from "remark-stringify"; +import { VFile } from "vfile"; +import { Node } from "unist"; +import { MarkdownRootNode } from "markdown-nodes"; + +function releaseNotesExtraction(version: string) { + return transformer as unknown as Transformer; + + function transformer(tree: MarkdownRootNode, _file: VFile) { + const children = tree.children; + + const firstNodeIndex = children.findIndex( + node => node.type === "heading" && node.depth === 2 && + node.children.length > 1 && node.children[0].type === "linkReference" && + node.children[0].identifier === version + ) + 1; + const firstNode = children.slice(firstNodeIndex); + + let lastNodeIndex = firstNode.findIndex( + node => node.type === "heading" && node.depth === 2 + ); + // special case: release notes for first release will not end with another + // section, instead they end with the compare URLs + if (lastNodeIndex === -1) { + lastNodeIndex = firstNode.findIndex( + node => node.type === "definition" && node.identifier === "unreleased" + ); + } + + const releaseNotesNodes = firstNode.slice(0, lastNodeIndex); + tree.children = releaseNotesNodes; + return tree as Node; + } +} + +export default function getReleaseNotes( + file: VFile, + version: string +): string { + // @ts-ignore + return unified() + .use(markdown) + .use(releaseNotesExtraction, version) + .data("settings", { + listItemIndent: "1", + tightDefinitions: true, + bullet: "-" + }) + .use(stringify) + .processSync(file) + .toString("utf-8") + .trim(); +} diff --git a/src/index.ts b/src/index.ts index 21fe63dd..4f169135 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ import { setFailed } from "@actions/core"; +import { setOutput } from "@actions/core/lib/core"; import { read, write } from "to-vfile"; import updateChangelog from "./updateChangelog"; import getInputs from "./getInputs"; import getGenesisHash from "./getGenesisHash"; +import getReleaseNotes from "./getReleaseNotes"; async function run(): Promise { try { @@ -20,8 +22,10 @@ async function run(): Promise { owner, repo ); - await write(newChangelog, { encoding: "utf-8" }); + + const releaseNotes = getReleaseNotes(newChangelog, version); + setOutput("release-notes", releaseNotes); } catch (error) { setFailed(error.message); }