From 6ff42d162794fe0afee855a36ee44d70a2cc97fb Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Thu, 29 Feb 2024 16:01:51 -0500 Subject: [PATCH] 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 --- .gitignore | 2 + package.json | 9 +- packages/db-mongodb/package.json | 2 +- packages/payload/package.json | 2 +- packages/plugin-seo/package.json | 12 +- packages/plugin-stripe/package.json | 4 +- pnpm-lock.yaml | 31 ++- scripts/lib/getPackageDetails.ts | 73 +------ scripts/release.ts | 282 ++++++++++++++++------------ scripts/utils/updateChangelog.ts | 136 +++++++++++--- turbo.json | 2 +- 11 files changed, 316 insertions(+), 239 deletions(-) diff --git a/.gitignore b/.gitignore index 5912171ff..943505139 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ test-results .localstack .turbo +.turbo + # Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,webstorm,sublimetext,visualstudiocode diff --git a/package.json b/package.json index acde382b0..2c76bf194 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload-monorepo", - "version": "0.0.1", + "version": "2.11.2", "private": true, "workspaces:": [ "packages/*" @@ -60,6 +60,7 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.142.0", "@next/bundle-analyzer": "^14.1.0", + "@octokit/core": "^5.1.0", "@payloadcms/eslint-config": "workspace:*", "@playwright/test": "1.40.1", "@swc/cli": "^0.1.62", @@ -71,6 +72,7 @@ "@types/conventional-changelog": "^3.1.4", "@types/conventional-changelog-core": "^4.2.5", "@types/conventional-changelog-preset-loader": "^2.3.4", + "@types/conventional-changelog-writer": "^4.0.10", "@types/fs-extra": "^11.0.2", "@types/jest": "29.5.7", "@types/minimist": "1.2.2", @@ -86,12 +88,15 @@ "chalk-template": "1.1.0", "concat-stream": "^2.0.0", "conventional-changelog": "^5.1.0", + "conventional-changelog-conventionalcommits": "^7.0.2", "conventional-changelog-core": "^7.0.0", "conventional-changelog-preset-loader": "^4.1.0", + "conventional-changelog-writer": "^7.0.1", "copyfiles": "2.4.1", "cross-env": "7.0.3", "dotenv": "8.6.0", "drizzle-orm": "0.29.4", + "execa": "5.1.1", "express": "4.18.2", "form-data": "3.0.1", "fs-extra": "10.1.0", @@ -127,7 +132,7 @@ "slate": "0.91.4", "tempfile": "^3.0.0", "ts-node": "10.9.2", - "turbo": "^1.11.1", + "turbo": "^1.12.4", "typescript": "5.2.2", "uuid": "^9.0.1", "yocto-queue": "^1.0.0" diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 85eec6dfb..feef92f89 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -1,7 +1,7 @@ { "name": "@payloadcms/db-mongodb", "version": "1.4.1", - "description": "The officially supported MongoDB database adapter for Payload", + "description": "The officially supported MongoDB database adapter for Payload - Update 2", "repository": "https://github.com/payloadcms/payload", "license": "MIT", "homepage": "https://payloadcms.com", diff --git a/packages/payload/package.json b/packages/payload/package.json index fae5ede52..2bd1860f7 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "2.4.0", + "version": "2.11.0", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "MIT", "main": "./dist/index.js", diff --git a/packages/plugin-seo/package.json b/packages/plugin-seo/package.json index 6aa6b1038..81eab78c4 100644 --- a/packages/plugin-seo/package.json +++ b/packages/plugin-seo/package.json @@ -7,11 +7,14 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "build": "tsc", + "build": "echo \"Build temporarily disabled.\" && exit 0", + "build:swc": "swc ./src -d ./dist --config-file .swcrc", + "build:types": "tsc --emitDeclarationOnly --outDir dist", + "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/", "lint": "eslint src", "lint:fix": "eslint --fix --ext .ts,.tsx src", "clean": "rimraf {dist,*.tsbuildinfo}", - "prepublishOnly": "pnpm clean && pnpm turbo build" + "prepublishOnly": "pnpm clean && pnpm build" }, "keywords": [ "payload", @@ -51,8 +54,5 @@ "dist", "types.js", "types.d.ts" - ], - "dependencies": { - "@payloadcms/ui": "workspace:^" - } + ] } diff --git a/packages/plugin-stripe/package.json b/packages/plugin-stripe/package.json index 9be717164..a3a4cb58c 100644 --- a/packages/plugin-stripe/package.json +++ b/packages/plugin-stripe/package.json @@ -7,12 +7,12 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "_build": "pnpm build:swc && pnpm build:types", + "build": "pnpm build:swc && pnpm build:types", "build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:types": "tsc --emitDeclarationOnly --outDir dist", "clean": "rimraf {dist,*.tsbuildinfo}", "test": "echo 'No tests available.'", - "prepublishOnly": "yarn clean && yarn build" + "prepublishOnly": "pnpm clean && pnpm turbo run build && pnpm test" }, "keywords": [ "payload", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f6238f5a..43f618ff6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@next/bundle-analyzer': specifier: ^14.1.0 version: 14.1.0 + '@octokit/core': + specifier: ^5.1.0 + version: 5.1.0 '@payloadcms/eslint-config': specifier: workspace:* version: link:packages/eslint-config-payload @@ -68,6 +71,9 @@ importers: '@types/conventional-changelog-preset-loader': specifier: ^2.3.4 version: 2.3.5 + '@types/conventional-changelog-writer': + specifier: ^4.0.10 + version: 4.0.10 '@types/fs-extra': specifier: ^11.0.2 version: 11.0.4 @@ -113,12 +119,18 @@ importers: conventional-changelog: specifier: ^5.1.0 version: 5.1.0 + conventional-changelog-conventionalcommits: + specifier: ^7.0.2 + version: 7.0.2 conventional-changelog-core: specifier: ^7.0.0 version: 7.0.0 conventional-changelog-preset-loader: specifier: ^4.1.0 version: 4.1.0 + conventional-changelog-writer: + specifier: ^7.0.1 + version: 7.0.1 copyfiles: specifier: 2.4.1 version: 2.4.1 @@ -131,6 +143,9 @@ importers: drizzle-orm: specifier: 0.29.4 version: 0.29.4(@libsql/client@0.5.2)(@types/pg@8.10.2)(@types/react@18.2.15)(pg@8.11.3)(react@18.2.0) + execa: + specifier: 5.1.1 + version: 5.1.1 express: specifier: 4.18.2 version: 4.18.2 @@ -237,7 +252,7 @@ importers: specifier: 10.9.2 version: 10.9.2(@swc/core@1.4.2)(@types/node@20.5.7)(typescript@5.2.2) turbo: - specifier: ^1.11.1 + specifier: ^1.12.4 version: 1.12.4 typescript: specifier: 5.2.2 @@ -1039,10 +1054,6 @@ importers: version: 5.90.3(@swc/core@1.4.2) packages/plugin-seo: - dependencies: - '@payloadcms/ui': - specifier: workspace:^ - version: link:../ui devDependencies: '@payloadcms/eslint-config': specifier: workspace:* @@ -5447,7 +5458,7 @@ packages: resolution: {integrity: sha512-EK654IW1SBqTZSzN9+PlB6SKSYlDhsrCv1olfeEXvt/FHcmd7kvFWW0cFvMtnWalR/D7baYzY3FJ2TSt7UUySg==} dependencies: '@types/conventional-commits-parser': 5.0.0 - '@types/node': 20.6.2 + '@types/node': 16.18.85 dev: true /@types/conventional-changelog@3.1.5: @@ -5462,7 +5473,7 @@ packages: /@types/conventional-commits-parser@5.0.0: resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} dependencies: - '@types/node': 20.6.2 + '@types/node': 16.18.85 dev: true /@types/conventional-recommended-bump@9.0.3: @@ -5557,7 +5568,7 @@ packages: /@types/git-raw-commits@2.0.4: resolution: {integrity: sha512-PYmTK156j6TilbwtnO2JErH4TCC1izpgP3kRE3KUX35bjBOD2A+syCGeercqHXBcM6wL+DU0WbJB2Gz8fPR+8A==} dependencies: - '@types/node': 20.6.2 + '@types/node': 16.18.85 dev: true /@types/glob@7.2.0: @@ -5935,7 +5946,7 @@ packages: /@types/tunnel@0.0.3: resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==} dependencies: - '@types/node': 20.6.2 + '@types/node': 16.18.85 dev: true /@types/uuid@8.3.4: @@ -11366,7 +11377,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.6.2 + '@types/node': 16.18.85 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true diff --git a/scripts/lib/getPackageDetails.ts b/scripts/lib/getPackageDetails.ts index 7016e7347..b438c08c6 100644 --- a/scripts/lib/getPackageDetails.ts +++ b/scripts/lib/getPackageDetails.ts @@ -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 => { - 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 => { + 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 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 return packageDetails.filter((p): p is Exclude => 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')} - -`) -} diff --git a/scripts/release.ts b/scripts/release.ts index 79a94bd78..9ab8af26c 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -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, - ) + 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 { return confirm } + +async function header(message: string) { + console.log(chalk.bold.green(`${message}\n`)) +} diff --git a/scripts/utils/updateChangelog.ts b/scripts/utils/updateChangelog.ts index 3820043aa..5176b3a22 100755 --- a/scripts/utils/updateChangelog.ts +++ b/scripts/utils/updateChangelog.ts @@ -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 { + 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 } diff --git a/turbo.json b/turbo.json index 77b371bd9..29a1b5a3c 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,7 @@ "dependsOn": ["^clean"] }, "build": { - "cache": true, + "cache": false, "dependsOn": ["^build"], "outputs": ["./dist/**"] },