From: Eduardo San Martin Morote Date: Fri, 10 Jun 2022 14:29:14 +0000 (+0200) Subject: build: add new release script X-Git-Tag: v4.1.0~34 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ed238db64b2a605f785ee4e2f847a6788c06a2ea;p=thirdparty%2Fvuejs%2Frouter.git build: add new release script --- diff --git a/.gitignore b/.gitignore index bc9ecae6..bdf5aad1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ yalc.lock .yalc local.log _selenium-server.log +packages/*/LICENSE diff --git a/packages/router/LICENSE b/LICENSE similarity index 95% rename from packages/router/LICENSE rename to LICENSE index 2d297f23..0c77562b 100644 --- a/packages/router/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Eduardo San Martin Morote +Copyright (c) 2019-present Eduardo San Martin Morote Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 5b13861c..4a4d235f 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,14 @@ "test": "pnpm run -r test" }, "devDependencies": { - "chalk": "^4.1.0", + "chalk": "^4.1.2", + "enquirer": "^2.3.6", + "execa": "^6.1.0", + "globby": "^13.1.1", "lint-staged": "^13.0.0", + "minimist": "^1.2.6", + "p-series": "^3.0.0", + "semver": "^7.3.7", "yorkie": "^2.0.0" }, "gitHooks": { diff --git a/packages/router/package.json b/packages/router/package.json index 5b21dd22..d40bf673 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -48,7 +48,6 @@ ], "scripts": { "dev": "vite --config playground/vite.config.js", - "release": "bash scripts/release.sh", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "build": "rimraf dist && rollup -c rollup.config.js", "build:dts": "api-extractor run --local --verbose && tail -n +9 src/globalExtensions.ts >> dist/vue-router.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 683f2928..b03cb8cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,12 +4,24 @@ importers: .: specifiers: - chalk: ^4.1.0 + chalk: ^4.1.2 + enquirer: ^2.3.6 + execa: ^6.1.0 + globby: ^13.1.1 lint-staged: ^13.0.0 + minimist: ^1.2.6 + p-series: ^3.0.0 + semver: ^7.3.7 yorkie: ^2.0.0 devDependencies: chalk: 4.1.2 - lint-staged: 13.0.0 + enquirer: 2.3.6 + execa: 6.1.0 + globby: 13.1.1 + lint-staged: 13.0.0_enquirer@2.3.6 + minimist: 1.2.6 + p-series: 3.0.0 + semver: 7.3.7 yorkie: 2.0.0 packages/docs: @@ -2443,6 +2455,13 @@ packages: once: 1.4.0 dev: true + /enquirer/2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.1 + dev: true + /envinfo/7.8.1: resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} engines: {node: '>=4'} @@ -3102,6 +3121,17 @@ packages: slash: 3.0.0 dev: true + /globby/13.1.1: + resolution: {integrity: sha512-XMzoDZbGZ37tufiv7g0N4F/zp3zkwdFtVbV3EHsVl1KQr4RPLfNoT068/97RPshz2J5xYNEjLKKBKaGHifBd3Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.2.11 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + /got/11.8.2: resolution: {integrity: sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==} engines: {node: '>=10.19.0'} @@ -4174,7 +4204,7 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /lint-staged/13.0.0: + /lint-staged/13.0.0_enquirer@2.3.6: resolution: {integrity: sha512-vWban5utFt78VZohbosUxNIa46KKJ+KOQTDWTQ8oSl1DLEEVl9zhUtaQbiiydAmx+h2wKJK2d0+iMaRmknuWRQ==} engines: {node: ^14.13.1 || >=16.0.0} hasBin: true @@ -4185,7 +4215,7 @@ packages: debug: 4.3.4 execa: 6.1.0 lilconfig: 2.0.5 - listr2: 4.0.5 + listr2: 4.0.5_enquirer@2.3.6 micromatch: 4.0.5 normalize-path: 3.0.0 object-inspect: 1.12.2 @@ -4197,7 +4227,7 @@ packages: - supports-color dev: true - /listr2/4.0.5: + /listr2/4.0.5_enquirer@2.3.6: resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} engines: {node: '>=12'} peerDependencies: @@ -4208,6 +4238,7 @@ packages: dependencies: cli-truncate: 2.1.0 colorette: 2.0.17 + enquirer: 2.3.6 log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.3.0 @@ -4854,6 +4885,11 @@ packages: aggregate-error: 3.1.0 dev: true + /p-series/3.0.0: + resolution: {integrity: sha512-geaabIwiqy+jN4vuJROl1rpMJT/myHAMAfdubPQGJT3Grr8td+ogWvTk2qLsNlhYXcoZZAfl01pfq7lK3/gYKQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /p-try/1.0.0: resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} engines: {node: '>=4'} @@ -5412,6 +5448,11 @@ packages: engines: {node: '>=8'} dev: true + /slash/4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + /slice-ansi/3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} diff --git a/scripts/release.mjs b/scripts/release.mjs new file mode 100644 index 00000000..d77723e3 --- /dev/null +++ b/scripts/release.mjs @@ -0,0 +1,385 @@ +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: optionTag, + dry: isDryRun, + skipCleanCheck: skipCleanGitCheck, +} = args + +// const preId = +// args.preid || +// (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0]) +const EXPECTED_BRANCH = 'main' + +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 branch "${EXPECTED_BRANCH}" but are on "${currentBranch}"` + ) + ) + return + } + } else { + console.log(chalk.bold.white(`Skipping git checks...`)) + } + + if (!skipCleanGitCheck) { + 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') + } + + // NOTE: I'm unsure if this would mess up the changelog + // const { pickedPackages } = await prompt({ + // type: 'multiselect', + // name: 'pickedPackages', + // messages: 'What packages do you want to release?', + // choices: changedPackages.map((pkg) => pkg.name), + // }) + + const packagesToRelease = changedPackages + // const packagesToRelease = changedPackages.filter((pkg) => + // pickedPackages.includes(pkg.name) + // ) + + step( + `Ready to release ${packagesToRelease + .map(({ name }) => chalk.bold.white(name)) + .join(', ')}` + ) + + const pkgWithVersions = await pSeries( + packagesToRelease.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: isReleaseConfirmed } = 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 (!isReleaseConfirmed) { + return + } + + step('\nUpdating versions in package.json files...') + await updateVersions(pkgWithVersions) + + step('\nGenerating changelogs...') + for (const pkg of pkgWithVersions) { + step(` -> ${pkg.name} (${pkg.path})`) + await runIfNotDry(`pnpm`, ['run', 'changelog'], { cwd: pkg.path }) + await runIfNotDry(`pnpm`, ['exec', 'prettier', '--write', 'CHANGELOG.md'], { + cwd: pkg.path, + }) + await fs.copyFile( + resolve(__dirname, '../LICENSE'), + resolve(pkg.path, 'LICENSE') + ) + } + + const { yes: isChangelogCorrect } = await prompt({ + type: 'confirm', + name: 'yes', + message: 'Are the changelogs correct?', + }) + + if (!isChangelogCorrect) { + return + } + + step('\nBuilding all packages...') + if (!skipBuild && !isDryRun) { + await run('pnpm', ['run', 'build']) + await run('pnpm', ['run', 'build:dts']) + } 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], { + dependencies: pkg.dependencies, + peerDependencies: pkg.peerDependencies, + }) + : fs.writeFile(join(path, 'package.json'), content) + }) + ) +} + +function updateDeps(pkg, depType, updatedPackages) { + const deps = pkg[depType] + if (!deps) return + step(`Updating ${chalk.bold(depType)} for ${chalk.bold.white(pkg.name)}...`) + Object.keys(deps).forEach(dep => { + const updatedDep = updatedPackages.find(pkg => pkg.name === dep) + // avoid updated peer deps that are external like @vue/devtools-api + if (dep && updatedDep) { + console.log( + chalk.yellow( + `${pkg.name} -> ${depType} -> ${dep}@~${updatedDep.version}` + ) + ) + deps[dep] = '>=' + updatedDep.version + } + }) +} + +async function publishPackage(pkg) { + step(`Publishing ${pkg.name}...`) + + try { + await runIfNotDry( + 'pnpm', + [ + 'publish', + ...(optionTag ? ['--tag', optionTag] : []), + '--access', + 'public', + '--publish-branch', + EXPECTED_BRANCH, + ], + { + 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` but without lerna. + * + * @returns {Promise<{ name: string; path: string; pkg: any; version: string }[]} + */ +async function getChangedPackages() { + let lastTag + + try { + const { stdout } = await run('git', ['describe', '--tags', '--abbrev=0'], { + stdio: 'pipe', + }) + lastTag = stdout + } catch (error) { + // maybe there are no tags + console.error(`Couldn't get the last tag, using first commit...`) + const { stdout } = await run( + 'git', + ['rev-list', '--max-parents=0', 'HEAD'], + { stdio: 'pipe' } + ) + lastTag = stdout + } + 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, + '--', + // apparently {src,package.json} doesn't work + join(folder, 'src'), + join(folder, 'package.json'), + ], + { 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) +}) diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100644 index f3bea32f..00000000 --- a/scripts/release.sh +++ /dev/null @@ -1,39 +0,0 @@ -set -e -echo "Current version:" $(grep version package.json | sed -E 's/^.*"([0-9][^"]+)".*$/\1/') -echo "Enter version e.g., 4.0.1: " -read VERSION - -read -p "Releasing v$VERSION - are you sure? (y/n)" -n 1 -r -echo # (optional) move to a new line -if [[ $REPLY =~ ^[Yy]$ ]] -then - echo "Releasing v$VERSION ..." - - # clear existing ts cache - rm -rf node_modules/.rts2_cache - - # generate the version so that the changelog can be generated too - yarn version --no-git-tag-version --no-commit-hooks --new-version $VERSION - - yarn run build - yarn run build:dts - yarn run test:dts - - # changelog - yarn run changelog - yarn prettier --write CHANGELOG.md - echo "Please check the git history and the changelog and press enter" - read OKAY - - # commit and tag - git add CHANGELOG.md package.json - git commit -m "release: v$VERSION" - git tag "v$VERSION" - - # commit - yarn publish --tag latest --new-version "$VERSION" --no-commit-hooks --no-git-tag-version - - # publish - git push origin refs/tags/v$VERSION - git push -fi