ci: canary releases (#6308)
This commit is contained in:
48
.github/actions/setup/action.yml
vendored
Normal file
48
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Setup node and pnpm
|
||||
description: Configure the Node.js and pnpm versions
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'The Node.js version to use'
|
||||
required: true
|
||||
default: 18.20.2
|
||||
pnpm-version:
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 8.15.7
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- shell: bash
|
||||
run: pnpm install
|
||||
33
.github/workflows/release-canary.yml
vendored
Normal file
33
.github/workflows/release-canary.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: release-canary
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
- name: Load npm token
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Canary release script
|
||||
# dry run hard-coded to true for testing and no npm token provided
|
||||
run: pnpm tsx ./scripts/publish-canary.ts
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -2,12 +2,12 @@ import chalk from 'chalk'
|
||||
import pLimit from 'p-limit'
|
||||
|
||||
import { getPackageDetails } from './getPackageDetails.js'
|
||||
import { packageWhitelist } from './whitelist.js'
|
||||
import { packagePublishList } from './publishList.js'
|
||||
|
||||
const npmRequestLimit = pLimit(40)
|
||||
|
||||
export const getPackageRegistryVersions = async (): Promise<void> => {
|
||||
const packageDetails = await getPackageDetails(packageWhitelist)
|
||||
const packageDetails = await getPackageDetails(packagePublishList)
|
||||
|
||||
const results = await Promise.all(
|
||||
packageDetails.map(async (pkg) =>
|
||||
|
||||
231
scripts/lib/getWorkspace.ts
Normal file
231
scripts/lib/getWorkspace.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import type { ReleaseType } from 'semver'
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import pLimit from 'p-limit'
|
||||
import path from 'path'
|
||||
import semver from 'semver'
|
||||
|
||||
import { getPackageDetails } from './getPackageDetails.js'
|
||||
import { packagePublishList } from './publishList.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const projectRoot = path.resolve(dirname, '../../')
|
||||
const rootPackageJsonPath = path.resolve(projectRoot, 'package.json')
|
||||
const npmPublishLimit = pLimit(5)
|
||||
const cwd = path.resolve(dirname, '../../')
|
||||
|
||||
const execaOpts: execa.Options = { stdio: 'inherit' }
|
||||
|
||||
type PackageDetails = {
|
||||
/** Name in package.json / npm registry */
|
||||
name: string
|
||||
/** Full path to package relative to project root */
|
||||
packagePath: `packages/${string}`
|
||||
/** Short name is the directory name */
|
||||
shortName: string
|
||||
/** Version in package.json */
|
||||
version: string
|
||||
}
|
||||
|
||||
type PackageReleaseType = 'canary' | ReleaseType
|
||||
|
||||
type PublishResult = {
|
||||
name: string
|
||||
success: boolean
|
||||
details?: string
|
||||
}
|
||||
|
||||
type PublishOpts = {
|
||||
dryRun?: boolean
|
||||
tag?: 'beta' | 'canary' | 'latest'
|
||||
}
|
||||
|
||||
type Workspace = {
|
||||
version: () => Promise<string>
|
||||
tag: string
|
||||
packages: PackageDetails[]
|
||||
showVersions: () => Promise<void>
|
||||
bumpVersion: (type: PackageReleaseType) => Promise<void>
|
||||
build: () => Promise<void>
|
||||
publish: (opts: PublishOpts) => Promise<void>
|
||||
publishSync: (opts: PublishOpts) => Promise<void>
|
||||
}
|
||||
|
||||
export const getWorkspace = async () => {
|
||||
const build = async () => {
|
||||
await execa('pnpm', ['install'], execaOpts)
|
||||
|
||||
const buildResult = await execa('pnpm', ['build:all', '--output-logs=errors-only'], execaOpts)
|
||||
if (buildResult.exitCode !== 0) {
|
||||
console.error('Build failed')
|
||||
console.log(buildResult.stderr)
|
||||
throw new Error('Build failed')
|
||||
}
|
||||
}
|
||||
|
||||
// Publish one package at a time
|
||||
const publishSync: Workspace['publishSync'] = async ({ dryRun, tag = 'canary' }) => {
|
||||
const packageDetails = await getPackageDetails(packagePublishList)
|
||||
const results: PublishResult[] = []
|
||||
for (const pkg of packageDetails) {
|
||||
const res = await publishSinglePackage(pkg, { dryRun, tag })
|
||||
results.push(res)
|
||||
}
|
||||
|
||||
console.log(`\n\nResults:\n`)
|
||||
|
||||
console.log(
|
||||
results
|
||||
.map((result) => {
|
||||
if (!result.success) {
|
||||
console.error(result.details)
|
||||
return ` ❌ ${result.name}`
|
||||
}
|
||||
return ` ✅ ${result.name}`
|
||||
})
|
||||
.join('\n') + '\n',
|
||||
)
|
||||
}
|
||||
|
||||
const publish = async () => {
|
||||
const packageDetails = await getPackageDetails(packagePublishList)
|
||||
const results = await Promise.allSettled(
|
||||
packageDetails.map((pkg) => publishPackageThrottled(pkg, { dryRun: true })),
|
||||
)
|
||||
|
||||
console.log(`\n\nResults:\n`)
|
||||
|
||||
console.log(
|
||||
results
|
||||
.map((result) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(result.reason)
|
||||
return ` ❌ ${String(result.reason)}`
|
||||
}
|
||||
const { name, success, details } = result.value
|
||||
let summary = ` ${success ? '✅' : '❌'} ${name}`
|
||||
if (details) {
|
||||
summary += `\n ${details}\n`
|
||||
}
|
||||
return summary
|
||||
})
|
||||
.join('\n') + '\n',
|
||||
)
|
||||
}
|
||||
|
||||
const showVersions = async () => {
|
||||
const { packages, version } = await getCurrentPackageState()
|
||||
console.log(`\n Version: ${version}\n`)
|
||||
console.log(` Changes (${packages.length} packages):\n`)
|
||||
console.log(`${packages.map((p) => ` - ${p.name.padEnd(32)} ${p.version}`).join('\n')}\n`)
|
||||
}
|
||||
|
||||
const setVersion = async (version: string) => {
|
||||
const rootPackageJson = await fse.readJSON(rootPackageJsonPath)
|
||||
rootPackageJson.version = version
|
||||
await fse.writeJSON(rootPackageJsonPath, rootPackageJson, { spaces: 2 })
|
||||
|
||||
const packageJsons = await getPackageDetails(packagePublishList)
|
||||
await Promise.all(
|
||||
packageJsons.map(async (pkg) => {
|
||||
const packageJson = await fse.readJSON(`${pkg.packagePath}/package.json`)
|
||||
packageJson.version = version
|
||||
await fse.writeJSON(`${pkg.packagePath}/package.json`, packageJson, { spaces: 2 })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const bumpVersion = async (bumpType: PackageReleaseType) => {
|
||||
const { version: monorepoVersion, packages: packageDetails } = await getCurrentPackageState()
|
||||
|
||||
let nextReleaseVersion
|
||||
if (bumpType === 'canary') {
|
||||
const hash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim().slice(0, 7)
|
||||
nextReleaseVersion = semver.inc(monorepoVersion, 'patch') + `-canary.${hash}`
|
||||
} else {
|
||||
nextReleaseVersion = semver.inc(monorepoVersion, bumpType)
|
||||
}
|
||||
|
||||
console.log(`\n Version: ${monorepoVersion} => ${nextReleaseVersion}\n`)
|
||||
console.log(` Bump: ${bumpType}`)
|
||||
console.log(` Changes (${packageDetails.length} packages):\n`)
|
||||
console.log(
|
||||
`${packageDetails.map((p) => ` - ${p.name.padEnd(32)} ${p.version} => ${nextReleaseVersion}`).join('\n')}\n`,
|
||||
)
|
||||
|
||||
await setVersion(nextReleaseVersion)
|
||||
}
|
||||
|
||||
const workspace: Workspace = {
|
||||
version: async () => (await fse.readJSON(rootPackageJsonPath)).version,
|
||||
tag: 'latest',
|
||||
packages: await getPackageDetails(packagePublishList),
|
||||
showVersions,
|
||||
bumpVersion,
|
||||
build,
|
||||
publish,
|
||||
publishSync,
|
||||
}
|
||||
|
||||
return workspace
|
||||
}
|
||||
|
||||
async function getCurrentPackageState(): Promise<{
|
||||
packages: PackageDetails[]
|
||||
version: string
|
||||
}> {
|
||||
const packageDetails = await getPackageDetails(packagePublishList)
|
||||
const rootPackageJson = await fse.readJSON(rootPackageJsonPath)
|
||||
return { packages: packageDetails, version: rootPackageJson.version }
|
||||
}
|
||||
|
||||
/** Publish with promise concurrency throttling */
|
||||
async function publishPackageThrottled(pkg: PackageDetails, opts?: { dryRun?: boolean }) {
|
||||
const { dryRun = true } = opts ?? {}
|
||||
return npmPublishLimit(() => publishSinglePackage(pkg, { dryRun }))
|
||||
}
|
||||
|
||||
async function publishSinglePackage(pkg: PackageDetails, opts: PublishOpts) {
|
||||
console.log(`🚀 ${pkg.name} publishing...`)
|
||||
|
||||
const { dryRun, tag = 'canary' } = opts
|
||||
|
||||
try {
|
||||
const cmdArgs = ['publish', '-C', pkg.packagePath, '--no-git-checks', '--tag', tag]
|
||||
if (dryRun) {
|
||||
cmdArgs.push('--dry-run')
|
||||
}
|
||||
const { exitCode, stderr } = await execa('pnpm', cmdArgs, {
|
||||
cwd,
|
||||
// stdio: ['ignore', 'ignore', 'pipe'],
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log(`\n\n❌ ${pkg.name} ERROR: pnpm publish failed\n\n${stderr}`)
|
||||
|
||||
return {
|
||||
name: pkg.name,
|
||||
success: false,
|
||||
details: `Exit Code: ${exitCode}, stderr: ${stderr}`,
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${pkg.name} published`)
|
||||
return { name: pkg.name, success: true }
|
||||
} catch (err: unknown) {
|
||||
console.error(err)
|
||||
return {
|
||||
name: pkg.name,
|
||||
success: false,
|
||||
details:
|
||||
err instanceof Error
|
||||
? `Error publishing ${pkg.name}: ${err.message}`
|
||||
: `Unexpected error publishing ${pkg.name}: ${String(err)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
// Update this list with any packages to publish
|
||||
export const packageWhitelist = [
|
||||
/**
|
||||
* Packages that should be published to NPM
|
||||
*
|
||||
* Note that this does not include all packages in the monorepo
|
||||
*/
|
||||
export const packagePublishList = [
|
||||
'payload',
|
||||
'translations',
|
||||
'ui',
|
||||
@@ -32,5 +36,11 @@ export const packageWhitelist = [
|
||||
'plugin-search',
|
||||
'plugin-seo',
|
||||
'plugin-stripe',
|
||||
// 'plugin-sentry',
|
||||
|
||||
// Unpublished
|
||||
// 'plugin-sentry'
|
||||
// 'storage-uploadthing',
|
||||
// 'eslint-config-payload',
|
||||
// 'eslint-plugin-payload',
|
||||
// 'live-preview-vue',
|
||||
]
|
||||
72
scripts/publish-canary.ts
Executable file
72
scripts/publish-canary.ts
Executable file
@@ -0,0 +1,72 @@
|
||||
import type { ExecSyncOptions } from 'child_process'
|
||||
import type execa from 'execa'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import { loadChangelogConfig } from 'changelogen'
|
||||
import { execSync } from 'child_process'
|
||||
import fse from 'fs-extra'
|
||||
import minimist from 'minimist'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import pLimit from 'p-limit'
|
||||
import path from 'path'
|
||||
import prompts from 'prompts'
|
||||
import semver from 'semver'
|
||||
|
||||
import type { PackageDetails } from './lib/getPackageDetails.js'
|
||||
|
||||
import { getPackageDetails } from './lib/getPackageDetails.js'
|
||||
import { getPackageRegistryVersions } from './lib/getPackageRegistryVersions.js'
|
||||
import { getWorkspace } from './lib/getWorkspace.js'
|
||||
import { packagePublishList } from './lib/publishList.js'
|
||||
import { getRecommendedBump } from './utils/getRecommendedBump.js'
|
||||
import { updateChangelog } from './utils/updateChangelog.js'
|
||||
|
||||
const npmPublishLimit = pLimit(5)
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const cwd = path.resolve(dirname, '..')
|
||||
|
||||
const execOpts: ExecSyncOptions = { stdio: 'inherit' }
|
||||
const execaOpts: execa.Options = { stdio: 'inherit' }
|
||||
|
||||
const args = minimist(process.argv.slice(2))
|
||||
|
||||
// const {
|
||||
// bump = 'patch', // Semver release type
|
||||
// changelog = false, // Whether to update the changelog. WARNING: This gets throttled on too many commits
|
||||
// 'dry-run': dryRun,
|
||||
// 'git-tag': gitTag = true, // Whether to run git tag and commit operations
|
||||
// 'git-commit': gitCommit = true, // Whether to run git commit operations
|
||||
// tag = 'latest',
|
||||
// } = args
|
||||
|
||||
const dryRun = true
|
||||
|
||||
const logPrefix = dryRun ? chalk.bold.magenta('[dry-run] >') : ''
|
||||
|
||||
async function main() {
|
||||
const workspace = await getWorkspace()
|
||||
await workspace.bumpVersion('canary')
|
||||
await workspace.build()
|
||||
await workspace.publishSync({ dryRun: false, tag: 'canary' })
|
||||
|
||||
header('🎉 Done!')
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
function abort(message = 'Abort', exitCode = 1) {
|
||||
console.error(chalk.bold.red(`\n${message}\n`))
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
function header(message: string, opts?: { enable?: boolean }) {
|
||||
const { enable } = opts ?? {}
|
||||
if (!enable) return
|
||||
|
||||
console.log(chalk.bold.green(`${message}\n`))
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import type { PackageDetails } from './lib/getPackageDetails.js'
|
||||
|
||||
import { getPackageDetails } from './lib/getPackageDetails.js'
|
||||
import { getPackageRegistryVersions } from './lib/getPackageRegistryVersions.js'
|
||||
import { packageWhitelist } from './lib/whitelist.js'
|
||||
import { packagePublishList } from './lib/publishList.js'
|
||||
import { getRecommendedBump } from './utils/getRecommendedBump.js'
|
||||
import { updateChangelog } from './utils/updateChangelog.js'
|
||||
|
||||
@@ -143,7 +143,7 @@ async function main() {
|
||||
console.log(chalk.gray(changelogContent) + '\n\n')
|
||||
console.log(`Release URL: ${chalk.dim(releaseUrl)}`)
|
||||
|
||||
let packageDetails = await getPackageDetails(packageWhitelist)
|
||||
let packageDetails = await getPackageDetails(packagePublishList)
|
||||
|
||||
console.log(chalk.bold(`\n Version: ${monorepoVersion} => ${chalk.green(nextReleaseVersion)}\n`))
|
||||
console.log(chalk.bold.yellow(` Bump: ${bump}`))
|
||||
|
||||
Reference in New Issue
Block a user