mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-04-19 18:02:16 +03:00
166 lines
5.6 KiB
JavaScript
Executable File
166 lines
5.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const fs = require("fs");
|
|
|
|
async function listReleases(github, owner, repo) {
|
|
const response = await github.rest.repos.listReleases({
|
|
owner,
|
|
repo,
|
|
per_page: 100,
|
|
});
|
|
// Filters out draft releases
|
|
return response.data.filter((release) => !release.draft);
|
|
}
|
|
|
|
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
|
// Or it can be a string in the form accepted by `getRelease`
|
|
async function getReleases(github, dependency) {
|
|
if (Array.isArray(dependency)) {
|
|
const [dep, fromVersion, toVersion] = dependency;
|
|
const upstreamPackageJson = getDependencyPackageJson(dep);
|
|
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
|
|
|
const unfilteredReleases = await listReleases(github, owner, repo);
|
|
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
|
|
const releases = unfilteredReleases.filter(
|
|
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
|
|
);
|
|
|
|
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
|
|
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
|
|
|
|
return releases.slice(toVersionIndex, fromVersionIndex);
|
|
}
|
|
|
|
return [await getRelease(github, dependency)];
|
|
}
|
|
|
|
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
|
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
|
|
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
|
|
async function getRelease(github, dependency) {
|
|
let owner;
|
|
let repo;
|
|
let tag;
|
|
|
|
if (dependency.includes("/")) {
|
|
let rest;
|
|
[owner, rest] = dependency.split("/");
|
|
|
|
if (dependency.includes("@")) {
|
|
[repo, tag] = rest.split("@");
|
|
} else if (dependency.includes("~")) {
|
|
[repo, tag] = rest.split("~");
|
|
|
|
if (tag.includes("-rc.")) {
|
|
// If the tag is an RC, find the latest matching RC in the set
|
|
try {
|
|
const releases = await listReleases(github, owner, repo);
|
|
const baseVersion = tag.split("-rc.")[0];
|
|
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
|
|
if (release) return release;
|
|
} catch (e) {
|
|
// Fall back to getReleaseByTag
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const upstreamPackageJson = getDependencyPackageJson(dependency);
|
|
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
|
tag = `v${upstreamPackageJson.version}`;
|
|
}
|
|
|
|
const response = await github.rest.repos.getReleaseByTag({
|
|
owner,
|
|
repo,
|
|
tag,
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
function getDependencyPackageJson(dependency) {
|
|
return JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
|
}
|
|
|
|
const HEADING_PREFIX = "## ";
|
|
|
|
const categories = [
|
|
"🔒 SECURITY FIXES",
|
|
"🚨 BREAKING CHANGESd",
|
|
"🦖 Deprecations",
|
|
"✨ Features",
|
|
"🐛 Bug Fixes",
|
|
"🧰 Maintenance",
|
|
];
|
|
|
|
const parseReleaseNotes = (body, sections) => {
|
|
let heading = null;
|
|
for (const line of body.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith(HEADING_PREFIX)) {
|
|
heading = trimmed.slice(HEADING_PREFIX.length);
|
|
if (!categories.includes(heading)) heading = null;
|
|
continue;
|
|
}
|
|
if (heading && trimmed) {
|
|
sections[heading].push(trimmed);
|
|
}
|
|
}
|
|
};
|
|
|
|
const main = async ({ github, releaseId, dependencies }) => {
|
|
const { GITHUB_REPOSITORY } = process.env;
|
|
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
|
|
|
const { data: release } = await github.rest.repos.getRelease({
|
|
owner,
|
|
repo,
|
|
release_id: releaseId,
|
|
});
|
|
|
|
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
|
parseReleaseNotes(release.body, sections);
|
|
for (const dependency of dependencies) {
|
|
const releases = await getReleases(github, dependency);
|
|
for (const release of releases) {
|
|
parseReleaseNotes(release.body, sections);
|
|
}
|
|
}
|
|
|
|
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
|
|
|
let output = "";
|
|
if (intro) {
|
|
output = intro + "\n\n";
|
|
}
|
|
|
|
for (const section in sections) {
|
|
const lines = sections[section];
|
|
if (!lines.length) continue;
|
|
output += HEADING_PREFIX + section + "\n\n";
|
|
output += lines.join("\n");
|
|
output += "\n\n";
|
|
}
|
|
|
|
return output;
|
|
};
|
|
|
|
// This is just for testing locally
|
|
// Needs environment variables GITHUB_TOKEN & GITHUB_REPOSITORY
|
|
if (require.main === module) {
|
|
const { Octokit } = require("@octokit/rest");
|
|
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
if (process.argv.length < 4) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Usage: node merge-release-notes.js owner/repo:release_id npm-package-name ...");
|
|
process.exit(1);
|
|
}
|
|
const [releaseId, ...dependencies] = process.argv.slice(2);
|
|
main({ github, releaseId, dependencies }).then((output) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(output);
|
|
});
|
|
}
|
|
|
|
module.exports = main;
|