Files
2026-07-13 12:25:07 +08:00

247 lines
8.7 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import fs from "fs";
import { Octokit } from "octokit";
import { parseReleases } from "auto-changelog/src/releases.js";
import { fetchTags } from "auto-changelog/src/tags.js";
import { fetchRemote } from "auto-changelog/src/remote.js";
import * as toml from "@iarna/toml";
import "zx/globals";
// if (!process.env.GITHUB_TOKEN) {
// throw new Error("Missing GITHUB_TOKEN");
// }
const IS_NIGHTLY = process.argv.indexOf("--nightly") > -1;
const PKG_VERSION = JSON.parse(fs.readFileSync("./package.json")).version.match(
/[0-9]+\.[0-9]+\.[0-9]+/,
)[0];
const GIT_REV = await $`git show --no-patch --format=%ct HEAD`;
console.log();
const NEW_VERSION = IS_NIGHTLY
? `${PKG_VERSION}-dev${+new Date(parseInt(GIT_REV.toString()))}`
: PKG_VERSION;
/**
* A Github data fetching cache designed to run in parallel with changelog formatting.
* When a SHA is requested, it is looked up in a local cache, and if it isn't there,
* the next Github API page is awaited. Requests are always triggered in advance.
*/
class GithubPRCache {
constructor() {
this.prs = new Map();
this.page = 1;
this.octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
this.next();
}
async next() {
let ret;
if (this._next_req) {
ret = await this._next_req;
}
console.log(
`Fetching Github commits ${(this.page - 1) * 100} - ${
this.page * 100 - 1
}`,
);
this._next_req = this.octokit.request(
"GET /repos/{owner}/{repo}/pulls?state=closed&per_page={per_page}&page={page}",
{
owner: "perspective-dev",
repo: "perspective",
per_page: 100,
page: this.page++,
},
);
return ret;
}
async get(sha) {
while (!this.prs.has(sha) && !this.failed) {
const resp = await this.next();
if (resp.data.length == 0) {
this.failed = true;
}
for (const pr of resp.data) {
this.prs.set(pr.merge_commit_sha, pr);
}
}
return this.prs.get(sha);
}
}
function template_group(label, rows, prs) {
if (rows.length > 0) {
const group_prs = rows
.map((merge) => `- ${merge.message} [#${merge.id}](${merge.href})`)
.join("\n");
return `${label}\n\n${group_prs}\n`;
} else {
return "";
}
}
function template_release(args) {
const prs = Object.keys(args.row)
.map((label) => template_group(label, args.row[label]))
.filter((x) => x.length > 0)
.join("\n");
return `# [${args.release.title}](https://github.com/perspective-dev/perspective/releases/tag/${args.release.title})
_${args.release.niceDate}_ ([Full changelog](${args.release.href}))
${prs}
`;
}
/**
* Generate the changelog text from the output of `auto-changelog`, iteratively calling
* Github's API when necessary to get label information for grouping.
*
* @param {*} json the output data from `auto-changelog`
* @returns
*/
async function template(json) {
const cache = new GithubPRCache();
let changelog = "";
for (const release of json) {
const row = {
"**Breaking**": [],
Features: [],
Fixes: [],
Misc: [],
};
for (const merge of release.merges) {
const pr = await cache.get(merge.commit.hash);
merge.pr = pr;
if (pr) {
const labels = pr.labels.map((x) => x.name);
if (labels.indexOf("breaking") > -1) {
row["**Breaking**"].push(merge);
} else if (labels.indexOf("enhancement") > -1) {
row.Features.push(merge);
} else if (labels.indexOf("bug") > -1) {
row.Fixes.push(merge);
} else {
row.Misc.push(merge);
}
} else {
row.Misc.push(merge);
}
}
changelog += template_release({
row,
release,
});
}
return changelog;
}
/**
* Update the `CHANGELOG.md` file from Github API.
*/
async function update_changelog() {
let options = {
remote: "origin",
commitLimit: 10000,
backfillLimit: 10000,
tagPrefix: "",
sortCommits: "relevance",
appendGitLog: "",
appendGitTag: "",
latestVersion: NEW_VERSION,
};
const remote = await fetchRemote(options);
options = {
...options,
...remote,
};
console.log("Fetching tags…");
const tags = await fetchTags(options);
console.log(`${tags.length} version tags found…`);
const onParsed = ({ title }) => console.log(`Fetched ${title}…`);
const json = await parseReleases(tags, options, onParsed);
const changelog = await template(json);
fs.writeFileSync("./CHANGELOG.md", changelog);
$.sync`git add CHANGELOG.md`;
}
/**
* Update all project `package.json` files.
*/
async function update_package_jsons() {
const pkg = JSON.parse(fs.readFileSync("./package.json"));
pkg.version = NEW_VERSION;
const pkg_json = `${JSON.stringify(pkg, undefined, 4)}\n`;
fs.writeFileSync("./package.json", pkg_json);
const packages = {};
for (const ws of pkg.workspaces) {
if (ws === "packages/jupyterlab" && IS_NIGHTLY) {
continue;
}
for (const path of glob.sync(`${ws}/package.json`, {
sync: true,
})) {
const pkg = JSON.parse(fs.readFileSync(path));
pkg.version = NEW_VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, undefined, 4) + "\n");
packages[pkg.name] = {
pkg,
path,
};
}
}
const cargo = toml.parse(fs.readFileSync("./Cargo.toml"));
for (const pkg of cargo.workspace.members) {
const crate = toml.parse(fs.readFileSync(`./${pkg}/Cargo.toml`));
if (crate?.package.version) {
crate.package.version = NEW_VERSION;
}
for (const key of Object.keys(crate?.dependencies)) {
if (key.startsWith("perspective")) {
crate.dependencies[key].version = NEW_VERSION;
}
}
fs.writeFileSync(`./${pkg}/Cargo.toml`, toml.stringify(crate));
}
// if (!IS_NIGHTLY) {
const pyproject = toml.parse(
fs.readFileSync(`./rust/perspective-python/pyproject.toml`),
);
pyproject.tool.maturin.data = `perspective_python-${NEW_VERSION}.data`;
fs.writeFileSync(
`./rust/perspective-python/pyproject.toml`,
toml.stringify(pyproject),
);
// }
}
if (!IS_NIGHTLY) {
if (!process.env.GITHUB_TOKEN) {
throw new Error("Missing GITHUB_TOKEN");
}
await update_changelog();
}
await update_package_jsons();