From 8013d90e43445b2c5fd40920b5c0dedea4f3d882 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 12 Feb 2022 19:14:31 +0800 Subject: [PATCH] workflow: separate version bumping and publishing on release (#6879) --- .github/workflows/publish.yml | 37 ++++ .github/workflows/release.yml | 79 -------- package.json | 2 + packages/plugin-vue-jsx/package.json | 4 - packages/plugin-vue/package.json | 3 +- scripts/publishCI.ts | 31 ++++ scripts/release.ts | 265 ++++++++------------------- scripts/releaseUtils.ts | 215 ++++++++++++++++++++++ 8 files changed, 359 insertions(+), 277 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yml create mode 100644 scripts/publishCI.ts create mode 100644 scripts/releaseUtils.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..f4df5af0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,37 @@ +name: Publish Package + +on: + push: + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + - "plugin-*" # Push events to matching plugin-*, i.e. plugin-(vue|vue-jsx|react|legacy)@1.0.0 + - "create-vite*" # # Push events to matching create-vite*, i.e. create-vite@1.0.0 + +jobs: + publish: + # prevents this action from running on forks + if: github.repository == 'vitejs/vite' + runs-on: ubuntu-latest + environment: Release + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 6 + + - name: Set node version to 16.x + uses: actions/setup-node@v2 + with: + node-version: 16.x + cache: "pnpm" + + - name: Install deps + run: pnpm install + + - name: Publish package + run: pnpm run ci-publish -- ${{ github.ref_name }} --dry + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b059c0bf..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - branch: - description: "branch" - required: true - type: string - default: "main" - package: - description: "package" - required: true - type: choice - options: - - vite - - plugin-legacy - - plugin-vue - - plugin-vue-jsx - - plugin-react - - create-vite - type: - description: "type" - required: true - type: choice - options: - - next - - stable - - minor-beta - - major-beta - - minor - - major - -jobs: - release: - # prevents this action from running on forks - if: github.repository == 'vitejs/vite' - name: Release - runs-on: ${{ matrix.os }} - environment: Release - strategy: - matrix: - # pseudo-matrix for convenience, NEVER use more than a single combination - node: [16] - os: [ubuntu-latest] - steps: - - name: checkout - uses: actions/checkout@v2 - with: - # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits - ref: ${{ github.event.inputs.branch }} - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - run: git config user.name vitebot - - run: git config user.email vitejs.bot@gmail.com - - run: npm i -g pnpm@6 - - run: npm i -g yarn # even if the repo is using pnpm, Vite still uses yarn v1 for publishing - - run: yarn config set registry https://registry.npmjs.org # Yarn's default registry proxy doesn't work in CI - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - cache-dependency-path: "**/pnpm-lock.yaml" - - name: install - run: pnpm install --frozen-lockfile --prefer-offline - - name: Creating .npmrc - run: | - cat << EOF > "$HOME/.npmrc" - //registry.npmjs.org/:_authToken=$NPM_TOKEN - EOF - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Release - run: pnpm --dir packages/${{ github.event.inputs.package }} release -- --quiet --type ${{ github.event.inputs.type }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 63c7e38d..72b7076e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "docs": "vitepress dev docs", "build-docs": "vitepress build docs", "serve-docs": "vitepress serve docs", + "release": "ts-node scripts/release.ts", + "ci-publish": "ts-node scripts/publishCI.ts", "build": "run-s build-vite build-plugin-vue build-plugin-react", "build-vite": "cd packages/vite && npm run build", "build-plugin-vue": "cd packages/plugin-vue && npm run build", diff --git a/packages/plugin-vue-jsx/package.json b/packages/plugin-vue-jsx/package.json index c26a343c..dff19a22 100644 --- a/packages/plugin-vue-jsx/package.json +++ b/packages/plugin-vue-jsx/package.json @@ -9,10 +9,6 @@ ], "main": "index.js", "types": "index.d.ts", - "scripts": { - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . --lerna-package plugin-vue-jsx", - "release": "ts-node ../../scripts/release.ts --skipBuild" - }, "engines": { "node": ">=12.0.0" }, diff --git a/packages/plugin-vue/package.json b/packages/plugin-vue/package.json index f54042da..ca8836d8 100644 --- a/packages/plugin-vue/package.json +++ b/packages/plugin-vue/package.json @@ -16,8 +16,7 @@ "build-bundle": "esbuild src/index.ts --bundle --platform=node --target=node12 --external:@vue/compiler-sfc --external:vue/compiler-sfc --external:vite --outfile=dist/index.js & npm run patch-dist", "patch-dist": "ts-node ../../scripts/patchEsbuildDist.ts dist/index.js vuePlugin", "build-types": "tsc -p . --emitDeclarationOnly --outDir temp && api-extractor run && rimraf temp", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . --lerna-package plugin-vue", - "release": "ts-node ../../scripts/release.ts" + "prepublishOnly": "npm run build" }, "engines": { "node": ">=12.0.0" diff --git a/scripts/publishCI.ts b/scripts/publishCI.ts new file mode 100644 index 00000000..7df0893a --- /dev/null +++ b/scripts/publishCI.ts @@ -0,0 +1,31 @@ +import { args, getPackageInfo, publishPackage, step } from './releaseUtils' + +async function main() { + const tag = args._[0] + + if (!tag) { + throw new Error('No tag specified') + } + + let pkgName = 'vite' + let version + + if (tag.includes('@')) [pkgName, version] = tag.split('@') + else version = tag + + if (version.startsWith('v')) version = version.slice(1) + + const { currentVersion, pkgDir } = getPackageInfo(pkgName) + if (currentVersion !== version) + throw new Error( + `Package version from tag "${version}" mismatches with current version "${currentVersion}"` + ) + + step('Publishing package...') + await publishPackage(pkgDir, version.includes('beta') ? 'beta' : undefined) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/scripts/release.ts b/scripts/release.ts index e24f5395..6ec929a1 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -1,134 +1,54 @@ -/** - * modified from https://github.com/vuejs/core/blob/master/scripts/release.js - */ -import colors from 'picocolors' -import type { ExecaChildProcess, Options as ExecaOptions } from 'execa' -import execa from 'execa' -import { readFileSync, writeFileSync } from 'fs' -import path from 'path' import prompts from 'prompts' -import type { ReleaseType } from 'semver' import semver from 'semver' +import colors from 'picocolors' +import { + args, + getPackageInfo, + getVersionChoices, + isDryRun, + logRecentCommits, + packages, + run, + runIfNotDry, + step, + updateTemplateVersions, + updateVersion +} from './releaseUtils' -const args = require('minimist')(process.argv.slice(2)) - -// For GitHub Actions use -// Regular release : release --type next --quiet -// Start beta : release --type (minor-beta|major-beta) --quiet -// Release from beta : release --type stable --quiet - -const pkgDir = process.cwd() -const pkgPath = path.resolve(pkgDir, 'package.json') -const pkg: { name: string; version: string } = require(pkgPath) -const pkgName = pkg.name.replace(/^@vitejs\//, '') -const currentVersion = pkg.version -const isDryRun: boolean = args.dry -const skipBuild: boolean = args.skipBuild - -const versionIncrements: ReleaseType[] = [ - 'patch', - 'minor', - 'major', - 'prepatch', - 'preminor', - 'premajor', - 'prerelease' -] - -const inc: (i: ReleaseType) => string = (i) => - semver.inc(currentVersion, i, 'beta')! - -type RunFn = ( - bin: string, - args: string[], - opts?: ExecaOptions -) => ExecaChildProcess - -const run: RunFn = (bin, args, opts = {}) => - execa(bin, args, { stdio: 'inherit', ...opts }) - -type DryRunFn = (bin: string, args: string[], opts?: any) => void +async function main(): Promise { + let targetVersion: string | undefined -const dryRun: DryRunFn = (bin, args, opts: any) => - console.log(colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts) + const { pkg }: { pkg: string } = await prompts({ + type: 'select', + name: 'pkg', + message: 'Select package', + choices: packages.map((i) => ({ value: i, title: i })) + }) -const runIfNotDry = isDryRun ? dryRun : run + if (!pkg) return -const step: (msg: string) => void = (msg) => console.log(colors.cyan(msg)) + await logRecentCommits(pkg) -async function main(): Promise { - let targetVersion: string | undefined = args._[0] + const { currentVersion, pkgName, pkgPath, pkgDir } = getPackageInfo(pkg) if (!targetVersion) { - const type: string | undefined = args.type - if (type) { - const currentBeta = currentVersion.includes('beta') - if (type === 'next') { - targetVersion = inc(currentBeta ? 'prerelease' : 'patch') - } else if (type === 'stable') { - // Out of beta - if (!currentBeta) { - throw new Error( - `Current version: ${currentVersion} isn't a beta, stable can't be used` - ) - } - targetVersion = inc('patch') - } else if (type === 'minor-beta') { - if (currentBeta) { - throw new Error( - `Current version: ${currentVersion} is already a beta, minor-beta can't be used` - ) - } - targetVersion = inc('preminor') - } else if (type === 'major-beta') { - if (currentBeta) { - throw new Error( - `Current version: ${currentVersion} is already a beta, major-beta can't be used` - ) - } - targetVersion = inc('premajor') - } else if (type === 'minor') { - if (currentBeta) { - throw new Error( - `Current version: ${currentVersion} is a beta, use stable to release it first` - ) - } - targetVersion = inc('minor') - } else if (type === 'major') { - if (currentBeta) { - throw new Error( - `Current version: ${currentVersion} is a beta, use stable to release it first` - ) - } - targetVersion = inc('major') - } else { - throw new Error( - `type: ${type} isn't a valid type. Use stable, minor-beta, major-beta, or next` - ) - } - } else { - // no explicit version or type, offer suggestions - const { release }: { release: string } = await prompts({ - type: 'select', - name: 'release', - message: 'Select release type', - choices: versionIncrements - .map((i) => `${i} (${inc(i)})`) - .concat(['custom']) - .map((i) => ({ value: i, title: i })) - }) + const { release }: { release: string } = await prompts({ + type: 'select', + name: 'release', + message: 'Select release type', + choices: getVersionChoices(currentVersion) + }) - if (release === 'custom') { - const res: { version: string } = await prompts({ - type: 'text', - name: 'version', - message: 'Input custom version', - initial: currentVersion - }) - targetVersion = res.version - } else { - targetVersion = release.match(/\((.*)\)/)![1] - } + if (release === 'custom') { + const res: { version: string } = await prompts({ + type: 'text', + name: 'version', + message: 'Input custom version', + initial: currentVersion + }) + targetVersion = res.version + } else { + targetVersion = release } } @@ -139,44 +59,37 @@ async function main(): Promise { const tag = pkgName === 'vite' ? `v${targetVersion}` : `${pkgName}@${targetVersion}` - if (!args.quiet) { - if (targetVersion.includes('beta') && !args.tag) { - const { tagBeta }: { tagBeta: boolean } = await prompts({ - type: 'confirm', - name: 'tagBeta', - message: `Publish under dist-tag "beta"?` - }) - - if (tagBeta) args.tag = 'beta' - } + if (targetVersion.includes('beta') && !args.tag) { + args.tag = 'beta' + } - const { yes }: { yes: boolean } = await prompts({ - type: 'confirm', - name: 'yes', - message: `Releasing ${tag}. Confirm?` - }) + const { yes }: { yes: boolean } = await prompts({ + type: 'confirm', + name: 'yes', + message: `Releasing ${colors.yellow(tag)} Confirm?` + }) - if (!yes) { - return - } - } else { - if (targetVersion.includes('beta') && !args.tag) { - args.tag = 'beta' - } + if (!yes) { + return } step('\nUpdating package version...') - updateVersion(targetVersion) - - step('\nBuilding package...') - if (!skipBuild && !isDryRun) { - await run('pnpm', ['run', 'build']) - } else { - console.log(`(skipped)`) - } + updateVersion(pkgPath, targetVersion) + if (pkgName === 'create-vite') updateTemplateVersions(targetVersion) step('\nGenerating changelog...') - await run('pnpm', ['run', 'changelog']) + const changelogArgs = [ + 'conventional-changelog', + '-p', + 'angular', + '-i', + 'CHANGELOG.md', + '-s', + '--commit-path', + '.' + ] + if (pkgName !== 'vite') changelogArgs.push('--lerna-package', 'plugin-vue') + await run('npx', changelogArgs, { cwd: pkgDir }) const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) if (stdout) { @@ -186,59 +99,27 @@ async function main(): Promise { await runIfNotDry('git', ['tag', tag]) } else { console.log('No changes to commit.') + return } - step('\nPublishing package...') - await publishPackage(targetVersion, runIfNotDry) - step('\nPushing to GitHub...') await runIfNotDry('git', ['push', 'origin', `refs/tags/${tag}`]) await runIfNotDry('git', ['push']) if (isDryRun) { console.log(`\nDry run finished - run git diff to see package changes.`) + } else { + console.log( + colors.green( + '\nPushed, publishing should starts shortly on CI.\nhttps://github.com/vitejs/vite/actions/workflows/publish.yml' + ) + ) } console.log() } -function updateVersion(version: string): void { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) - pkg.version = version - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') -} - -async function publishPackage( - version: string, - runIfNotDry: RunFn | DryRunFn -): Promise { - const publicArgs = [ - 'publish', - '--no-git-tag-version', - '--new-version', - version, - '--access', - 'public' - ] - if (args.tag) { - publicArgs.push(`--tag`, args.tag) - } - try { - // important: we still use Yarn 1 to publish since we rely on its specific - // behavior - await runIfNotDry('yarn', publicArgs, { - stdio: 'pipe' - }) - console.log(colors.green(`Successfully published ${pkgName}@${version}`)) - } catch (e: any) { - if (e.stderr.match(/previously published/)) { - console.log(colors.red(`Skipping already published: ${pkgName}`)) - } else { - throw e - } - } -} - main().catch((err) => { console.error(err) + process.exit(1) }) diff --git a/scripts/releaseUtils.ts b/scripts/releaseUtils.ts new file mode 100644 index 00000000..c2626a08 --- /dev/null +++ b/scripts/releaseUtils.ts @@ -0,0 +1,215 @@ +/** + * modified from https://github.com/vuejs/core/blob/master/scripts/release.js + */ +import colors from 'picocolors' +import type { Options as ExecaOptions } from 'execa' +import execa from 'execa' +import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs' +import path from 'path' +import type { ReleaseType } from 'semver' +import semver from 'semver' + +export const args = require('minimist')(process.argv.slice(2)) + +export const isDryRun = !!args.dry + +if (isDryRun) { + console.log(colors.inverse(colors.yellow(' DRY RUN '))) + console.log() +} + +export const packages = [ + 'vite', + 'create-vite', + 'plugin-legacy', + 'plugin-react', + 'plugin-vue', + 'plugin-vue-jsx' +] + +export const versionIncrements: ReleaseType[] = [ + 'patch', + 'minor', + 'major' + // 'prepatch', + // 'preminor', + // 'premajor', + // 'prerelease' +] + +export function getPackageInfo(pkgName: string) { + const pkgDir = path.resolve(__dirname, '../packages/' + pkgName) + + if (!existsSync(pkgDir)) { + throw new Error(`Package ${pkgName} not found`) + } + + const pkgPath = path.resolve(pkgDir, 'package.json') + const pkg: { + name: string + version: string + private?: boolean + } = require(pkgPath) + const currentVersion = pkg.version + + if (pkg.private) { + throw new Error(`Package ${pkgName} is private`) + } + + return { + pkg, + pkgName, + pkgDir, + pkgPath, + currentVersion + } +} + +export async function run( + bin: string, + args: string[], + opts: ExecaOptions = {} +) { + return execa(bin, args, { stdio: 'inherit', ...opts }) +} + +export async function dryRun( + bin: string, + args: string[], + opts?: ExecaOptions +) { + return console.log( + colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), + opts || '' + ) +} + +export const runIfNotDry = isDryRun ? dryRun : run + +export function step(msg: string) { + return console.log(colors.cyan(msg)) +} + +export function getVersionChoices(currentVersion: string) { + const currentBeta = currentVersion.includes('beta') + + const inc: (i: ReleaseType) => string = (i) => + semver.inc(currentVersion, i, 'beta')! + + const versionChoices = [ + { + title: 'next', + value: inc(currentBeta ? 'prerelease' : 'patch') + }, + ...(currentBeta + ? [ + { + title: 'stable', + value: inc('patch') + } + ] + : [ + { + title: 'beta-minor', + value: inc('preminor') + }, + { + title: 'beta-major', + value: inc('premajor') + }, + { + title: 'minor', + value: inc('minor') + }, + { + title: 'major', + value: inc('major') + } + ]), + { value: 'custom', title: 'custom' } + ].map((i) => { + i.title = `${i.title} (${i.value})` + return i + }) + + return versionChoices +} + +export function updateVersion(pkgPath: string, version: string): void { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) + pkg.version = version + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') +} + +export async function publishPackage( + pkdDir: string, + tag?: string +): Promise { + const publicArgs = ['publish', '--access', 'public'] + if (tag) { + publicArgs.push(`--tag`, tag) + } + await runIfNotDry('npm', publicArgs, { + stdio: 'pipe', + cwd: pkdDir + }) +} + +export async function getLatestTag(pkgName: string) { + const tags = (await run('git', ['tag'], { stdio: 'pipe' })).stdout + .split(/\n/) + .filter(Boolean) + const prefix = pkgName === 'vite' ? 'v' : `${pkgName}@` + return tags + .filter((tag) => tag.startsWith(prefix)) + .sort() + .reverse()[0] +} + +export async function logRecentCommits(pkgName: string) { + const tag = await getLatestTag(pkgName) + if (!tag) return + const sha = await run('git', ['rev-list', '-n', '1', tag], { + stdio: 'pipe' + }).then((res) => res.stdout.trim()) + console.log( + colors.bold( + `\n${colors.blue(`i`)} Commits of ${colors.green( + pkgName + )} since ${colors.green(tag)} ${colors.gray(`(${sha.slice(0, 5)})`)}` + ) + ) + await run( + 'git', + [ + '--no-pager', + 'log', + `${sha}..HEAD`, + '--oneline', + '--', + `packages/${pkgName}` + ], + { stdio: 'inherit' } + ) + console.log() +} + +export async function updateTemplateVersions(version: string) { + if (/beta|alpha|rc/.test(version)) return + + const dir = path.resolve(__dirname, '../packages/create-vite') + + const templates = readdirSync(dir).filter((dir) => + dir.startsWith('template-') + ) + for (const template of templates) { + const pkgPath = path.join(dir, template, `package.json`) + const pkg = require(pkgPath) + pkg.devDependencies.vite = `^` + version + if (template.startsWith('template-vue')) { + pkg.devDependencies['@vitejs/plugin-vue'] = + `^` + require('../packages/plugin-vue/package.json').version + } + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') + } +}