workflow: separate version bumping and publishing on release (#6879)

This commit is contained in:
Anthony Fu 2022-02-12 19:14:31 +08:00 committed by GitHub
parent 6ea6e08d23
commit fe8ef39eb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 364 additions and 315 deletions

37
.github/workflows/publish.yml vendored Normal file
View File

@ -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 }}

View File

@ -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 }}

View File

@ -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",

View File

@ -12,10 +12,6 @@
"template-*"
],
"main": "index.js",
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . --lerna-package create-vite",
"release": "ts-node updateVersions && ts-node ../../scripts/release.ts --skipBuild"
},
"engines": {
"node": ">=12.0.0"
},

View File

@ -1,23 +0,0 @@
import { readdirSync, writeFileSync } from 'fs'
import { join } from 'path'
const latestVersion = require('../vite/package.json').version
const isLatestPreRelease = /beta|alpha|rc/.test(latestVersion)
;(async () => {
const templates = readdirSync(__dirname).filter((dir) =>
dir.startsWith('template-')
)
for (const template of templates) {
const pkgPath = join(__dirname, template, `package.json`)
const pkg = require(pkgPath)
if (!isLatestPreRelease) {
pkg.devDependencies.vite = `^` + latestVersion
}
if (template.startsWith('template-vue')) {
pkg.devDependencies['@vitejs/plugin-vue'] =
`^` + require('../plugin-vue/package.json').version
}
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
})()

View File

@ -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-legacy",
"release": "ts-node ../../scripts/release.ts --skipBuild"
},
"engines": {
"node": ">=12.0.0"
},

View File

@ -18,8 +18,7 @@
"build-bundle": "esbuild src/index.ts --bundle --platform=node --target=node12 --external:@babel/* --external:@rollup/* --external:resolve --external:react-refresh/* --outfile=dist/index.js && npm run patch-dist",
"patch-dist": "ts-node ../../scripts/patchEsbuildDist.ts dist/index.js viteReact",
"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-react",
"release": "ts-node ../../scripts/release.ts"
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=12.0.0"

View File

@ -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"
},

View File

@ -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"

View File

@ -39,8 +39,7 @@
"roll-types": "api-extractor run && rimraf temp",
"lint": "eslint --ext .ts src/**",
"format": "prettier --write --parser typescript \"src/**/*.ts\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .",
"release": "ts-node ../../scripts/release.ts"
"prepublishOnly": "npm run build"
},
"//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!",
"dependencies": {

31
scripts/publishCI.ts Normal file
View File

@ -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)
})

View File

@ -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'
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<string>
) => ExecaChildProcess<string>
const run: RunFn = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
type DryRunFn = (bin: string, args: string[], opts?: any) => void
const dryRun: DryRunFn = (bin, args, opts: any) =>
console.log(colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
const runIfNotDry = isDryRun ? dryRun : run
const step: (msg: string) => void = (msg) => console.log(colors.cyan(msg))
import colors from 'picocolors'
import {
args,
getPackageInfo,
getVersionChoices,
isDryRun,
logRecentCommits,
packages,
run,
runIfNotDry,
step,
updateTemplateVersions,
updateVersion
} from './releaseUtils'
async function main(): Promise<void> {
let targetVersion: string | undefined = args._[0]
let targetVersion: string | undefined
const { pkg }: { pkg: string } = await prompts({
type: 'select',
name: 'pkg',
message: 'Select package',
choices: packages.map((i) => ({ value: i, title: i }))
})
if (!pkg) return
await logRecentCommits(pkg)
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<void> {
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 (targetVersion.includes('beta') && !args.tag) {
args.tag = 'beta'
}
if (tagBeta) args.tag = 'beta'
}
const { yes }: { yes: boolean } = await prompts({
type: 'confirm',
name: 'yes',
message: `Releasing ${colors.yellow(tag)} Confirm?`
})
const { yes }: { yes: boolean } = await prompts({
type: 'confirm',
name: 'yes',
message: `Releasing ${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<void> {
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<void> {
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)
})

215
scripts/releaseUtils.ts Normal file
View File

@ -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<string> = {}
) {
return execa(bin, args, { stdio: 'inherit', ...opts })
}
export async function dryRun(
bin: string,
args: string[],
opts?: ExecaOptions<string>
) {
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<void> {
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')
}
}