ci: fixed versioning (#5214)

* chore(deps): add lerna-lite

* feat: update-1

* feat(db-mongodb): update 2

* chore: lerna init

* chore: add version option to lerna config

* chore(ci): add gh usernames to changelog and user root package.json for version

* chore(ci): whitelist poc branches

* chore(ci): add contributors section

* chore(ci): use turbo for prepublishOnly scripts, enable caching

* chore(deps): update turborepo, add execa

* feat(plugin-stripe): adjust type import

* chore: remove lerna-lite

* chore(ci): new and improved release script for fixed versioning

* chore: remove unused lerna-lite packages

* chore: sync root package.json version

* chore: remove remnants of bundler packages

* chore(plugin-seo): update packagea.json from main, disable build

* chore: disable turbo caching

* chore(ci): update release script

* chore: sync pnpm-lock.yaml

* chore: ci cleanup
This commit is contained in:
Elliot DeNolf
2024-02-29 16:01:51 -05:00
committed by GitHub
parent 7188cfe85a
commit 6ff42d1627
11 changed files with 316 additions and 239 deletions

View File

@@ -1,32 +1,17 @@
import path from 'path'
import fse from 'fs-extra'
import chalk from 'chalk'
import chalkTemplate from 'chalk-template'
import simpleGit from 'simple-git'
import path from 'path'
const git = simpleGit()
const packagesDir = path.resolve(__dirname, '../../packages')
export type PackageDetails = {
commitMessage: string
name: string
newCommits: number
shortName: string
packagePath: string
prevGitTag: string
prevGitTagHash: string
publishedVersion: string
publishDate: string
version: string
}
export const getPackageDetails = async (pkg?: string): Promise<PackageDetails[]> => {
let packageDirs: string[] = []
if (pkg) {
packageDirs = fse.readdirSync(packagesDir).filter((d) => d === pkg)
} else {
packageDirs = fse.readdirSync(packagesDir).filter((d) => d !== 'eslint-config-payload')
}
export const getPackageDetails = async (): Promise<PackageDetails[]> => {
const packageDirs = fse.readdirSync(packagesDir).filter((d) => d !== 'eslint-config-payload')
const packageDetails = await Promise.all(
packageDirs.map(async (dirName) => {
@@ -34,33 +19,10 @@ export const getPackageDetails = async (pkg?: string): Promise<PackageDetails[]>
const isPublic = packageJson.private !== true
if (!isPublic) return null
// Get published version from npm
const json = await fetch(`https://registry.npmjs.org/${packageJson.name}`).then((res) =>
res.json(),
)
const publishedVersion = json?.['dist-tags']?.latest
const publishDate = json?.time?.[publishedVersion]
const prevGitTag =
dirName === 'payload' ? `v${packageJson.version}` : `${dirName}/${packageJson.version}`
const prevGitTagHash = await git.revparse(prevGitTag)
const newCommits = await git.log({
from: prevGitTagHash,
file: `packages/${dirName}`,
})
return {
commitMessage: newCommits.latest?.message ?? '',
name: packageJson.name as string,
newCommits: newCommits.total,
shortName: dirName,
packagePath: `packages/${dirName}`,
prevGitTag,
prevGitTagHash,
publishedVersion,
publishDate,
packagePath: path.resolve(packagesDir, dirName),
version: packageJson.version,
}
}),
@@ -68,30 +30,3 @@ export const getPackageDetails = async (pkg?: string): Promise<PackageDetails[]>
return packageDetails.filter((p): p is Exclude<typeof p, null> => p !== null)
}
export const showPackageDetails = (details: PackageDetails[]) => {
console.log(chalkTemplate`
{bold Packages:}
${details
.map((p) => {
const name = p?.newCommits
? chalk.bold.green(p?.shortName.padEnd(28))
: chalk.dim(p?.shortName.padEnd(28))
const publishData = `${p?.publishedVersion.padEnd(8)}${p?.publishDate.split('T')[0]}`
const newCommits = p?.newCommits ? chalk.bold.green(`${p?.newCommits} `) : ' '
const commitMessage = p?.commitMessage
? chalk.dim(
p.commitMessage.length < 57
? p.commitMessage
: p.commitMessage.substring(0, 60).concat('...'),
)
: ''
return ` ${name}${newCommits}${publishData} ${commitMessage}`
})
.join('\n')}
`)
}

View File

@@ -1,22 +1,52 @@
import fse from 'fs-extra'
import path from 'path'
import { ExecSyncOptions, execSync } from 'child_process'
import chalk from 'chalk'
import prompts from 'prompts'
import minimist from 'minimist'
import chalkTemplate from 'chalk-template'
import { PackageDetails, getPackageDetails, showPackageDetails } from './lib/getPackageDetails'
import { ExecSyncOptions, execSync } from 'child_process'
import execa from 'execa'
import fse from 'fs-extra'
import minimist from 'minimist'
import path from 'path'
import prompts from 'prompts'
import semver from 'semver'
import { updateChangelog } from './utils/updateChangelog'
import simpleGit from 'simple-git'
import { getPackageDetails } from './lib/getPackageDetails'
import { updateChangelog } from './utils/updateChangelog'
const git = simpleGit(path.resolve(__dirname, '..'))
const rootPath = path.resolve(__dirname, '..')
const git = simpleGit(rootPath)
const execOpts: ExecSyncOptions = { stdio: 'inherit' }
const args = minimist(process.argv.slice(2))
const { tag = 'latest', bump = 'patch', 'dry-run': dryRun = true, changelog = false } = args
const logPrefix = dryRun ? chalk.bold.magenta('[dry-run] >') : ''
const cmdRunner = (dryRun: boolean) => (cmd: string, execOpts: ExecSyncOptions) => {
if (dryRun) {
console.log(logPrefix, cmd)
} else {
execSync(cmd, execOpts)
}
}
const cmdRunnerAsync =
(dryRun: boolean) => async (cmd: string, args: string[], options?: execa.Options) => {
if (dryRun) {
console.log(logPrefix, cmd, args.join(' '))
return { exitCode: 0 }
} else {
return await execa(cmd, args, options ?? { stdio: 'inherit' })
}
}
async function main() {
const { tag = 'latest', bump = 'patch', pkg } = args
if (dryRun) {
console.log(chalk.bold.yellow(chalk.bold.magenta('\n 👀 Dry run mode enabled')))
}
const runCmd = cmdRunner(dryRun)
const runCmdAsync = cmdRunnerAsync(dryRun)
if (!semver.RELEASE_TYPES.includes(bump)) {
abort(`Invalid bump type: ${bump}.\n\nMust be one of: ${semver.RELEASE_TYPES.join(', ')}`)
@@ -26,151 +56,151 @@ async function main() {
abort(`Prerelease bumps must have tag: beta or canary`)
}
const packageDetails = await getPackageDetails(pkg)
showPackageDetails(packageDetails)
const monorepoVersion = fse.readJSONSync('package.json')?.version
let packagesToRelease: string[] = []
if (packageDetails.length > 1 && !pkg) {
;({ packagesToRelease } = (await prompts({
type: 'multiselect',
name: 'packagesToRelease',
message: 'Select packages to release',
instructions: 'Space to select. Enter to submit.',
choices: packageDetails.map((p) => {
const title = p?.newCommits ? chalk.bold.green(p?.shortName) : p?.shortName
return {
title,
value: p.shortName,
}
}),
})) as { packagesToRelease: string[] })
if (!packagesToRelease) {
abort()
}
if (packagesToRelease.length === 0) {
abort('Please specify a package to publish')
}
if (packagesToRelease.find((p) => p === 'payload' && packagesToRelease.length > 1)) {
abort('Cannot publish payload with other packages. Release Payload first.')
}
} else {
packagesToRelease = [packageDetails[0].shortName]
if (!monorepoVersion) {
throw new Error('Could not find version in package.json')
}
const packageMap = packageDetails.reduce(
(acc, p) => {
acc[p.shortName] = p
return acc
},
{} as Record<string, PackageDetails>,
)
const lastTag = (await git.tags()).all.reverse().filter((t) => t.startsWith('v'))?.[0]
if (monorepoVersion !== lastTag.replace('v', '')) {
throw new Error(
`Version in package.json (${monorepoVersion}) does not match last tag (${lastTag})`,
)
}
const nextReleaseVersion = semver.inc(monorepoVersion, bump, undefined, tag) as string
const packageDetails = await getPackageDetails()
console.log(chalkTemplate`
{bold.green Publishing packages:}
{bold Version: ${monorepoVersion} => {green ${nextReleaseVersion}}}
{bold.yellow Bump: ${bump}}
{bold.yellow Tag: ${tag}}
${packagesToRelease
{bold.green Changes (${packageDetails.length} packages):}
${packageDetails
.map((p) => {
const { shortName, version } = packageMap[p]
return ` ${shortName.padEnd(24)} ${version} -> ${semver.inc(version, bump, tag)}`
return ` - ${p.name.padEnd(32)} ${p.version} => ${chalk.green(nextReleaseVersion)}`
})
.join('\n')}
`)
const confirmPublish = await confirm(`Publish ${packagesToRelease.length} package(s)?`)
const confirmPublish = await confirm('Are you sure your want to create these versions?')
if (!confirmPublish) {
abort()
}
const results: { name: string; success: boolean }[] = []
for (const pkg of packagesToRelease) {
const { packagePath, shortName, name: registryName } = packageMap[pkg]
try {
console.log(chalk.bold(`\n\n🚀 Publishing ${shortName}...\n\n`))
let npmVersionCmd = `npm --no-git-tag-version --prefix ${packagePath} version ${bump}`
if (tag !== 'latest') {
npmVersionCmd += ` --preid ${tag}`
}
execSync(npmVersionCmd, execOpts)
const packageObj = await fse.readJson(`${packagePath}/package.json`)
const newVersion = packageObj.version
if (pkg === 'payload') {
const shouldUpdateChangelog = await confirm(`🧑‍💻 Update Changelog?`)
if (shouldUpdateChangelog) {
updateChangelog({ pkg: packageMap[pkg], bump })
}
}
const tagName = `${shortName}/${newVersion}`
const shouldCommit = await confirm(`🧑‍💻 Commit Release?`)
if (shouldCommit) {
if (pkg === 'payload') {
execSync(`git add CHANGELOG.md`, execOpts)
}
execSync(`git add ${packagePath}/package.json`, execOpts)
execSync(`git commit -m "chore(release): ${tagName} [skip ci]" `, execOpts)
}
const shouldTag = await confirm(`🏷️ Tag ${tagName}?`)
if (shouldTag) {
execSync(`git tag -a ${tagName} -m "${tagName}"`, execOpts)
if (pkg === 'payload') {
execSync(`git tag -a v${newVersion} -m "v${newVersion}"`, execOpts)
}
}
let publishCmd = `pnpm publish -C ${packagePath} --no-git-checks`
if (tag !== 'latest') {
publishCmd += ` --tag ${tag}`
}
const shouldPublish = await confirm(`🚢 Publish ${registryName}${chalk.yellow('@' + tag)}?`)
if (shouldPublish) {
execSync(publishCmd, execOpts)
}
results.push({ name: shortName, success: true })
} catch (error) {
console.error(chalk.bold.red(`ERROR: ${error.message}`))
results.push({ name: shortName, success: false })
}
// Prebuild all packages
header(`\n🔨 Prebuilding all packages...`)
const buildResult = await execa('pnpm', ['build:all'], {
cwd: rootPath,
// stdio: ['ignore', 'ignore', 'pipe'],
stdio: 'inherit',
})
// const buildResult = execSync('pnpm build:all', execOpts)
if (buildResult.exitCode !== 0) {
console.error(chalk.bold.red('Build failed'))
console.log(buildResult.stderr)
abort('Build failed')
}
// Update changelog
if (changelog) {
header(`${logPrefix}📝 Updating changelog...`)
await updateChangelog({ newVersion: nextReleaseVersion, dryRun })
} else {
console.log(chalk.bold.yellow('📝 Skipping changelog update'))
}
// Increment all package versions
header(`${logPrefix}📦 Updating package.json versions...`)
await Promise.all(
packageDetails.map(async (pkg) => {
const packageJson = await fse.readJSON(`${pkg.packagePath}/package.json`)
packageJson.version = nextReleaseVersion
if (!dryRun) {
await fse.writeJSON(`${pkg.packagePath}/package.json`, packageJson, { spaces: 2 })
}
}),
)
// Set version in root package.json
header(`${logPrefix}📦 Updating root package.json...`)
const rootPackageJsonPath = path.resolve(__dirname, '../package.json')
const rootPackageJson = await fse.readJSON(rootPackageJsonPath)
rootPackageJson.version = nextReleaseVersion
if (!dryRun) {
await fse.writeJSON(rootPackageJsonPath, rootPackageJson, { spaces: 2 })
}
// Commit
header(`🧑‍💻 Committing changes...`)
// Commit all staged changes
runCmd(`git add CHANGELOG.md packages package.json`, execOpts)
runCmd(`git commit -m "chore(release): v${nextReleaseVersion} [skip ci]"`, execOpts)
// Tag
header(`🏷️ Tagging release v${nextReleaseVersion}`)
runCmd(`git tag -a v${nextReleaseVersion} -m "v${nextReleaseVersion}"`, execOpts)
// Publish
const results: { name: string; success: boolean }[] = await Promise.all(
packageDetails.map(async (pkg) => {
try {
console.log(logPrefix, chalk.bold(`🚀 ${pkg.name} publishing...`))
const cmdArgs = [
'publish',
'-C',
pkg.packagePath,
'--no-git-checks',
'--tag',
tag,
'--dry-run', // TODO: Use dryRun var
]
const { exitCode } = await execa('pnpm', cmdArgs, {
cwd: rootPath,
// stdio: ['ignore', 'ignore', 'pipe'],
stdio: 'inherit',
})
if (exitCode !== 0) {
console.log(chalk.bold.red(`\n\np❌ ${pkg.name} ERROR: pnpm publish failed\n\n`))
return { name: pkg.name, success: false }
}
console.log(chalk.green(`${pkg.name} published`))
return { name: pkg.name, success: true }
} catch (error) {
console.error(chalk.bold.red(`\n\np❌ ${pkg.name} ERROR: ${error.message}\n\n`))
return { name: pkg.name, success: false }
}
}),
)
console.log(chalkTemplate`
{bold.green Results:}
${results
.map(({ name, success }) => ` ${success ? chalk.bold.green('✔') : chalk.bold.red('✘')} ${name}`)
.map(
({ name, success }) => ` ${success ? chalk.bold.green('✅') : chalk.bold.red('❌')} ${name}`,
)
.join('\n')}
`)
// Show unpushed commits and tags
execSync(
`git log --oneline $(git rev-parse --abbrev-ref --symbolic-full-name @{u})..HEAD`,
execOpts,
)
// TODO: Push commit and tag
// const push = await confirm(`Push commits and tags?`)
// if (push) {
// header(`Pushing commits and tags...`)
// execSync(`git push --follow-tags`, execOpts)
// }
console.log('\n')
const push = await confirm(`Push commits and tags?`)
if (push) {
console.log(chalk.bold(`\n\nPushing commits and tags...\n\n`))
execSync(`git push --follow-tags`, execOpts)
}
console.log(chalk.bold.green(`\n\nDone!\n\n`))
header('🎉 Done!')
}
main().catch((error) => {
@@ -200,3 +230,7 @@ async function confirm(message: string): Promise<boolean> {
return confirm
}
async function header(message: string) {
console.log(chalk.bold.green(`${message}\n`))
}

View File

@@ -1,39 +1,82 @@
import addStream from 'add-stream'
import { ExecSyncOptions } from 'child_process'
import conventionalChangelog from 'conventional-changelog'
import { default as getConventionalPreset } from 'conventional-changelog-conventionalcommits'
import { GitRawCommitsOptions, ParserOptions, WriterOptions } from 'conventional-changelog-core'
import fse, { createReadStream, createWriteStream } from 'fs-extra'
import minimist from 'minimist'
import semver, { ReleaseType } from 'semver'
import tempfile from 'tempfile'
import { PackageDetails } from '../lib/getPackageDetails'
import { Octokit } from '@octokit/core'
import simpleGit from 'simple-git'
import { once } from 'events'
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
const git = simpleGit()
type Args = {
pkg: PackageDetails
bump: ReleaseType
newVersion: string
dryRun?: boolean
}
export const updateChangelog = ({ pkg, bump }: Args) => {
// Prefix to find prev tag
const tagPrefix = pkg.shortName === 'payload' ? 'v' : pkg.prevGitTag.split('/')[0] + '/'
export const updateChangelog = async ({ newVersion, dryRun }: Args) => {
const monorepoVersion = fse.readJSONSync('package.json')?.version
if (!monorepoVersion) {
throw new Error('Could not find version in package.json')
}
const lastTag = (await git.tags()).all.reverse().filter((t) => t.startsWith('v'))?.[0]
if (monorepoVersion !== lastTag.replace('v', '')) {
throw new Error(
`Version in package.json (${monorepoVersion}) does not match last tag (${lastTag})`,
)
}
// Load conventional commits preset and modify it
const conventionalPreset = (await getConventionalPreset()) as {
gitRawCommitsOpts: GitRawCommitsOptions
parserOpts: ParserOptions
writerOpts: WriterOptions
recommmendBumpOpts: unknown
conventionalChangelog: unknown
}
// Unbold scope
conventionalPreset.writerOpts.commitPartial =
conventionalPreset.writerOpts?.commitPartial?.replace('**{{scope}}:**', '{{scope}}:')
// Add footer to end of main template
conventionalPreset.writerOpts.mainTemplate = conventionalPreset.writerOpts?.mainTemplate?.replace(
/\n*$/,
'{{footer}}\n',
)
// Fetch commits from last tag to HEAD
const credits = await createContributorSection(lastTag)
// Add Credits to footer
conventionalPreset.writerOpts.finalizeContext = (context) => {
context.footer = credits
return context
}
const nextReleaseVersion = semver.inc(pkg.version, bump) as string
const changelogStream = conventionalChangelog(
// Options
{
preset: 'conventionalcommits',
tagPrefix,
pkg: {
path: `${pkg.packagePath}/package.json`,
},
},
// Context
{
version: nextReleaseVersion, // next release
version: newVersion, // next release
},
// GitRawCommitsOptions
{
path: 'packages',
// path: pkg.packagePath,
// from: pkg.prevGitTag,
// to: 'HEAD'
},
undefined,
conventionalPreset.writerOpts,
).on('error', (err) => {
console.error(err.stack)
console.error(err.toString())
@@ -42,13 +85,60 @@ export const updateChangelog = ({ pkg, bump }: Args) => {
const changelogFile = 'CHANGELOG.md'
const readStream = fse.createReadStream(changelogFile)
const tmp = tempfile()
changelogStream
.pipe(addStream(readStream))
.pipe(createWriteStream(tmp))
.on('finish', () => {
createReadStream(tmp).pipe(createWriteStream(changelogFile))
})
// Output to stdout if debug is true
const emitter = dryRun
? changelogStream.pipe(createWriteStream(tmp)).on('finish', () => {
createReadStream(tmp).pipe(process.stdout)
})
: changelogStream
.pipe(addStream(readStream))
.pipe(createWriteStream(tmp))
.on('finish', () => {
createReadStream(tmp).pipe(createWriteStream(changelogFile))
})
// Wait for the stream to finish
await once(emitter, 'finish')
}
// If file is executed directly, run the function
if (require.main === module) {
const { newVersion } = minimist(process.argv.slice(2))
updateChangelog({ newVersion, dryRun: true })
}
async function createContributorSection(lastTag: string): Promise<string> {
const commits = await git.log({ from: lastTag, to: 'HEAD' })
console.log(`Fetching contributors from ${commits.total} commits`)
const usernames = await Promise.all(
commits.all.map((c) =>
octokit
.request('GET /repos/{owner}/{repo}/commits/{ref}', {
owner: 'payloadcms',
repo: 'payload',
ref: c.hash,
})
.then(({ data }) => data.author?.login as string),
),
)
if (!usernames.length) return ''
// List of unique contributors
const contributors = Array.from(new Set(usernames)).map((c) => `@${c}`)
const formats = {
1: (contributors: string[]) => contributors[0],
2: (contributors: string[]) => contributors.join(' and '),
// Oxford comma ;)
default: (contributors: string[]) => contributors.join(', ').replace(/,([^,]*)$/, ', and$1'),
}
const formattedContributors =
formats[contributors.length]?.(contributors) || formats['default'](contributors)
const credits = `### Credits\n\nThanks to ${formattedContributors} for their contributions!\n`
return credits
}