Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
253 lines
6.9 KiB
TypeScript
253 lines
6.9 KiB
TypeScript
/* eslint-disable prefer-template */
|
|
/* eslint-disable no-console */
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { readFile, writeFile, appendFile } from 'node:fs/promises';
|
|
import * as readline from 'node:readline/promises';
|
|
import yargs from 'yargs';
|
|
import { hideBin } from 'yargs/helpers';
|
|
import { $ } from 'execa';
|
|
import chalk from 'chalk';
|
|
|
|
const $$ = $({ stdio: 'inherit' });
|
|
|
|
const currentDirectory = dirname(fileURLToPath(import.meta.url));
|
|
const workspaceRoot = resolve(currentDirectory, '..');
|
|
|
|
interface PackageInfo {
|
|
name: string;
|
|
path: string;
|
|
version: string;
|
|
private: boolean;
|
|
}
|
|
|
|
interface RunOptions {
|
|
accessToken?: string;
|
|
baseline?: string;
|
|
dryRun: boolean;
|
|
skipLastCommitComparison: boolean;
|
|
yes: boolean;
|
|
ignore: string[];
|
|
}
|
|
|
|
async function run({
|
|
dryRun,
|
|
accessToken,
|
|
baseline,
|
|
skipLastCommitComparison,
|
|
yes,
|
|
ignore,
|
|
}: RunOptions) {
|
|
await ensureCleanWorkingDirectory();
|
|
|
|
const changedPackages = await getChangedPackages(baseline, skipLastCommitComparison, ignore);
|
|
if (changedPackages.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await confirmPublishing(changedPackages, yes);
|
|
|
|
try {
|
|
await setAccessToken(accessToken);
|
|
await setVersion(changedPackages);
|
|
await buildPackages();
|
|
await publishPackages(changedPackages, dryRun);
|
|
} finally {
|
|
await cleanUp();
|
|
}
|
|
}
|
|
|
|
async function ensureCleanWorkingDirectory() {
|
|
try {
|
|
await $`git diff --quiet`;
|
|
await $`git diff --quiet --cached`;
|
|
} catch (error) {
|
|
console.error('❌ Working directory is not clean.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function listPublicChangedPackages(baseline: string) {
|
|
const { stdout: packagesJson } =
|
|
await $`pnpm list --recursive --filter ...[${baseline}] --depth -1 --only-projects --json`;
|
|
const packages = JSON.parse(packagesJson) as PackageInfo[];
|
|
return packages.filter((pkg) => !pkg.private);
|
|
}
|
|
|
|
async function getChangedPackages(
|
|
baseline: string | undefined,
|
|
skipLastCommitComparison: boolean,
|
|
ignore: string[],
|
|
): Promise<PackageInfo[]> {
|
|
if (!skipLastCommitComparison) {
|
|
const publicPackagesUpdatedInLastCommit = await listPublicChangedPackages('HEAD~1');
|
|
if (publicPackagesUpdatedInLastCommit.length === 0) {
|
|
console.log('No public packages changed in the last commit.');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (!baseline) {
|
|
const { stdout: latestTag } = await $`git describe --abbrev=0`;
|
|
baseline = latestTag;
|
|
}
|
|
|
|
console.log(`Looking for changed public packages since ${chalk.yellow(baseline)}...`);
|
|
|
|
const changedPackages = (await listPublicChangedPackages(baseline)).filter(
|
|
(p) => !ignore.includes(p.name),
|
|
);
|
|
|
|
if (changedPackages.length === 0) {
|
|
console.log('Nothing found.');
|
|
}
|
|
|
|
return changedPackages;
|
|
}
|
|
|
|
async function confirmPublishing(changedPackages: PackageInfo[], yes: boolean) {
|
|
if (!yes) {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
console.log('\nFound changes in the following packages:');
|
|
for (const pkg of changedPackages) {
|
|
console.log(` - ${pkg.name}`);
|
|
}
|
|
|
|
console.log('\nThis will publish the above packages to the npm registry.');
|
|
const answer = await rl.question('Do you want to proceed? (y/n) ');
|
|
|
|
rl.close();
|
|
|
|
if (answer.toLowerCase() !== 'y') {
|
|
console.log('Aborted.');
|
|
process.exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function setAccessToken(npmAccessToken: string | undefined) {
|
|
if (!npmAccessToken && !process.env.NPM_TOKEN) {
|
|
console.error(
|
|
'❌ NPM access token is required. Either pass it as an --access-token argument or set it as an NPM_TOKEN environment variable.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const npmrcPath = resolve(workspaceRoot, '.npmrc');
|
|
|
|
await appendFile(
|
|
npmrcPath,
|
|
`//registry.npmjs.org/:_authToken=${npmAccessToken ?? process.env.NPM_TOKEN}\n`,
|
|
);
|
|
}
|
|
|
|
async function setVersion(packages: PackageInfo[]) {
|
|
const { stdout: currentRevisionSha } = await $`git rev-parse --short HEAD`;
|
|
const { stdout: commitTimestamp } = await $`git show --no-patch --format=%ct HEAD`;
|
|
const timestamp = formatDate(new Date(+commitTimestamp * 1000));
|
|
let hasError = false;
|
|
|
|
const tasks = packages.map(async (pkg) => {
|
|
const packageJsonPath = resolve(pkg.path, './package.json');
|
|
try {
|
|
const packageJson = JSON.parse(await readFile(packageJsonPath, { encoding: 'utf8' }));
|
|
packageJson.version = `${packageJson.version}-dev.${timestamp}-${currentRevisionSha}`;
|
|
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
} catch (error) {
|
|
console.error(`${chalk.red(`❌ ${packageJsonPath}`)}`, error);
|
|
hasError = true;
|
|
}
|
|
});
|
|
|
|
await Promise.allSettled(tasks);
|
|
if (hasError) {
|
|
throw new Error('Failed to update package versions');
|
|
}
|
|
}
|
|
|
|
function formatDate(date: Date) {
|
|
// yyyyMMdd-HHmmss
|
|
return date
|
|
.toISOString()
|
|
.replace(/[-:Z.]/g, '')
|
|
.replace('T', '-')
|
|
.slice(0, 15);
|
|
}
|
|
|
|
function buildPackages() {
|
|
if (process.env.CI) {
|
|
return $$`pnpm build:public:ci`;
|
|
}
|
|
|
|
return $$`pnpm build:public`;
|
|
}
|
|
|
|
async function publishPackages(packages: PackageInfo[], dryRun: boolean) {
|
|
console.log(`\nPublishing packages${dryRun ? ' (dry run)' : ''}`);
|
|
const tasks = packages.map(async (pkg) => {
|
|
try {
|
|
const args = [pkg.path, '--tag', 'canary', '--no-git-checks'];
|
|
if (dryRun) {
|
|
args.push('--dry-run');
|
|
}
|
|
await $$`pnpm publish ${args}`;
|
|
} catch (error: any) {
|
|
console.error(chalk.red(`❌ ${pkg.name}`), error.shortMessage);
|
|
}
|
|
});
|
|
|
|
await Promise.allSettled(tasks);
|
|
}
|
|
|
|
async function cleanUp() {
|
|
await $`git restore .`;
|
|
}
|
|
|
|
yargs(hideBin(process.argv))
|
|
.command<RunOptions>(
|
|
'$0',
|
|
'Publishes packages that have changed since the last release (or a specified commit).',
|
|
(command) => {
|
|
return command
|
|
.option('dryRun', {
|
|
default: false,
|
|
describe: 'If true, no packages will be published to the registry.',
|
|
type: 'boolean',
|
|
})
|
|
.option('accessToken', {
|
|
describe: 'NPM access token',
|
|
type: 'string',
|
|
})
|
|
.option('baseline', {
|
|
describe: 'Baseline tag or commit to compare against (for example `master`).',
|
|
type: 'string',
|
|
})
|
|
.option('skipLastCommitComparison', {
|
|
default: false,
|
|
describe:
|
|
'By default, the script exits when there are no changes in public packages in the latest commit. Setting this flag will skip this check and compare only against the baseline.',
|
|
type: 'boolean',
|
|
})
|
|
.option('yes', {
|
|
default: false,
|
|
describe: "If set, the script doesn't ask for confirmation before publishing packages",
|
|
type: 'boolean',
|
|
})
|
|
.option('ignore', {
|
|
describe: 'List of packages to ignore',
|
|
type: 'string',
|
|
array: true,
|
|
default: [],
|
|
});
|
|
},
|
|
run,
|
|
)
|
|
.help()
|
|
.strict(true)
|
|
.version(false)
|
|
.parse();
|