Skip to content

Commit

Permalink
feat(generate:releaseNotes): Add inline links to headings (#22415)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
tylerbutler and alexvy86 authored Sep 6, 2024
1 parent 1cc829d commit 1a4b95f
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 41 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/data/release-notes-issue-intro.md

This file was deleted.

17 changes: 17 additions & 0 deletions .github/workflows/data/release-notes-issue-intro.njk
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 5 deletions .github/workflows/release-notes-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions build-tools/packages/build-cli/docs/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion build-tools/packages/build-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.`);

Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions build-tools/packages/build-cli/src/library/markdown.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
};
}
Loading

0 comments on commit 1a4b95f

Please sign in to comment.