From: Eduardo San Martin Morote Date: Mon, 16 Aug 2021 15:34:09 +0000 (+0200) Subject: build: release script X-Git-Tag: @pinia/nuxt@0.0.1~35 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3fd22d28433e187318b06abb06e9f4fa4d28404b;p=thirdparty%2Fvuejs%2Fpinia.git build: release script --- diff --git a/scripts/release.mjs b/scripts/release.mjs new file mode 100644 index 00000000..ab39e5a8 --- /dev/null +++ b/scripts/release.mjs @@ -0,0 +1,325 @@ +import minimist from 'minimist' +import _fs from 'fs' +import { join, resolve, dirname } from 'path' +import { fileURLToPath } from 'url' +import chalk from 'chalk' +import semver from 'semver' +import enquirer from 'enquirer' +import execa from 'execa' +import pSeries from 'p-series' +import { globby } from 'globby' + +const { prompt } = enquirer +const fs = _fs.promises + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const args = minimist(process.argv.slice(2)) +let { skipBuild, tag, dry: isDryRun, skipCleanCheck: skipCleanGitCheck } = args +// TODO: remove on stable +tag = tag || 'next' + +// const preId = +// args.preid || +// (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0]) +const EXPECTED_BRANCH = 'v2' + +const incrementVersion = (increment) => + semver.inc(currentVersion, increment, preId) +const bin = (name) => resolve(__dirname, '../node_modules/.bin/' + name) +/** + * @param bin {string} + * @param args {string} + * @param opts {import('execa').CommonOptions} + * @returns + */ +const run = (bin, args, opts = {}) => + execa(bin, args, { stdio: 'inherit', ...opts }) +const dryRun = (bin, args, opts = {}) => + console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts) +const runIfNotDry = isDryRun ? dryRun : run +const getPkgRoot = (pkg) => resolve(__dirname, '../packages/' + pkg) +const step = (msg) => console.log(chalk.cyan(msg)) + +async function main() { + if (!skipCleanGitCheck) { + const isDirtyGit = !!( + await run('git', ['status', '--porcelain'], { stdio: 'pipe' }) + ).stdout + + if (isDirtyGit) { + console.log(chalk.red(`Git repo isn't clean.`)) + return + } + } + + const currentBranch = ( + await run('git', ['branch', '--show-current'], { stdio: 'pipe' }) + ).stdout + + if (currentBranch !== EXPECTED_BRANCH) { + console.log( + chalk.red( + `You should be on brach "${EXPECTED_BRANCH}" but are on "${currentBranch}"` + ) + ) + return + } + + const isOutdatedRE = new RegExp( + `\\W${EXPECTED_BRANCH}\\W.*(?:fast-forwardable|local out of date)`, + 'i' + ) + const isOutdatedGit = isOutdatedRE.test( + (await run('git', ['remote', 'show', 'origin'], { stdio: 'pipe' })).stdout + ) + + if (isOutdatedGit) { + console.log(chalk.red(`Git branch is not in sync with remote`)) + return + } + + const changedPackages = await getChangedPackages() + if (!changedPackages.length) { + console.log(chalk.red(`No packages have changed since last release`)) + return + } + + if (isDryRun) { + console.log('\n' + chalk.bold.blue('This is a dry run') + '\n\n') + } + + step( + `Ready to release ${changedPackages + .map(({ name }) => chalk.bold.white(name)) + .join(', ')}` + ) + + const pkgWithVersions = await pSeries( + changedPackages.map(({ name, path, pkg }) => async () => { + let { version } = pkg + + const prerelease = semver.prerelease(version) + const preId = prerelease && prerelease[0] + + const versionIncrements = [ + 'patch', + 'minor', + 'major', + ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : []), + ] + + const { release } = await prompt({ + type: 'select', + name: 'release', + message: `Select release type for ${chalk.bold.white(name)}`, + choices: versionIncrements + .map((i) => `${i}: ${name} (${semver.inc(version, i, preId)})`) + .concat(['custom']), + }) + + if (release === 'custom') { + version = ( + await prompt({ + type: 'input', + name: 'version', + message: `Input custom version (${chalk.bold.white(name)})`, + initial: version, + }) + ).version + } else { + version = release.match(/\((.*)\)/)[1] + } + + if (!semver.valid(version)) { + throw new Error(`invalid target version: ${version}`) + } + + return { name, path, version, pkg } + }) + ) + + const { yes } = await prompt({ + type: 'confirm', + name: 'yes', + message: `Releasing \n${pkgWithVersions + .map( + ({ name, version }) => + ` · ${chalk.white(name)}: ${chalk.yellow.bold('v' + version)}` + ) + .join('\n')}\nConfirm?`, + }) + + if (!yes) { + return + } + + step('\nUpdating versions in package.json files...') + await updateVersions(pkgWithVersions) + + step('\n Generating changelogs...') + for (const pkg of pkgWithVersions) { + await runIfNotDry(`yarn`, ['changelog'], { cwd: pkg.path }) + await runIfNotDry(`yarn`, ['prettier', '--write', 'CHANGELOG.md'], { + cwd: pkg.path, + }) + } + + step('\nBuilding all packages...') + if (!skipBuild && !isDryRun) { + await run('yarn', ['build']) + } else { + console.log(`(skipped)`) + } + + const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) + if (stdout) { + step('\nCommitting changes...') + await runIfNotDry('git', [ + 'add', + 'packages/*/CHANGELOG.md', + 'packages/*/package.json', + ]) + await runIfNotDry('git', [ + 'commit', + '-m', + `release: ${pkgWithVersions + .map(({ name, version }) => `${name}@${version}`) + .join(' ')}`, + ]) + } else { + console.log('No changes to commit.') + } + + step('\nCreating tags...') + let versionsToPush = [] + for (const pkg of pkgWithVersions) { + versionsToPush.push(`refs/tags/${pkg.name}@${pkg.version}`) + await runIfNotDry('git', ['tag', `${pkg.name}@${pkg.version}`]) + } + + step('\nPublishing packages...') + for (const pkg of pkgWithVersions) { + await publishPackage(pkg) + } + + step('\nPushing to Github...') + await runIfNotDry('git', ['push', 'origin', ...versionsToPush]) + await runIfNotDry('git', ['push']) +} + +/** + * + * @param packageList {{ name: string; path: string; version: string, pkg: any }} + */ +async function updateVersions(packageList) { + return Promise.all( + packageList.map(({ pkg, version, path, name }) => { + pkg.version = version + updateDeps(pkg, 'dependencies', packageList) + updateDeps(pkg, 'peerDependencies', packageList) + const content = JSON.stringify(pkg, null, 2) + '\n' + return isDryRun + ? dryRun('write', [name], content) + : fs.writeFile(join(path, 'package.json'), content) + }) + ) +} + +function updateDeps(pkg, depType, updatedPackages) { + const deps = pkg[depType] + if (!deps) return + Object.keys(deps).forEach((dep) => { + const updatedDep = updatedPackages.find((pkg) => pkg.name === dep) + if (dep) { + console.log( + chalk.yellow( + `${pkg.name} -> ${depType} -> ${dep}@~${updateDep.version}` + ) + ) + deps[dep] = '~' + updatedDep.version + } + }) +} + +async function publishPackage(pkg) { + step(`Publishing ${pkg.name}...`) + + try { + await runIfNotDry( + 'yarn', + [ + 'publish', + '--new-version', + pkg.version, + '--no-commit-hooks', + '--no-git-tag-version', + ...(tag ? ['--tag', tag] : []), + '--access', + 'public', + ], + { + cwd: pkg.path, + stdio: 'pipe', + } + ) + console.log( + chalk.green(`Successfully published ${pkg.name}@${pkg.version}`) + ) + } catch (e) { + if (e.stderr.match(/previously published/)) { + console.log(chalk.red(`Skipping already published: ${pkg.name}`)) + } else { + throw e + } + } +} + +/** + * Get the packages that have changed based on `lerna changed`. + * + * @returns {Promise<{ name: string; path: string; pkg: any; version: string }[]} + */ +async function getChangedPackages() { + const { stdout: lastTag } = await run( + 'git', + ['describe', '--tags', '--abbrev=0'], + { stdio: 'pipe' } + ) + const folders = await globby(join(__dirname, '../packages/*'), { + onlyFiles: false, + }) + + const pkgs = await Promise.all( + folders.map(async (folder) => { + if (!(await fs.lstat(folder)).isDirectory()) return null + + const pkg = JSON.parse(await fs.readFile(join(folder, 'package.json'))) + if (!pkg.private) { + const { stdout: hasChanges } = await run( + 'git', + ['diff', lastTag, '--', folder], + { stdio: 'pipe' } + ) + if (hasChanges) { + return { + path: folder, + name: pkg.name, + version: pkg.version, + pkg, + } + } else { + return null + } + } + }) + ) + + return pkgs.filter((p) => p) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +})