Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: canary releases #6308

Merged
merged 12 commits into from
May 10, 2024
48 changes: 48 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 2 additions & 2 deletions scripts/lib/getPackageRegistryVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
231 changes: 231 additions & 0 deletions scripts/lib/getWorkspace.ts
Original file line number Diff line number Diff line change
@@ -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)}`,
}
}
}
16 changes: 13 additions & 3 deletions scripts/lib/whitelist.ts → scripts/lib/publishList.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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',
]
Loading
Loading