forked from Qiskit/documentation
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automate the regeneration of the API docs (Qiskit#739)
### Summary Thanks to Qiskit#737, the API generation script doesn't need an argument to specify the CI artifact URL used in the generation. Because of that, we can automatize the script to regenerate all API docs in the repository. ### New command The PR adds a new command `regen-apis` where we can regenerate multiple versions of our three APIs with one command: ```bash npm run regen-apis ``` By default, all minor releases of each API will be regenerated, but we can add the `--current-apis-only` argument to regenerate only the latest stable versions. ```bash npm run regen-apis -- --current-apis-only ``` Alternatively, you can regenerate only one of the three available APIs. We can use the `-p` argument, which could be combined with the other arguments, similarly to the API generation script: ```bash npm run regen-apis -- -p <pkg-name> ``` ### Git instructions To run the script, we need to create a dedicated branch and have a clean git status. The script will create a commit per version regenerated and the developers can git cherry-pick them how they want to create different PRs. If the regeneration fails for a specific version, the script will call `git restore` to avoid mixing changes from two different versions when regenerating the next one. ### Output At the end of each call to the new command, we will see a summarization of all versions regenerated with three different possible outcomes per version: <table style="width:100%"> <tr> <th>Possible Output</th> <th>Meaning</th> <th>Creates a new commit</th> <th>Restores the files</th> </tr> <tr> <td>✅ <pkg-name> <version> regenerated correctly</td> <td>The regeneration was successful</td> <td>Yes</td> <td>No</td> </tr> <tr> <td>☑️ <pkg-name> <version> is up-to-date</td> <td>The regeneration was successful, but no files were modified</td> <td>No</td> <td>No</td> </tr> <tr> <td>❌ <pkg-name> <version> failed to regenerate</td> <td>The regeneration failed</td> <td>No</td> <td>Yes</td> </tr> </table> Closes Qiskit#720 --------- Co-authored-by: Eric Arellano <[email protected]>
- Loading branch information
1 parent
00df162
commit c6ef8cd
Showing
4 changed files
with
222 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// This code is a Qiskit project. | ||
// | ||
// (C) Copyright IBM 2024. | ||
// | ||
// This code is licensed under the Apache License, Version 2.0. You may | ||
// obtain a copy of this license in the LICENSE file in the root directory | ||
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
// | ||
// Any modifications or derivative works of this code must retain this | ||
// copyright notice, and modified files need to carry a notice indicating | ||
// that they have been altered from the originals. | ||
|
||
import { readFile, readdir } from "fs/promises"; | ||
|
||
import yargs from "yargs/yargs"; | ||
import { hideBin } from "yargs/helpers"; | ||
import { $ } from "zx"; | ||
import fs from "fs"; | ||
|
||
import { Pkg } from "../lib/api/Pkg"; | ||
import { zxMain } from "../lib/zx"; | ||
|
||
interface Arguments { | ||
[x: string]: unknown; | ||
package?: string; | ||
currentApisOnly: boolean; | ||
} | ||
|
||
const readArgs = (): Arguments => { | ||
return yargs(hideBin(process.argv)) | ||
.version(false) | ||
.option("package", { | ||
alias: "p", | ||
type: "string", | ||
choices: Pkg.VALID_NAMES, | ||
demandOption: false, | ||
description: "Which package to update", | ||
}) | ||
.option("current-apis-only", { | ||
type: "boolean", | ||
default: false, | ||
description: "Regenerate only the current API docs?", | ||
}) | ||
.parseSync(); | ||
}; | ||
|
||
zxMain(async () => { | ||
const args = readArgs(); | ||
await validateGitStatus(); | ||
|
||
const results = new Map<string, string[]>(); | ||
for (const pkgName of Pkg.VALID_NAMES) { | ||
if (args.package && pkgName != args.package) { | ||
continue; | ||
} | ||
|
||
const [historicalVersions, currentVersion] = await getPackageVersions( | ||
pkgName, | ||
args.currentApisOnly, | ||
); | ||
const result = await processVersions( | ||
pkgName, | ||
historicalVersions, | ||
currentVersion, | ||
); | ||
results.set(pkgName, result); | ||
} | ||
|
||
console.log(""); | ||
results.forEach((result: string[], pkgName: string) => { | ||
console.log(`Regeneration of ${pkgName}:`); | ||
result.forEach((msg) => console.error(msg)); | ||
console.log(""); | ||
}); | ||
|
||
console.log(`Each regenerated version has been saved as a distinct commit. If the changes are | ||
too large for one single PR, consider splitting it up into multiple PRs by using | ||
git cherry-pick or git rebase -i so each PR only has the commits it wants to target.`); | ||
}); | ||
|
||
async function processVersions( | ||
pkgName: string, | ||
historicalVersions: string[], | ||
currentVersion: string, | ||
): Promise<string[]> { | ||
const results: string[] = []; | ||
|
||
for (const historicalVersion of historicalVersions) { | ||
results.push(await regenerateVersion(pkgName, historicalVersion)); | ||
} | ||
results.push(await regenerateVersion(pkgName, currentVersion, false)); | ||
return results; | ||
} | ||
|
||
async function regenerateVersion( | ||
pkgName: string, | ||
version: string, | ||
historical: boolean = true, | ||
): Promise<string> { | ||
try { | ||
if (historical) { | ||
await $`npm run gen-api -- -p ${pkgName} -v ${version} --historical`; | ||
} else { | ||
await $`npm run gen-api -- -p ${pkgName} -v ${version}`; | ||
} | ||
|
||
if ((await gitStatus()) !== "") { | ||
await gitCommit(`Regenerate ${pkgName} ${version}`); | ||
return `✅ ${pkgName} ${version} regenerated correctly`; | ||
} else { | ||
return `☑️ ${pkgName} ${version} is up-to-date`; | ||
} | ||
} catch (_) { | ||
await gitRestore("."); | ||
return `❌ ${pkgName} ${version} failed to regenerate`; | ||
} | ||
} | ||
|
||
async function getPackageVersions( | ||
pkgName: string, | ||
currentApisOnly: boolean, | ||
): Promise<[string[], string]> { | ||
const pkgDocsPath = `docs/api/${pkgName}`; | ||
const historicalVersions: string[] = []; | ||
|
||
if (!currentApisOnly) { | ||
const historicalFolders = ( | ||
await readdir(`${pkgDocsPath}`, { withFileTypes: true }) | ||
).filter((file) => file.isDirectory() && file.name.match(/[0-9].*/)); | ||
|
||
for (const folder of historicalFolders) { | ||
const historicalVersion = JSON.parse( | ||
await readFile(`${pkgDocsPath}/${folder.name}/_package.json`, "utf-8"), | ||
); | ||
historicalVersions.push(historicalVersion.version); | ||
} | ||
} | ||
|
||
const currentVersion = JSON.parse( | ||
await readFile(`${pkgDocsPath}/_package.json`, "utf-8"), | ||
); | ||
|
||
return [historicalVersions, currentVersion.version]; | ||
} | ||
|
||
async function validateGitStatus(): Promise<void> { | ||
const initialStatus = await gitStatus(); | ||
if (initialStatus !== "") { | ||
console.error(` | ||
Repository must have clean Git state when calling | ||
\`npm run regenerate-apis\`. 'git status' returned:\n\n${initialStatus}`); | ||
process.exit(1); | ||
} | ||
|
||
const currentBranch = await gitBranch(); | ||
if (currentBranch == "main\n") { | ||
console.error(` | ||
Please create a dedicated branch to regenerate the API docs correctly. | ||
Use 'git checkout -b <name-branch>' to continue`); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
async function gitStatus(): Promise<string> { | ||
const status = await $`git status --porcelain`.quiet(); | ||
return status.stdout; | ||
} | ||
|
||
async function gitBranch(): Promise<string> { | ||
const status = await $`git branch --show-current`.quiet(); | ||
return status.stdout; | ||
} | ||
|
||
async function gitCommit(message: string): Promise<void> { | ||
await $`git add .`; | ||
await $`git commit -m ${message}`; | ||
} | ||
|
||
async function gitRestore(path: string): Promise<void> { | ||
await $`git restore ${path}`; | ||
} |