feat(generate:releaseNotes): Add inline links to headings (#22415)

GitHub does not create heading IDs in the Releases UI, so our release
note TOC links don't work. This change adds a remarkjs/unist plugin that
inserts HTML anchors into the headings.

Also updates the intro message on the release notes issue to include a
command that can be used to generate the release notes locally.


[AB#14174](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/14174)

---------

Co-authored-by: Alex Villarreal <716334+alexvy86@users.noreply.github.com>
This commit is contained in:
Tyler Butler 2024-09-06 14:24:00 -07:00 коммит произвёл GitHub
Родитель 1cc829db27
Коммит 1a4b95f7bf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 154 добавлений и 41 удалений

Просмотреть файл

@ -1,6 +0,0 @@
This issue is automatically updated with a preview of the release notes for the upcoming Fluid Framework release.
> [!NOTE]
> The table of contents links do not work in this preview, but they will work when the release notes are published.
---

17
.github/workflows/data/release-notes-issue-intro.njk поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
This issue is automatically updated with a preview of the release notes for the upcoming Fluid Framework release.
**To generate release notes locally to commit to the `RELEASE_NOTES` folder in the repo,** run the following command:
```shell
pnpm flub generate releaseNotes -g client -t minor --outFile RELEASE_NOTES/{{ version }}.md
```
**To generate the release notes to paste into the GitHub Release,** run the following command:
```shell
pnpm flub generate releaseNotes -g client -t minor --headingLinks --excludeH1 --outFile temporary-file.md
```
You can then copy and paste the contents of the `temporary-file.md` into the GitHub Release.
---

15
.github/workflows/release-notes-issue.yml поставляемый
Просмотреть файл

@ -68,7 +68,7 @@ jobs:
# be included.
- name: Generate release notes file
run: |
flub generate releaseNotes -g client -t minor --includeUnknown --out RELEASE_NOTES.md -v
flub generate releaseNotes -g client -t minor --includeUnknown --headingLinks --out RELEASE_NOTES.md -v
# Read the release notes file that we just generated into an output variable.
- name: Read release notes file
@ -78,18 +78,23 @@ jobs:
path: ./RELEASE_NOTES.md
# Read the issue intro from a data file and put it in an output variable.
- name: Read issue intro file
- name: Read issue intro template
id: intro
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # ratchet:juliangruber/read-file-action@v1
# release notes: https://github.com/Lehoczky/render-nunjucks-template-action/releases/tag/v1.0.0
uses: Lehoczky/render-nunjucks-template-action@9e23a64f080194d15347e881438ee53201e25c25 # ratchet:Lehoczky/render-nunjucks-template-action@v1.0.0
with:
path: ${{ github.workspace }}/.github/workflows/data/release-notes-issue-intro.md
template-path: ${{ github.workspace }}/.github/workflows/data/release-notes-issue-intro.njk
vars: |
{
"version": "${{ steps.setVersion.outputs.VERSION }}"
}
# Replace the issue body with the intro; we'll append the new release notes in the next step.
- name: Replace issue body with intro
uses: julien-deramond/update-issue-body@a7fae45395cac5a23318d38f7b09a58650dfe84f # ratchet:julien-deramond/update-issue-body@v1
with:
issue-number: ${{ env.ISSUE }}
body: ${{ steps.intro.outputs.content }}
body: ${{ steps.intro.outputs.result }}
edit-mode: replace
# Append the release notes we generated to the issue body.

Просмотреть файл

@ -319,17 +319,22 @@ Generates release notes from individual changeset files.
```
USAGE
$ flub generate releaseNotes -g client|server|azure|build-tools|gitrest|historian -t major|minor --out <value> [--json]
[-v | --quiet] [--includeUnknown]
$ flub generate releaseNotes -g client|server|azure|build-tools|gitrest|historian -t major|minor --outFile <value>
[--json] [-v | --quiet] [--includeUnknown] [--headingLinks] [--excludeH1]
FLAGS
-g, --releaseGroup=<option> (required) Name of a release group.
<options: client|server|azure|build-tools|gitrest|historian>
-t, --releaseType=<option> (required) The type of release for which the release notes are being generated.
<options: major|minor>
--excludeH1 Pass this flag to omit the top H1 heading. This is useful when the Markdown output will
be used as part of another document.
--headingLinks Pass this flag to output HTML anchor anchor tags inline for every heading. This is useful
when the Markdown output will be used in places like GitHub Releases, where headings
don't automatically get links.
--includeUnknown Pass this flag to include changesets in unknown sections in the generated release notes.
By default, these are excluded.
--out=<value> (required) [default: RELEASE_NOTES.md] Output the results to this file.
--outFile=<value> (required) [default: RELEASE_NOTES.md] Output the results to this file.
LOGGING FLAGS
-v, --verbose Enable verbose logging.

Просмотреть файл

@ -102,6 +102,7 @@
"execa": "^5.1.1",
"fflate": "^0.8.2",
"fs-extra": "^11.2.0",
"github-slugger": "^2.0.0",
"globby": "^11.1.0",
"gray-matter": "^4.0.3",
"human-id": "^4.0.0",
@ -110,6 +111,7 @@
"json5": "^2.2.3",
"jssm": "5.98.2",
"latest-version": "^5.1.0",
"mdast": "^3.0.0",
"minimatch": "^7.4.6",
"node-fetch": "^3.3.2",
"npm-check-updates": "^16.14.20",
@ -132,7 +134,8 @@
"strip-ansi": "^6.0.1",
"table": "^6.8.1",
"ts-morph": "^22.0.0",
"type-fest": "^2.19.0"
"type-fest": "^2.19.0",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@biomejs/biome": "~1.8.3",
@ -146,6 +149,7 @@
"@types/fs-extra": "^11.0.4",
"@types/inquirer": "^8.2.6",
"@types/issue-parser": "^3.0.5",
"@types/mdast": "^4.0.4",
"@types/mocha": "^9.1.1",
"@types/node": "^18.18.6",
"@types/node-fetch": "^2.5.10",
@ -154,6 +158,7 @@
"@types/semver": "^7.5.0",
"@types/semver-utils": "^1.1.1",
"@types/sort-json": "^2.0.1",
"@types/unist": "^3.0.3",
"c8": "^7.14.0",
"chai": "^4.3.7",
"chai-arrays": "^2.2.0",

Просмотреть файл

@ -25,6 +25,8 @@ import {
groupBySection,
loadChangesets,
} from "../../library/index.js";
// eslint-disable-next-line import/no-internal-modules
import { remarkHeadingLinks } from "../../library/markdown.js";
/**
* Generates release notes from individual changeset files.
@ -54,16 +56,31 @@ export default class GenerateReleaseNotesCommand extends BaseCommand<
throw new Error(`Invalid release type: ${input}`);
},
})(),
out: Flags.file({
outFile: Flags.file({
description: `Output the results to this file.`,
required: true,
default: "RELEASE_NOTES.md",
deprecateAliases: true,
aliases: [
// Can be removed in 0.46+
"out",
],
}),
includeUnknown: Flags.boolean({
default: false,
description:
"Pass this flag to include changesets in unknown sections in the generated release notes. By default, these are excluded.",
}),
headingLinks: Flags.boolean({
default: false,
description:
"Pass this flag to output HTML anchor anchor tags inline for every heading. This is useful when the Markdown output will be used in places like GitHub Releases, where headings don't automatically get links.",
}),
excludeH1: Flags.boolean({
default: false,
description:
"Pass this flag to omit the top H1 heading. This is useful when the Markdown output will be used as part of another document.",
}),
...BaseCommand.flags,
} as const;
@ -105,7 +122,9 @@ export default class GenerateReleaseNotesCommand extends BaseCommand<
[Discussion](https://github.com/microsoft/FluidFramework/discussions) and
[Issue](https://github.com/microsoft/FluidFramework/issues) pages as you adopt Fluid Framework!
`;
const intro = `# Fluid Framework v${version}\n\n## Contents`;
const intro = flags.excludeH1
? "## Contents"
: `# Fluid Framework v${version}\n\n## Contents`;
this.info(`Loaded ${changesets.length} changes.`);
@ -186,21 +205,26 @@ export default class GenerateReleaseNotesCommand extends BaseCommand<
}
}
const baseProcessor = remark()
.use(remarkGfm)
.use(admonitions)
.use(remarkGithub, {
buildUrl(values) {
// Disable linking mentions
return values.type === "mention" ? false : defaultBuildUrl(values);
},
})
.use(remarkToc, { maxDepth: 3, skip: ".*Start Building Today.*" });
const processor = flags.headingLinks
? baseProcessor.use(remarkHeadingLinks)
: baseProcessor;
const contents = String(
await remark()
.use(remarkGfm)
.use(admonitions)
.use(remarkGithub, {
buildUrl(values) {
// Disable linking mentions
return values.type === "mention" ? false : defaultBuildUrl(values);
},
})
.use(remarkToc, { maxDepth: 3, skip: ".*Start Building Today.*" })
.process(`${header}\n\n${intro}\n\n${body.toString()}\n\n${footer}`),
await processor.process(`${header}\n\n${intro}\n\n${body.toString()}\n\n${footer}`),
);
const outputPath = path.join(context.repo.resolvedRoot, flags.out);
const outputPath = path.join(context.repo.resolvedRoot, flags.outFile);
this.info(`Writing output file: ${outputPath}`);
await writeFile(
outputPath,

Просмотреть файл

@ -0,0 +1,43 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import GithubSlugger from "github-slugger";
import type { Heading, Html } from "mdast";
import type { Node } from "unist";
import { visit } from "unist-util-visit";
/**
* Using the same instance for all slug generation ensures that no duplicate IDs are generated.
*/
const slugger = new GithubSlugger();
/**
* A remarkjs/unist plugin that inserts HTML anchor nodes before heading text. This is a workaround for GitHub's lack of
* automatic heading links in GitHub Releases. GitHub's markdown rendering is inconsistent, and in this case it does not
* add automatic links.
*
* For more details, see: https://github.com/orgs/community/discussions/48311#discussioncomment-10436184
*/
export function remarkHeadingLinks(): (tree: Node) => void {
return (tree: Node): void => {
visit(tree, "heading", (node: Heading) => {
if (node.children?.length > 0) {
const firstChild = node.children[0];
if (firstChild.type === "text") {
const headingValue = firstChild.value;
const slug = slugger.slug(headingValue);
// We need to insert an Html node instead of a string, because raw
// strings will get markdown-escaped when rendered
const htmlNode: Html = {
type: "html",
value: `<a id="${slug}"></a>`,
};
// Insert the HTML node before the text node of the heading
node.children.unshift(htmlNode);
}
}
});
};
}

Просмотреть файл

@ -162,6 +162,9 @@ importers:
fs-extra:
specifier: ^11.2.0
version: 11.2.0
github-slugger:
specifier: ^2.0.0
version: 2.0.0
globby:
specifier: ^11.1.0
version: 11.1.0
@ -186,6 +189,9 @@ importers:
latest-version:
specifier: ^5.1.0
version: 5.1.0
mdast:
specifier: ^3.0.0
version: 3.0.0
minimatch:
specifier: ^7.4.6
version: 7.4.6
@ -255,6 +261,9 @@ importers:
type-fest:
specifier: ^2.19.0
version: 2.19.0
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
devDependencies:
'@biomejs/biome':
specifier: ~1.8.3
@ -289,6 +298,9 @@ importers:
'@types/issue-parser':
specifier: ^3.0.5
version: 3.0.5
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
'@types/mocha':
specifier: ^9.1.1
version: 9.1.1
@ -313,6 +325,9 @@ importers:
'@types/sort-json':
specifier: ^2.0.1
version: 2.0.1
'@types/unist':
specifier: ^3.0.3
version: 3.0.3
c8:
specifier: ^7.14.0
version: 7.14.0
@ -2607,7 +2622,7 @@ packages:
/@types/mdast@4.0.4:
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
/@types/minimatch@3.0.5:
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
@ -2724,8 +2739,8 @@ packages:
/@types/ungap__structured-clone@1.2.0:
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
/@types/unist@3.0.2:
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
/@types/unist@3.0.3:
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
/@types/wrap-ansi@3.0.0:
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
@ -7728,7 +7743,7 @@ packages:
resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==}
dependencies:
'@types/mdast': 4.0.4
'@types/unist': 3.0.2
'@types/unist': 3.0.3
decode-named-character-reference: 1.0.2
devlop: 1.1.0
mdast-util-to-string: 4.0.0
@ -7815,7 +7830,7 @@ packages:
resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==}
dependencies:
'@types/mdast': 4.0.4
'@types/unist': 3.0.2
'@types/unist': 3.0.3
longest-streak: 3.1.0
mdast-util-phrasing: 4.1.0
mdast-util-to-string: 4.0.0
@ -7839,6 +7854,11 @@ packages:
unist-util-is: 6.0.0
unist-util-visit: 5.0.0
/mdast@3.0.0:
resolution: {integrity: sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==}
deprecated: '`mdast` was renamed to `remark`'
dev: false
/memfs-or-file-map-to-github-branch@1.2.1:
resolution: {integrity: sha512-I/hQzJ2a/pCGR8fkSQ9l5Yx+FQ4e7X6blNHyWBm2ojeFLT3GVzGkTj7xnyWpdclrr7Nq4dmx3xrvu70m3ypzAQ==}
dependencies:
@ -10888,7 +10908,7 @@ packages:
/unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
bail: 2.0.2
devlop: 1.1.0
extend: 3.0.2
@ -10929,23 +10949,23 @@ packages:
/unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
/unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
/unist-util-visit-parents@6.0.1:
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
unist-util-is: 6.0.0
/unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
@ -11072,13 +11092,13 @@ packages:
/vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
unist-util-stringify-position: 4.0.0
/vfile@6.0.2:
resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==}
dependencies:
'@types/unist': 3.0.2
'@types/unist': 3.0.3
unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2