285 lines
7 KiB
TypeScript
285 lines
7 KiB
TypeScript
// oxlint-disable no-console
|
|
import { resolve } from 'node:path';
|
|
import { exit } from 'node:process';
|
|
|
|
import { Octokit } from '@octokit/rest';
|
|
import { $, Glob } from 'bun';
|
|
|
|
const CHANGELOG_NAME = 'CHANGELOG.md';
|
|
const RELEASE_NOTES_NAME = 'RELEASE_NOTES.md';
|
|
|
|
const DRY_RUN = true;
|
|
const PUBLISH_VERSION = process.env.PUBLISH_VERSION;
|
|
|
|
const log = {
|
|
colors: {
|
|
reset: '\x1b[0m',
|
|
info: Bun.color('oklch(86.5% 0.127 207.078)', 'ansi'),
|
|
success: Bun.color('oklch(76.5% 0.177 163.223)', 'ansi'),
|
|
warn: Bun.color('oklch(87.9% 0.169 91.605)', 'ansi'),
|
|
error: Bun.color('oklch(71.2% 0.194 13.428)', 'ansi'),
|
|
border: Bun.color('oklch(70.7% 0.022 261.325)', 'ansi'),
|
|
},
|
|
log(message: string) {
|
|
process.stdout.write(`${message}\n`);
|
|
},
|
|
line() {
|
|
this.log('');
|
|
},
|
|
info(message: string) {
|
|
this.log(`${this.colors.info}• ${message}${this.colors.reset}`);
|
|
},
|
|
success(message: string) {
|
|
this.log(`${this.colors.success} ${message}${this.colors.reset}`);
|
|
},
|
|
warn(message: string) {
|
|
this.log(`${this.colors.warn}! ${message} !${this.colors.reset}`);
|
|
},
|
|
error(message: string) {
|
|
this.log(`${this.colors.error} ${message}${this.colors.reset}`);
|
|
},
|
|
section(title: string) {
|
|
this.log(
|
|
`${log.colors.border}--------- ${title} -----------${log.colors.reset}`
|
|
);
|
|
},
|
|
};
|
|
|
|
async function getPublishVersion() {
|
|
if (PUBLISH_VERSION && PUBLISH_VERSION.trim().length > 0) {
|
|
return PUBLISH_VERSION;
|
|
}
|
|
|
|
const { version } = await import('../packages/docsearch-react/src/version');
|
|
|
|
return version;
|
|
}
|
|
|
|
function getChangelogEntry(changelog: string, version: string) {
|
|
let headingStartInfo: { index: number; depth: number } | undefined;
|
|
let endIndex: number | undefined;
|
|
|
|
// Iterate through each headings and code blocks (for skipping its contents)
|
|
const regex = /^(#{1,6})\s(.*)$|^(`{3,})/gm;
|
|
let match: RegExpExecArray | null;
|
|
while ((match = regex.exec(changelog)) !== null) {
|
|
// Skip over code blocks so we don't match any headings inside of them
|
|
if (match[3]) {
|
|
const endOfCodeBlockRegex = new RegExp(`^${match[3]}`, 'gm');
|
|
endOfCodeBlockRegex.lastIndex = regex.lastIndex;
|
|
const endMatch = endOfCodeBlockRegex.exec(changelog);
|
|
if (endMatch) {
|
|
// Start next search for headings after the end of the code block
|
|
regex.lastIndex = endOfCodeBlockRegex.lastIndex;
|
|
continue;
|
|
} else {
|
|
// Can't find end of code block, probably malformed
|
|
break;
|
|
}
|
|
}
|
|
|
|
const headingDepth = match[1].length;
|
|
const headingText = match[2].trim();
|
|
|
|
// Search for heading of the entry
|
|
if (headingText === version) {
|
|
headingStartInfo = { index: regex.lastIndex, depth: headingDepth };
|
|
continue;
|
|
}
|
|
|
|
// If we've found the entry heading, search for the closing heading with the same depth
|
|
if (headingStartInfo && headingDepth === headingStartInfo.depth) {
|
|
endIndex = match.index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return changelog.slice(headingStartInfo?.index, endIndex).trim();
|
|
}
|
|
|
|
interface Pkg {
|
|
tagName: string;
|
|
path: string;
|
|
packageJson: { version: string } & Record<string, any>;
|
|
}
|
|
|
|
async function getPkg(tag: string): Promise<Pkg | null> {
|
|
const pkgJsonFiles = new Glob('{packages,adapters}/**/package.json');
|
|
let pkg: Pkg | null = null;
|
|
|
|
for await (const file of pkgJsonFiles.scan('.')) {
|
|
const pkgJson = await Bun.file(file).json();
|
|
|
|
if (tag.startsWith(`${pkgJson.name}@`)) {
|
|
pkg = {
|
|
tagName: tag,
|
|
path: file.replace(/\package\.json$/, ''),
|
|
packageJson: pkgJson,
|
|
};
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return pkg;
|
|
}
|
|
|
|
// Push git tag
|
|
async function pushTag(tagName: string) {
|
|
if (DRY_RUN) {
|
|
log.log(`[DRY_RUN] - Pushing tag ${tagName}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await $`git push origin ${tagName}`.quiet();
|
|
} catch (e) {
|
|
throw new Error(`Error pushing tag ${tagName}: ${e}`);
|
|
}
|
|
}
|
|
|
|
// Create GH release
|
|
async function createRelease({
|
|
token,
|
|
tagName,
|
|
changelog,
|
|
prerelease = false,
|
|
}: {
|
|
token?: string;
|
|
tagName: string;
|
|
changelog: string;
|
|
prerelease?: boolean;
|
|
}): Promise<void> {
|
|
if (DRY_RUN) {
|
|
log.log(`[DRY_RUN] - Creating release for ${tagName}:`);
|
|
log.log(` - Prerelease: ${prerelease}`);
|
|
log.line();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const octokit = new Octokit({ auth: token });
|
|
|
|
await octokit.rest.repos.createRelease({
|
|
owner: 'algolia',
|
|
repo: 'docsearch',
|
|
name: tagName,
|
|
tag_name: tagName,
|
|
body: changelog,
|
|
prerelease,
|
|
make_latest: prerelease ? 'false' : 'true',
|
|
});
|
|
} catch (e) {
|
|
throw new Error(`Error creating release for ${tagName}: ${e}`);
|
|
}
|
|
}
|
|
|
|
async function getTagsForVersion(version: string) {
|
|
const { stdout } = await $`git tag --list '*${version}'`.quiet();
|
|
const tags = stdout.toString().trim().split('\n').filter(Boolean);
|
|
|
|
return tags;
|
|
}
|
|
|
|
async function getPackages(tags: string[]) {
|
|
const pkgs: Pkg[] = [];
|
|
|
|
for (const t of tags) {
|
|
const pkg = await getPkg(t);
|
|
|
|
if (!pkg) {
|
|
continue;
|
|
}
|
|
|
|
pkgs.push(pkg);
|
|
}
|
|
|
|
return pkgs;
|
|
}
|
|
|
|
async function runFlow() {
|
|
const githubToken = process.env.GITHUB_TOKEN;
|
|
|
|
if (!DRY_RUN && (!githubToken || githubToken.trim() === '')) {
|
|
log.error('A `GITHUB_TOKEN` is required');
|
|
exit(1);
|
|
}
|
|
|
|
const version = await getPublishVersion();
|
|
const tags = await getTagsForVersion(version);
|
|
|
|
if (tags.length === 0) {
|
|
log.info(`No tags found to publish version v${version} with, exiting`);
|
|
exit(0);
|
|
}
|
|
|
|
const pkgs = await getPackages(tags);
|
|
|
|
if (pkgs.length === 0) {
|
|
log.info(`No packages found to create releases for, exiting`);
|
|
exit(0);
|
|
}
|
|
|
|
log.line();
|
|
log.log(`🚀 Generating package releases for v${version}`);
|
|
log.line();
|
|
|
|
const publishedPackages: string[] = [];
|
|
|
|
for (const pkg of pkgs) {
|
|
log.section(pkg.packageJson.name);
|
|
log.info(`Creating release for: ${pkg.packageJson.name}`);
|
|
|
|
if (DRY_RUN) {
|
|
log.warn('DRY_RUN enabled - no actual changes will be published');
|
|
}
|
|
|
|
if (!pkg.path) {
|
|
log.error(`No path found for ${pkg.tagName}`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const changelog = await Bun.file(
|
|
resolve(pkg.path, CHANGELOG_NAME)
|
|
).text();
|
|
|
|
const changelogEntry = getChangelogEntry(
|
|
changelog,
|
|
PUBLISH_VERSION ? PUBLISH_VERSION : pkg.packageJson.version
|
|
);
|
|
|
|
await Bun.write(resolve(pkg.path, RELEASE_NOTES_NAME), changelogEntry);
|
|
|
|
// await pushTag(pkg.tagName);
|
|
|
|
// await createRelease({
|
|
// token: githubToken,
|
|
// tagName: pkg.tagName,
|
|
// changelog: changelogEntry,
|
|
// prerelease: pkg.packageJson.version.includes('-'),
|
|
// });
|
|
|
|
publishedPackages.push(pkg.packageJson.name);
|
|
} catch (e) {
|
|
log.error(Error.isError(e) ? e.message : String(e));
|
|
}
|
|
log.line();
|
|
}
|
|
|
|
if (!DRY_RUN) {
|
|
log.line();
|
|
}
|
|
|
|
if (publishedPackages.length === 0) {
|
|
log.error('Failed to generate releases on GitHub');
|
|
exit(1);
|
|
}
|
|
|
|
log.success('GitHub releases generated for the following packages:');
|
|
publishedPackages.forEach((pkg) => {
|
|
log.log(` - ${pkg}`);
|
|
});
|
|
}
|
|
|
|
runFlow();
|