From 5e318b4cfac0163bafdb894119faf1eb7aabe6f0 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 7 May 2024 06:52:00 -0400 Subject: [PATCH] Refactor release notes handling of separate pages (#1305) This is a prefactor for the upcoming addition of the Cloud Transpiler. It does not yet have release notes, so we need to be able to disable the mechanism. --- docs/api/qiskit/release-notes/index.mdx | 86 +++++++-------- scripts/lib/api/Pkg.ts | 44 ++++---- scripts/lib/api/addFrontMatter.test.ts | 8 +- scripts/lib/api/addFrontMatter.ts | 15 +-- scripts/lib/api/conversionPipeline.test.ts | 2 - scripts/lib/api/conversionPipeline.ts | 62 +---------- scripts/lib/api/generateToc.test.ts | 9 +- scripts/lib/api/generateToc.ts | 8 +- scripts/lib/api/releaseNotes.ts | 116 ++++++++++++++------- 9 files changed, 170 insertions(+), 180 deletions(-) diff --git a/docs/api/qiskit/release-notes/index.mdx b/docs/api/qiskit/release-notes/index.mdx index ac686f8c927..3f3499e6e11 100644 --- a/docs/api/qiskit/release-notes/index.mdx +++ b/docs/api/qiskit/release-notes/index.mdx @@ -9,46 +9,46 @@ New features, bug fixes, and other changes in previous versions of Qiskit SDK. ## Release notes by version -* [1.0](/api/qiskit/release-notes/1.0) -* [0.46](/api/qiskit/release-notes/0.46) -* [0.45](/api/qiskit/release-notes/0.45) -* [0.44](/api/qiskit/release-notes/0.44) -* [0.43](/api/qiskit/release-notes/0.43) -* [0.42](/api/qiskit/release-notes/0.42) -* [0.41](/api/qiskit/release-notes/0.41) -* [0.40](/api/qiskit/release-notes/0.40) -* [0.39](/api/qiskit/release-notes/0.39) -* [0.38](/api/qiskit/release-notes/0.38) -* [0.37](/api/qiskit/release-notes/0.37) -* [0.36](/api/qiskit/release-notes/0.36) -* [0.35](/api/qiskit/release-notes/0.35) -* [0.34](/api/qiskit/release-notes/0.34) -* [0.33](/api/qiskit/release-notes/0.33) -* [0.32](/api/qiskit/release-notes/0.32) -* [0.31](/api/qiskit/release-notes/0.31) -* [0.30](/api/qiskit/release-notes/0.30) -* [0.29](/api/qiskit/release-notes/0.29) -* [0.28](/api/qiskit/release-notes/0.28) -* [0.27](/api/qiskit/release-notes/0.27) -* [0.26](/api/qiskit/release-notes/0.26) -* [0.25](/api/qiskit/release-notes/0.25) -* [0.24](/api/qiskit/release-notes/0.24) -* [0.23](/api/qiskit/release-notes/0.23) -* [0.22](/api/qiskit/release-notes/0.22) -* [0.21](/api/qiskit/release-notes/0.21) -* [0.20](/api/qiskit/release-notes/0.20) -* [0.19](/api/qiskit/release-notes/0.19) -* [0.18](/api/qiskit/release-notes/0.18) -* [0.17](/api/qiskit/release-notes/0.17) -* [0.16](/api/qiskit/release-notes/0.16) -* [0.15](/api/qiskit/release-notes/0.15) -* [0.14](/api/qiskit/release-notes/0.14) -* [0.13](/api/qiskit/release-notes/0.13) -* [0.12](/api/qiskit/release-notes/0.12) -* [0.11](/api/qiskit/release-notes/0.11) -* [0.10](/api/qiskit/release-notes/0.10) -* [0.9](/api/qiskit/release-notes/0.9) -* [0.8](/api/qiskit/release-notes/0.8) -* [0.7](/api/qiskit/release-notes/0.7) -* [0.6](/api/qiskit/release-notes/0.6) -* [0.5](/api/qiskit/release-notes/0.5) +* [1.0](./1.0) +* [0.46](./0.46) +* [0.45](./0.45) +* [0.44](./0.44) +* [0.43](./0.43) +* [0.42](./0.42) +* [0.41](./0.41) +* [0.40](./0.40) +* [0.39](./0.39) +* [0.38](./0.38) +* [0.37](./0.37) +* [0.36](./0.36) +* [0.35](./0.35) +* [0.34](./0.34) +* [0.33](./0.33) +* [0.32](./0.32) +* [0.31](./0.31) +* [0.30](./0.30) +* [0.29](./0.29) +* [0.28](./0.28) +* [0.27](./0.27) +* [0.26](./0.26) +* [0.25](./0.25) +* [0.24](./0.24) +* [0.23](./0.23) +* [0.22](./0.22) +* [0.21](./0.21) +* [0.20](./0.20) +* [0.19](./0.19) +* [0.18](./0.18) +* [0.17](./0.17) +* [0.16](./0.16) +* [0.15](./0.15) +* [0.14](./0.14) +* [0.13](./0.13) +* [0.12](./0.12) +* [0.11](./0.11) +* [0.10](./0.10) +* [0.9](./0.9) +* [0.8](./0.8) +* [0.7](./0.7) +* [0.6](./0.6) +* [0.5](./0.5) diff --git a/scripts/lib/api/Pkg.ts b/scripts/lib/api/Pkg.ts index 2640348b978..350881657a4 100644 --- a/scripts/lib/api/Pkg.ts +++ b/scripts/lib/api/Pkg.ts @@ -12,14 +12,17 @@ import { join } from "path/posix"; -import { findLegacyReleaseNotes } from "./releaseNotes"; +import { findSeparateReleaseNotesVersions } from "./releaseNotes"; import { getRoot } from "../fs"; import { determineHistoricalQiskitGithubUrl } from "../qiskitMetapackage"; import { TocGrouping } from "./TocGrouping"; -export interface ReleaseNoteEntry { - title: string; - url: string; +export class ReleaseNotesConfig { + readonly separatePagesVersions: string[]; + + constructor(kwargs: { separatePagesVersions?: string[] }) { + this.separatePagesVersions = kwargs.separatePagesVersions ?? []; + } } type PackageType = "latest" | "historical" | "dev"; @@ -31,11 +34,10 @@ export class Pkg { readonly name: string; readonly title: string; readonly githubSlug: string; - readonly hasSeparateReleaseNotes: boolean; readonly version: string; readonly versionWithoutPatch: string; readonly type: PackageType; - readonly releaseNoteEntries: ReleaseNoteEntry[]; + readonly releaseNotesConfig: ReleaseNotesConfig; readonly nestModulesInToc: boolean; readonly tocGrouping?: TocGrouping; @@ -45,22 +47,21 @@ export class Pkg { name: string; title: string; githubSlug: string; - hasSeparateReleaseNotes: boolean; version: string; versionWithoutPatch: string; type: PackageType; - releaseNoteEntries: ReleaseNoteEntry[]; + releaseNotesConfig?: ReleaseNotesConfig; nestModulesInToc?: boolean; tocGrouping?: TocGrouping; }) { this.name = kwargs.name; this.title = kwargs.title; this.githubSlug = kwargs.githubSlug; - this.hasSeparateReleaseNotes = kwargs.hasSeparateReleaseNotes; this.version = kwargs.version; this.versionWithoutPatch = kwargs.versionWithoutPatch; this.type = kwargs.type; - this.releaseNoteEntries = kwargs.releaseNoteEntries; + this.releaseNotesConfig = + kwargs.releaseNotesConfig ?? new ReleaseNotesConfig({}); this.nestModulesInToc = kwargs.nestModulesInToc ?? false; this.tocGrouping = kwargs.tocGrouping; } @@ -79,14 +80,15 @@ export class Pkg { }; if (name === "qiskit") { - const releaseNoteEntries = await findLegacyReleaseNotes(name); + const releaseNoteEntries = await findSeparateReleaseNotesVersions(name); return new Pkg({ ...args, title: "Qiskit SDK", name: "qiskit", githubSlug: "qiskit/qiskit", - hasSeparateReleaseNotes: true, - releaseNoteEntries, + releaseNotesConfig: new ReleaseNotesConfig({ + separatePagesVersions: releaseNoteEntries, + }), nestModulesInToc: true, }); } @@ -97,8 +99,6 @@ export class Pkg { title: "Qiskit Runtime IBM Client", name: "qiskit-ibm-runtime", githubSlug: "qiskit/qiskit-ibm-runtime", - hasSeparateReleaseNotes: false, - releaseNoteEntries: [], }); } @@ -108,8 +108,6 @@ export class Pkg { title: "Qiskit IBM Provider (deprecated)", name: "qiskit-ibm-provider", githubSlug: "qiskit/qiskit-ibm-provider", - hasSeparateReleaseNotes: false, - releaseNoteEntries: [], }); } @@ -120,11 +118,10 @@ export class Pkg { name?: string; title?: string; githubSlug?: string; - hasSeparateReleaseNotes?: boolean; version?: string; versionWithoutPatch?: string; type?: PackageType; - releaseNoteEntries?: ReleaseNoteEntry[]; + releaseNotesConfig?: ReleaseNotesConfig; nestModulesInToc?: boolean; tocGrouping?: TocGrouping; }): Pkg { @@ -132,11 +129,10 @@ export class Pkg { name: kwargs.name ?? "my-quantum-project", title: kwargs.title ?? "My Quantum Project", githubSlug: kwargs.githubSlug ?? "qiskit/my-quantum-project", - hasSeparateReleaseNotes: kwargs.hasSeparateReleaseNotes ?? false, version: kwargs.version ?? "0.1.0", versionWithoutPatch: kwargs.versionWithoutPatch ?? "0.1", type: kwargs.type ?? "latest", - releaseNoteEntries: kwargs.releaseNoteEntries ?? [], + releaseNotesConfig: kwargs.releaseNotesConfig, nestModulesInToc: kwargs.nestModulesInToc ?? false, tocGrouping: kwargs.tocGrouping ?? undefined, }); @@ -174,8 +170,12 @@ export class Pkg { return this.name !== "qiskit" || +this.versionWithoutPatch >= 0.45; } + hasSeparateReleaseNotes(): boolean { + return this.releaseNotesConfig.separatePagesVersions.length > 0; + } + releaseNotesTitle(): string { - const versionStr = this.hasSeparateReleaseNotes + const versionStr = this.hasSeparateReleaseNotes() ? ` ${this.versionWithoutPatch}` : ""; return `${this.title}${versionStr} release notes`; diff --git a/scripts/lib/api/addFrontMatter.test.ts b/scripts/lib/api/addFrontMatter.test.ts index 87572dfdefc..23a8aaf9793 100644 --- a/scripts/lib/api/addFrontMatter.test.ts +++ b/scripts/lib/api/addFrontMatter.test.ts @@ -13,7 +13,7 @@ import { expect, test } from "@jest/globals"; import addFrontMatter from "./addFrontMatter"; -import { Pkg } from "./Pkg"; +import { Pkg, ReleaseNotesConfig } from "./Pkg"; import { HtmlToMdResult } from "./HtmlToMdResult"; test("addFrontMatter()", () => { @@ -63,7 +63,9 @@ test("addFrontMatter()", () => { }, ]; const pkg = Pkg.mock({ - hasSeparateReleaseNotes: true, + releaseNotesConfig: new ReleaseNotesConfig({ + separatePagesVersions: ["0.1"], + }), }); addFrontMatter(results, pkg); @@ -105,7 +107,7 @@ python_api_name: quantum_software `--- title: My Quantum Project 0.1 release notes description: Changes made in My Quantum Project 0.1 -in_page_toc_max_heading_level: 2 +in_page_toc_max_heading_level: 3 --- # Some release notes! diff --git a/scripts/lib/api/addFrontMatter.ts b/scripts/lib/api/addFrontMatter.ts index 9b89a7c8c83..f09ba4b9d70 100644 --- a/scripts/lib/api/addFrontMatter.ts +++ b/scripts/lib/api/addFrontMatter.ts @@ -45,13 +45,14 @@ python_api_name: ${result.meta.apiName} ${markdown} `; } else if (result.isReleaseNotes) { - const versionStr = pkg.hasSeparateReleaseNotes - ? ` ${pkg.versionWithoutPatch}` - : ""; - const descriptionSuffix = pkg.hasSeparateReleaseNotes - ? `in ${pkg.title}${versionStr}` - : `to ${pkg.title}`; - const maxHeadingLevel = pkg.name == "qiskit" ? 3 : 2; + let versionStr = ""; + let descriptionSuffix = `to ${pkg.title}`; + let maxHeadingLevel = 2; + if (pkg.hasSeparateReleaseNotes()) { + versionStr = ` ${pkg.versionWithoutPatch}`; + descriptionSuffix = `in ${pkg.title}${versionStr}`; + maxHeadingLevel = 3; + } result.markdown = `--- title: ${pkg.title}${versionStr} release notes description: Changes made ${descriptionSuffix} diff --git a/scripts/lib/api/conversionPipeline.test.ts b/scripts/lib/api/conversionPipeline.test.ts index f449c2cfb1a..54ffdd8e5cf 100644 --- a/scripts/lib/api/conversionPipeline.test.ts +++ b/scripts/lib/api/conversionPipeline.test.ts @@ -55,11 +55,9 @@ test("qiskit-sphinx-theme", async () => { name: "qiskit-sphinx-theme", title: "Qiskit Sphinx Theme", githubSlug: "Qiskit/qiskit_sphinx_theme", - hasSeparateReleaseNotes: false, version: "0.1.1", versionWithoutPatch: "0.1", type: "latest", - releaseNoteEntries: [], }); const markdownFolder = pkg.outputDir(docsBaseFolder); diff --git a/scripts/lib/api/conversionPipeline.ts b/scripts/lib/api/conversionPipeline.ts index 31697a0eeed..25987641807 100644 --- a/scripts/lib/api/conversionPipeline.ts +++ b/scripts/lib/api/conversionPipeline.ts @@ -16,7 +16,6 @@ import { readFile, writeFile } from "fs/promises"; import { mkdirp } from "mkdirp"; import { globby } from "globby"; import { uniqBy } from "lodash"; -import transformLinks from "transform-markdown-links"; import { ObjectsInv } from "./objectsInv"; import { sphinxHtmlToMarkdown } from "./htmlToMd"; @@ -33,10 +32,8 @@ import removeMathBlocksIndentation from "./removeMathBlocksIndentation"; import { Pkg } from "./Pkg"; import { pathExists } from "../fs"; import { - addNewReleaseNotes, - generateReleaseNotesIndex, - updateHistoricalTocFiles, - writeReleaseNoteForVersion, + maybeUpdateReleaseNotesFolder, + handleReleaseNotesFile, } from "./releaseNotes"; export async function runConversionPipeline( @@ -175,61 +172,6 @@ async function writeMarkdownResults( } } -/** - * Determine what to do with release-notes.mdx, such as simply ignoring it. - * - * @returns true if the release notes file should be written. - */ -async function handleReleaseNotesFile( - result: HtmlToMdResultWithUrl, - pkg: Pkg, -): Promise { - // Dev versions haven't been released yet and we don't want to modify the release notes - // of prior versions. - if (pkg.isDev()) { - return false; - } - - // When the release notes are a single file, only use them if this is the latest version rather - // than a historical release. - if (!pkg.hasSeparateReleaseNotes) { - return pkg.isLatest(); - } - - // Else, there is a distinct release note per version. So, we use the release note but - // have custom logic to handle it. - - const baseUrl = pkg.isHistorical() - ? `/api/${pkg.name}/${pkg.versionWithoutPatch}` - : `/api/${pkg.name}`; - result.markdown = transformLinks(result.markdown, (link, _) => - link.startsWith("http") || link.startsWith("#") || link.startsWith("/") - ? link - : `${baseUrl}/${link}`, - ); - - await writeReleaseNoteForVersion(pkg, result.markdown); - return false; -} - -/** - * If there was a new release notes file, update the release notes index page and _toc.json for - * every docs version. - */ -async function maybeUpdateReleaseNotesFolder( - pkg: Pkg, - markdownPath: string, -): Promise { - if (!pkg.hasSeparateReleaseNotes || !pkg.isLatest()) { - return; - } - addNewReleaseNotes(pkg); - await updateHistoricalTocFiles(pkg); - console.log("Generating release-notes/index"); - const indexMarkdown = generateReleaseNotesIndex(pkg); - await writeFile(`${markdownPath}/release-notes/index.mdx`, indexMarkdown); -} - async function writeTocFile( pkg: Pkg, markdownPath: string, diff --git a/scripts/lib/api/generateToc.test.ts b/scripts/lib/api/generateToc.test.ts index 9d84f0d44ba..3aea2de5db2 100644 --- a/scripts/lib/api/generateToc.test.ts +++ b/scripts/lib/api/generateToc.test.ts @@ -13,7 +13,7 @@ import { describe, expect, test } from "@jest/globals"; import { generateToc } from "./generateToc"; -import { Pkg } from "./Pkg"; +import { Pkg, ReleaseNotesConfig } from "./Pkg"; import type { TocGroupingEntry } from "./TocGrouping"; const DEFAULT_ARGS = { @@ -276,10 +276,9 @@ describe("generateToc", () => { test("TOC with distinct release note files", () => { const toc = generateToc( Pkg.mock({ - releaseNoteEntries: [ - { title: "0.39", url: "/api/my-quantum-project/release-notes/0.39" }, - { title: "0.38", url: "/api/my-quantum-project/release-notes/0.38" }, - ], + releaseNotesConfig: new ReleaseNotesConfig({ + separatePagesVersions: ["0.39", "0.38"], + }), }), [ { diff --git a/scripts/lib/api/generateToc.ts b/scripts/lib/api/generateToc.ts index d67929e3c7d..f8e8cc9f4e5 100644 --- a/scripts/lib/api/generateToc.ts +++ b/scripts/lib/api/generateToc.ts @@ -271,8 +271,12 @@ function generateReleaseNotesEntries(pkg: Pkg) { const releaseNotesEntry: TocEntry = { title: "Release notes", }; - if (pkg.releaseNoteEntries.length) { - releaseNotesEntry.children = pkg.releaseNoteEntries; + if (pkg.hasSeparateReleaseNotes()) { + releaseNotesEntry.children = + pkg.releaseNotesConfig.separatePagesVersions.map((vers) => ({ + title: vers, + url: `${releaseNotesUrl}/${vers}`, + })); } else { releaseNotesEntry.url = releaseNotesUrl; } diff --git a/scripts/lib/api/releaseNotes.ts b/scripts/lib/api/releaseNotes.ts index bfe29b1b1dc..b4827142cb7 100644 --- a/scripts/lib/api/releaseNotes.ts +++ b/scripts/lib/api/releaseNotes.ts @@ -14,18 +14,81 @@ import { parse } from "path"; import { readFile, writeFile, readdir } from "fs/promises"; import { $ } from "zx"; +import transformLinks from "transform-markdown-links"; import { getRoot, pathExists } from "../fs"; -import type { Pkg, ReleaseNoteEntry } from "./Pkg"; +import type { Pkg } from "./Pkg"; +import type { HtmlToMdResultWithUrl } from "./HtmlToMdResult"; + +// --------------------------------------------------------------------------- +// Generic release notes handling +// --------------------------------------------------------------------------- + +/** + * Determine what to do with release-notes.mdx, such as simply ignoring it. + * + * @returns true if the release notes file should be written. + */ +export async function handleReleaseNotesFile( + result: HtmlToMdResultWithUrl, + pkg: Pkg, +): Promise { + // Dev versions haven't been released yet and we don't want to modify the release notes + // of prior versions. + if (pkg.isDev()) { + return false; + } + + // When the release notes are a single file, only use them if this is the latest version rather + // than a historical release. + if (!pkg.hasSeparateReleaseNotes()) { + return pkg.isLatest(); + } + + // Else, there is a distinct release note per version. So, we use the release note but + // have custom logic to handle it. + const baseUrl = pkg.isHistorical() + ? `/api/${pkg.name}/${pkg.versionWithoutPatch}` + : `/api/${pkg.name}`; + result.markdown = transformLinks(result.markdown, (link, _) => + link.startsWith("http") || link.startsWith("#") || link.startsWith("/") + ? link + : `${baseUrl}/${link}`, + ); + await writeReleaseNoteForVersion(pkg, result.markdown); + return false; +} + +// --------------------------------------------------------------------------- +// Utils for release-note per version +// --------------------------------------------------------------------------- + +/** + * If there was a new release notes file, update the release notes index page and _toc.json for + * every docs version. + */ +export async function maybeUpdateReleaseNotesFolder( + pkg: Pkg, + markdownPath: string, +): Promise { + if (!pkg.hasSeparateReleaseNotes() || !pkg.isLatest()) { + return; + } + addNewReleaseNotes(pkg); + await updateHistoricalTocFiles(pkg); + console.log("Generating release-notes/index"); + const indexMarkdown = generateReleaseNotesIndex(pkg); + await writeFile(`${markdownPath}/release-notes/index.mdx`, indexMarkdown); +} /** - * Check for markdown files in `docs/api/package-name/release-notes/ - * then sort them and create entries for the TOC. + * Check for markdown files in `docs/api//release-notes/, + * then sort them and return the list of versions. */ -export const findLegacyReleaseNotes = async ( +export const findSeparateReleaseNotesVersions = async ( pkgName: string, -): Promise => { - const legacyReleaseNoteVersions = ( +): Promise => { + return ( await $`ls ${getRoot()}/docs/api/${pkgName}/release-notes`.quiet() ).stdout .trim() @@ -46,29 +109,19 @@ export const findLegacyReleaseNotes = async ( return 0; }) .reverse(); - - const legacyReleaseNoteEntries = []; - for (let version of legacyReleaseNoteVersions) { - legacyReleaseNoteEntries.push({ - title: version, - url: `/api/${pkgName}/release-notes/${version}`, - }); - } - return legacyReleaseNoteEntries; }; -export function addNewReleaseNotes(pkg: Pkg): void { - if (pkg.releaseNoteEntries[0].title === pkg.versionWithoutPatch) { +function addNewReleaseNotes(pkg: Pkg): void { + if ( + pkg.releaseNotesConfig.separatePagesVersions[0] === pkg.versionWithoutPatch + ) { // Entries already includes most recent release notes return; } - pkg.releaseNoteEntries.unshift({ - title: pkg.versionWithoutPatch, - url: `/api/${pkg.name}/release-notes/${pkg.versionWithoutPatch}`, - }); + pkg.releaseNotesConfig.separatePagesVersions.unshift(pkg.versionWithoutPatch); } -export function generateReleaseNotesIndex(pkg: Pkg): string { +function generateReleaseNotesIndex(pkg: Pkg): string { let markdown = `--- title: ${pkg.title} release notes description: New features, bug fixes, and other changes in previous versions of ${pkg.title}. @@ -81,26 +134,17 @@ New features, bug fixes, and other changes in previous versions of ${pkg.title}. ## Release notes by version `; - for (const entry of pkg.releaseNoteEntries) { - markdown += `* [${entry.title}](${entry.url})\n`; + for (const version of pkg.releaseNotesConfig.separatePagesVersions) { + markdown += `* [${version}](./${version})\n`; } return markdown; } -/** - * Path to release notes file for the version we're adding - */ -export function currentReleaseNotesPath(pkg: Pkg): string { - return `${getRoot()}/docs/api/${pkg.name}/release-notes/${ - pkg.versionWithoutPatch - }.mdx`; -} - /** * Adds a new entry for the release notes of the current API version to the _toc.json * of all historical API versions. */ -export async function updateHistoricalTocFiles(pkg: Pkg): Promise { +async function updateHistoricalTocFiles(pkg: Pkg): Promise { console.log("Updating _toc.json files for the historical versions"); const historicalFolders = ( @@ -148,11 +192,11 @@ function addNewReleaseNoteToc(releaseNotesNode: any, newVersion: string) { * Creates the release note file for the minor version found in `pkg`.If the file * already exists, it will keep the initial header and overwrite the rest of the content. */ -export async function writeReleaseNoteForVersion( +async function writeReleaseNoteForVersion( pkg: Pkg, releaseNoteMarkdown: string, ): Promise { - if (!pkg.hasSeparateReleaseNotes) { + if (!pkg.hasSeparateReleaseNotes()) { throw new Error( `The package ${pkg.name} doesn't have separate release notes`, );