Skip to content

Commit 0d79fe3

Browse files
authored
Add release-notes output (#28)
Closes: #27.
1 parent 77ac767 commit 0d79fe3

File tree

15 files changed

+189
-63
lines changed

15 files changed

+189
-63
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Use LF as default EOL marker
2+
* text=auto eol=lf

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add a new `release-notes` output to the action containing the release notes for the newly released versions.
13+
1014
## [3.0.0] - 2024-04-08
1115

1216
### Fixed

__tests__/fixtures/empty_release/release-notes.expected.md

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- Everything since the beginning!

__tests__/fixtures/lowercase_link_reference/release-notes.expected.md

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- Our main theme is now blue instead of red.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- Our main theme is now blue instead of red.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- Our main theme is now blue instead of red.

__tests__/getReleaseNotes.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import getReleaseNotes from "../src/getReleaseNotes";
2+
import { read } from "to-vfile";
3+
4+
interface Fixture {
5+
tag: string;
6+
version: string;
7+
date: string;
8+
genesisHash: string;
9+
owner: string;
10+
repo: string;
11+
}
12+
13+
it.each(["empty_release", "standard", "first_release", "lowercase_link_reference", "tag_release", "tag_on_tag"])(
14+
`should extract %s release-notes output`,
15+
async function(testcase) {
16+
const expectedChangelog = await read(
17+
`./__tests__/fixtures/${testcase}/CHANGELOG.expected.md`,
18+
{
19+
encoding: "utf-8"
20+
}
21+
);
22+
const release: Fixture = await import(
23+
`./fixtures/${testcase}/fixture`
24+
).then(module => module.default);
25+
26+
const expectedReleaseNotes = await read(
27+
`./__tests__/fixtures/${testcase}/release-notes.expected.md`,
28+
{
29+
encoding: "utf-8"
30+
}
31+
).then(expected => expected.toString("utf-8"));
32+
const actualReleaseNotes = getReleaseNotes(expectedChangelog, release.version);
33+
expect(actualReleaseNotes).toEqual(expectedReleaseNotes);
34+
}
35+
);

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ inputs:
1717
changelogPath:
1818
description: 'The path to the changelog file. Defaults to `./CHANGELOG.md`'
1919
required: false
20+
outputs:
21+
release-notes:
22+
description: 'The release notes of the newly released version'
2023
runs:
2124
using: 'node20'
2225
main: 'dist/index.js'

dist/index.js

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/getReleaseNotes.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import unified, { Transformer } from "unified";
2+
import markdown from "remark-parse";
3+
import stringify from "remark-stringify";
4+
import { VFile } from "vfile";
5+
import { Node } from "unist";
6+
import { MarkdownRootNode } from "markdown-nodes";
7+
8+
function releaseNotesExtraction(version: string) {
9+
return transformer as unknown as Transformer;
10+
11+
function transformer(tree: MarkdownRootNode, _file: VFile) {
12+
const children = tree.children;
13+
14+
const firstNodeIndex = children.findIndex(
15+
node => node.type === "heading" && node.depth === 2 &&
16+
node.children.length > 1 && node.children[0].type === "linkReference" &&
17+
node.children[0].identifier === version
18+
) + 1;
19+
const firstNode = children.slice(firstNodeIndex);
20+
21+
let lastNodeIndex = firstNode.findIndex(
22+
node => node.type === "heading" && node.depth === 2
23+
);
24+
// special case: release notes for first release will not end with another
25+
// section, instead they end with the compare URLs
26+
if (lastNodeIndex === -1) {
27+
lastNodeIndex = firstNode.findIndex(
28+
node => node.type === "definition" && node.identifier === "unreleased"
29+
);
30+
}
31+
32+
const releaseNotesNodes = firstNode.slice(0, lastNodeIndex);
33+
tree.children = releaseNotesNodes;
34+
return tree as Node;
35+
}
36+
}
37+
38+
export default function getReleaseNotes(
39+
file: VFile,
40+
version: string
41+
): string {
42+
// @ts-ignore
43+
return unified()
44+
.use(markdown)
45+
.use(releaseNotesExtraction, version)
46+
.data("settings", {
47+
listItemIndent: "1",
48+
tightDefinitions: true,
49+
bullet: "-"
50+
})
51+
.use(stringify)
52+
.processSync(file)
53+
.toString("utf-8")
54+
.trim();
55+
}

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { setFailed } from "@actions/core";
2+
import { setOutput } from "@actions/core/lib/core";
23
import { read, write } from "to-vfile";
34
import updateChangelog from "./updateChangelog";
45
import getInputs from "./getInputs";
56
import getGenesisHash from "./getGenesisHash";
7+
import getReleaseNotes from "./getReleaseNotes";
68

79
async function run(): Promise<void> {
810
try {
@@ -20,8 +22,10 @@ async function run(): Promise<void> {
2022
owner,
2123
repo
2224
);
23-
2425
await write(newChangelog, { encoding: "utf-8" });
26+
27+
const releaseNotes = getReleaseNotes(newChangelog, version);
28+
setOutput("release-notes", releaseNotes);
2529
} catch (error) {
2630
setFailed(error.message);
2731
}

src/updateChangelog.ts

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,7 @@ import markdown from "remark-parse";
33
import stringify from "remark-stringify";
44
import { VFile } from "vfile";
55
import { Node, Position } from "unist";
6-
7-
type MarkdownRootNode = {
8-
type: "root";
9-
children: MarkdownNode[];
10-
};
11-
12-
interface HeadingNode {
13-
type: "heading";
14-
depth: number;
15-
children: MarkdownNode[];
16-
position: Position;
17-
}
18-
19-
interface DefinitionNode {
20-
type: "definition";
21-
identifier: string;
22-
label: string;
23-
url: string;
24-
position?: Position;
25-
}
26-
27-
interface ListNode {
28-
type: "list";
29-
ordered: boolean;
30-
start: any;
31-
spread: boolean;
32-
url: string;
33-
children: object[];
34-
position: Position;
35-
}
36-
37-
interface ParagraphNode {
38-
type: "paragraph";
39-
children: object[];
40-
position: Position;
41-
}
42-
43-
interface LinkReferenceNode {
44-
type: "linkReference";
45-
identifier: string;
46-
label: string;
47-
referenceType: string;
48-
children: TextNode[];
49-
position?: Position;
50-
}
51-
52-
interface TextNode {
53-
type: "text";
54-
value: string;
55-
position?: Position;
56-
}
57-
58-
type MarkdownNode =
59-
| HeadingNode
60-
| DefinitionNode
61-
| ListNode
62-
| ParagraphNode
63-
| LinkReferenceNode
64-
| TextNode;
6+
import { MarkdownRootNode, HeadingNode, DefinitionNode, LinkReferenceNode, TextNode } from "markdown-nodes";
657

668
interface Options {
679
tag: string;

types/markdown-nodes/index.d.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
declare module "markdown-nodes" {
2+
import { Position } from "unist";
3+
4+
type MarkdownRootNode = {
5+
type: "root";
6+
children: MarkdownNode[];
7+
};
8+
9+
interface HeadingNode {
10+
type: "heading";
11+
depth: number;
12+
children: MarkdownNode[];
13+
position: Position;
14+
}
15+
16+
interface DefinitionNode {
17+
type: "definition";
18+
identifier: string;
19+
label: string;
20+
url: string;
21+
position?: Position;
22+
}
23+
24+
interface ListNode {
25+
type: "list";
26+
ordered: boolean;
27+
start: any;
28+
spread: boolean;
29+
url: string;
30+
children: object[];
31+
position: Position;
32+
}
33+
34+
interface ParagraphNode {
35+
type: "paragraph";
36+
children: object[];
37+
position: Position;
38+
}
39+
40+
interface LinkReferenceNode {
41+
type: "linkReference";
42+
identifier: string;
43+
label: string;
44+
referenceType: string;
45+
children: TextNode[];
46+
position?: Position;
47+
}
48+
49+
interface TextNode {
50+
type: "text";
51+
value: string;
52+
position?: Position;
53+
}
54+
55+
type MarkdownNode =
56+
| HeadingNode
57+
| DefinitionNode
58+
| ListNode
59+
| ParagraphNode
60+
| LinkReferenceNode
61+
| TextNode;
62+
}

0 commit comments

Comments
 (0)