publish.ts•30.8 kB
import fs from 'node:fs'
import path from 'node:path'
import { dependencies as dependenciesPrismaEnginesPkg } from '@prisma/engines/package.json'
import slugify from '@sindresorhus/slugify'
import { IncomingWebhook } from '@slack/webhook'
import arg from 'arg'
import topo from 'batching-toposort'
import { execaCommand, type ExecaError } from 'execa'
import globby from 'globby'
import { blue, bold, cyan, dim, magenta, red, underline } from 'kleur/colors'
import pRetry from 'p-retry'
import semver from 'semver'
const onlyPackages = process.env.ONLY_PACKAGES ? process.env.ONLY_PACKAGES.split(',') : null
const skipPackages = process.env.SKIP_PACKAGES ? process.env.SKIP_PACKAGES.split(',') : null
async function getLatestCommitHash(dir: string): Promise<string> {
if (process.env.GITHUB_CONTEXT) {
const context = JSON.parse(process.env.GITHUB_CONTEXT)
return context.sha
}
const result = await runResult(dir, 'git log --pretty=format:"%ad %H %P" --date=iso-strict -n 1')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [date, hash] = result.split(' ')
return hash
}
/**
* Runs a command and returns the resulting stdout in a Promise.
* @param cwd cwd for running the command
* @param cmd command to run
*/
async function runResult(cwd: string, cmd: string): Promise<string> {
try {
const result = await execaCommand(cmd, {
cwd,
stdio: 'pipe',
shell: true,
})
return result.stdout
} catch (_e) {
const e = _e as ExecaError
throw new Error(red(`Error running ${bold(cmd)} in ${underline(cwd)}:`) + (e.stderr || e.stack || e.message))
}
}
/**
* Runs a command and pipes the stdout & stderr to the current process.
* @param cwd cwd for running the command
* @param cmd command to run
*/
async function run(cwd: string, cmd: string, dry = false, hidden = false): Promise<void> {
const args = [underline('./' + cwd).padEnd(20), bold(cmd)]
if (dry) {
args.push(dim('(dry)'))
}
if (!hidden) {
console.log(...args)
}
if (dry) {
return
}
try {
await execaCommand(cmd, {
cwd,
stdio: 'inherit',
shell: true,
env: {
...process.env,
PRISMA_SKIP_POSTINSTALL_GENERATE: 'true',
},
})
} catch (_e) {
const e = _e as ExecaError
throw new Error(red(`Error running ${bold(cmd)} in ${underline(cwd)}:`) + (e.stderr || e.stack || e.message))
}
}
type RawPackage = {
path: string
packageJson: any
}
type RawPackages = { [packageName: string]: RawPackage }
export async function getPackages(): Promise<RawPackages> {
const packagePaths = await globby(['packages/*/package.json'], {
ignore: ['**/node_modules/**', '**/examples/**', '**/fixtures/**'],
})
const packages = await Promise.all(
packagePaths.map(async (p) => ({
path: p,
packageJson: JSON.parse(await fs.promises.readFile(p, 'utf-8')),
})),
)
return packages.reduce<RawPackages>((acc, p) => {
if (p.packageJson.name) {
acc[p.packageJson.name] = p
}
return acc
}, {})
}
interface Package {
private?: boolean
name: string
path: string
version: string
usedBy: string[]
usedByDev: string[]
uses: string[]
usesDev: string[]
packageJson: any
}
type Packages = { [packageName: string]: Package }
export function getPackageDependencies(packages: RawPackages): Packages {
const packageCache = Object.entries(packages).reduce<Packages>((acc, [name, pkg]) => {
const usesDev = getPrismaDependencies(pkg.packageJson.devDependencies)
acc[name] = {
private: pkg.packageJson.private,
version: pkg.packageJson.version,
name,
path: pkg.path,
usedBy: [],
usedByDev: [],
uses: getPrismaDependencies(pkg.packageJson.dependencies),
usesDev,
packageJson: pkg.packageJson,
}
return acc
}, {})
for (const pkg of Object.values(packageCache)) {
for (const dependency of pkg.uses) {
if (packageCache[dependency]) {
packageCache[dependency].usedBy.push(pkg.name)
} else {
console.info(`Skipping ${dependency} as it's not in this workspace`)
}
}
for (const devDependency of pkg.usesDev) {
if (packageCache[devDependency]) {
packageCache[devDependency].usedByDev.push(pkg.name)
} else {
console.info(`Skipping ${devDependency} as it's not in this workspace`)
}
}
}
return packageCache
}
function getPrismaDependencies(dependencies?: { [name: string]: string }): string[] {
if (!dependencies) {
return []
}
return Object.keys(dependencies).filter((d) => d.startsWith('@prisma') && !d.startsWith('@prisma/studio'))
}
function getCircularDependencies(packages: Packages): string[][] {
const circularDeps = [] as string[][]
for (const pkg of Object.values(packages)) {
const uses = [...pkg.uses, ...pkg.usesDev]
const usedBy = [...pkg.usedBy, ...pkg.usedByDev]
const circles = intersection(uses, usedBy)
if (circles.length > 0) {
circularDeps.push(circles)
}
}
return circularDeps
}
export function getPublishOrder(packages: Packages): string[][] {
const dag: { [pkg: string]: string[] } = Object.values(packages).reduce((acc, curr) => {
acc[curr.name] = [...curr.usedBy, ...curr.usedByDev]
return acc
}, {})
return topo(dag) // TODO: this is now done by pnpm (top sort), can we remove?
}
function zeroOutPatch(version: string): string {
const parts = version.split('.')
parts[parts.length - 1] = '0'
return parts.join('.')
}
/**
* Takes the max dev version + 1
* For now supporting X.Y.Z-dev.#
* @param packages Local package definitions
*/
async function getNewDevVersion(packages: Packages): Promise<string> {
const before = Math.round(performance.now())
console.log('\nCalculating new dev version...')
// Why are we calling zeroOutPatch?
// Because here we're only interested in the 2.5.0 <- the next minor stable version
// If the current version would be 2.4.7, we would end up with 2.5.7
const nextStable = zeroOutPatch((await getNextMinorStable())!)
console.log(`getNewDevVersion: Next minor stable: ${nextStable}`)
const versions = await getAllVersionsPublishedFor(packages, 'dev', nextStable + '-dev')
const maxDev = getMaxDevVersionIncrement(versions)
const version = `${nextStable}-dev.${maxDev + 1}`
console.log(`Got ${version} in ${Math.round(performance.now()) - before}ms`)
return version
}
/**
* Takes the max dev version + 1
* For now supporting X.Y.Z-dev.#
* @param packages Local package definitions
*/
async function getNewIntegrationVersion(packages: Packages, branch: string): Promise<string> {
const before = Math.round(performance.now())
console.log('\nCalculating new integration version...')
// Why are we calling zeroOutPatch?
// Because here we're only interested in the 2.5.0 <- the next minor stable version
// If the current version would be 2.4.7, we would end up with 2.5.7
const nextStable = zeroOutPatch((await getNextMinorStable())!)
console.log(`getNewIntegrationVersion: Next minor stable: ${nextStable}`)
const branchWithoutPrefix = branch.replace(/^integration\//, '')
const versionNameSlug = `${nextStable}-integration-${slugify(branchWithoutPrefix)}`
const versions = await getAllVersionsPublishedFor(packages, 'integration', versionNameSlug)
const maxIntegration = getMaxIntegrationVersionIncrement(versions)
const version = `${versionNameSlug}.${maxIntegration + 1}`
// TODO: can we remove this?
console.log(`Got ${version} in ${Math.round(performance.now()) - before}ms`)
return version
}
// This function gets the current "patchMajorMinor" (major and minor of the patch branch),
// then retrieves the current versions of @prisma/client from npm,
// and filters that array down to the major and minor of the patch branch
// to figure out what the current highest patch number there currently is
async function getCurrentPatchForPatchVersions(patchMajorMinor: { major: number; minor: number }): Promise<number> {
// TODO: could we add the name of the branch, as well as the relevant versions => faster
// $ npm view '@prisma/client@3.0.x' version --json
// [
// "3.0.1",
// "3.0.2"
// ]
// We retry a few times if it fails
// npm can have some hiccups
const remoteVersionsString = await pRetry(
async () => {
return await runResult('.', 'npm view @prisma/client@* version --json')
},
{
retries: 6,
onFailedAttempt: (e) => {
console.error(e)
},
},
)
let versions = JSON.parse(remoteVersionsString)
// inconsistent npm api
if (!Array.isArray(versions)) {
versions = [versions]
}
const relevantVersions: Array<{
major: number
minor: number
patch: number
}> = versions
.map((v) => {
const match = semverRegex.exec(v)
if (match?.groups) {
return {
major: Number(match.groups.major),
minor: Number(match.groups.minor),
patch: Number(match.groups.patch),
}
}
return null
})
.filter((group) => group && group.minor === patchMajorMinor.minor && group.major === patchMajorMinor.major)
if (relevantVersions.length === 0) {
return 0
}
// sort descending by patch
relevantVersions.sort((a, b) => {
return a.patch < b.patch ? 1 : -1
})
return relevantVersions[0].patch
}
async function getNewPatchDevVersion(packages: Packages, patchBranch: string): Promise<string> {
const patchMajorMinor = getSemverFromPatchBranch(patchBranch)
if (!patchMajorMinor) {
throw new Error(`Could not get major and minor for ${patchBranch}`)
}
const currentPatch = await getCurrentPatchForPatchVersions(patchMajorMinor)
const newPatch = currentPatch + 1
const newVersion = `${patchMajorMinor.major}.${patchMajorMinor.minor}.${newPatch}`
const versions = [...(await getAllVersionsPublishedFor(packages, 'dev', newVersion))]
const maxIncrement = getMaxPatchVersionIncrement(versions)
return `${newVersion}-dev.${maxIncrement + 1}`
}
function getMaxDevVersionIncrement(versions: string[]): number {
const regex = /\d+\.\d+\.\d+-dev\.(\d+)/
const increments = versions
.filter((v) => v.trim().length > 0)
.map((v) => {
const match = regex.exec(v)
if (match) {
return Number(match[1])
}
return 0
})
.filter((v) => v)
return Math.max(...increments, 0)
}
function getMaxIntegrationVersionIncrement(versions: string[]): number {
const regex = /\d+\.\d+\.\d+-integration.*\.(\d+)/
const increments = versions
.filter((v) => v.trim().length > 0)
.map((v) => {
const match = regex.exec(v)
if (match) {
return Number(match[1])
}
return 0
})
.filter((v) => v)
return Math.max(...increments, 0)
}
// TODO: Adjust this for stable releases
function getMaxPatchVersionIncrement(versions: string[]): number {
const regex = /\d+\.\d+\.\d+-dev\.(\d+)/
const increments = versions
.filter((v) => v.trim().length > 0)
.map((v) => {
const match = regex.exec(v)
if (match && match[1]) {
return Number(match[1])
}
return 0
})
.filter((v) => v)
return Math.max(...increments, 0)
}
/**
* @param pkgs
* @param channel
* @param prefix
* @returns All versions published on npm for a given channel and prefix
*/
export async function getAllVersionsPublishedFor(pkgs: Packages, channel: string, prefix: string): Promise<string[]> {
// We check the versions for the `@prisma/debug` package
// Why?
// Because `@prisma/debug` is the first package that will be published
// So if npm fails to publish one of the packages,
// we cannot republish on the same version on a next run
const pkg = pkgs['@prisma/debug']
const values = async (pkg: Package) => {
const pkgVersions = [] as string[]
if (pkg.version.startsWith(prefix)) {
pkgVersions.push(pkg.version)
}
// We retry a few times if it fails
// npm can have some hiccups
const remoteVersionsString = await pRetry(
async () => {
return await runResult('.', `npm info ${pkg.name} versions --json`)
},
{
retries: 6,
onFailedAttempt: (e) => {
console.error(e)
},
},
)
const remoteVersions: string = JSON.parse(remoteVersionsString)
for (const remoteVersion of remoteVersions) {
if (remoteVersion.includes(channel) && remoteVersion.startsWith(prefix) && !pkgVersions.includes(remoteVersion)) {
pkgVersions.push(remoteVersion)
}
}
return pkgVersions
}
return [...new Set(await values(pkg))]
}
/**
* Only used when publishing to the `dev` and `integration` npm channels
* (see `getNewDevVersion()` and `getNewIntegrationVersion()`)
* @returns The next minor version for the `latest` channel
* Example: If latest is `4.9.0` it will return `4.10.0`
*/
async function getNextMinorStable() {
// We check the Prisma CLI `latest` version
// We retry a few times if it fails
// npm can have some hiccups
const remoteVersionString = await pRetry(
async () => {
return await runResult('.', `npm info prisma version`)
},
{
retries: 6,
onFailedAttempt: (e) => {
console.error(e)
},
},
)
return increaseMinor(remoteVersionString)
}
// TODO: could probably use the semver package
function getSemverFromPatchBranch(version: string) {
// the branch name must match
// number.number.x like 3.0.x or 2.29.x
// as an exact match, no character before or after
const regex = /^(\d+)\.(\d+)\.x$/
const match = regex.exec(version)
if (match) {
return {
major: Number(match[1]),
minor: Number(match[2]),
}
}
return undefined
}
// TODO: name this to main
async function publish() {
const args = arg({
'--publish': Boolean,
'--dry-run': Boolean,
'--release': String, // TODO What does that do? Can we remove this? probably
'--test': Boolean,
})
if (!process.env.GITHUB_REF_NAME) {
throw new Error(`Missing env var GITHUB_REF_NAME`)
}
if (process.env.DRY_RUN) {
console.log(blue(bold(`\nThe DRY_RUN env var is set, so we'll do a dry run!\n`)))
args['--dry-run'] = true
}
const dryRun = args['--dry-run'] ?? false
if (args['--publish'] && process.env.RELEASE_VERSION) {
if (args['--release']) {
throw new Error(`Can't provide env var RELEASE_VERSION and --release at the same time`)
}
console.log(`Setting --release to RELEASE_VERSION = ${process.env.RELEASE_VERSION}`)
args['--release'] = process.env.RELEASE_VERSION
// TODO: put this into a global variable VERSION
// and then replace the args['--release'] with it
}
if (!args['--test'] && !args['--publish'] && !dryRun) {
throw new Error('Please either provide --test or --publish or --dry-run')
}
if (args['--release']) {
if (!semver.valid(args['--release'])) {
throw new Error(`New release version ${bold(underline(args['--release']))} is not a valid semver version.`)
}
// TODO: this can probably be replaced by semver lib
const releaseRegex = /\d{1,2}\.\d{1,2}\.\d{1,2}/
if (!releaseRegex.test(args['--release'])) {
throw new Error(
`New release version ${bold(underline(args['--release']))} does not follow the stable naming scheme: ${bold(
underline('x.y.z'),
)}`,
)
}
// If there is --release, it's always also --publish
args['--publish'] = true
}
// TODO: does this make more sense to be in our tests? or in the monorepo postinstall?
const rawPackages = await getPackages()
const packages = getPackageDependencies(rawPackages)
const circles = getCircularDependencies(packages)
if (circles.length > 0) {
// TODO this can be done by esbuild
throw new Error(`Oops, there are circular dependencies: ${circles}`)
}
let prismaVersion: undefined | string
let tag: undefined | string
let tagForEcosystemTestsCheck: undefined | string
const patchBranch = getPatchBranch()
console.log({ patchBranch })
// TODO: can be refactored into one branch utility
const branch = await getPrismaBranch()
console.log({ branch })
// For branches that are named "integration/" we publish to the integration npm tag
if (branch && (process.env.FORCE_INTEGRATION_RELEASE || branch.startsWith('integration/'))) {
prismaVersion = await getNewIntegrationVersion(packages, branch)
tag = 'integration'
}
// Is it a patch branch? (Like 2.20.x)
else if (patchBranch) {
prismaVersion = await getNewPatchDevVersion(packages, patchBranch)
tag = 'patch-dev'
if (args['--release']) {
tagForEcosystemTestsCheck = 'patch-dev' //?
prismaVersion = args['--release']
tag = 'latest'
}
} else if (args['--release']) {
// TODO:Where each patch branch goes
prismaVersion = args['--release']
tag = 'latest'
tagForEcosystemTestsCheck = 'dev'
} else {
prismaVersion = await getNewDevVersion(packages)
tag = 'dev'
}
console.log({
patchBranch,
tag,
tagForEcosystemTestsCheck,
prismaVersion,
})
if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `patchBranch=${patchBranch}\n`)
fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\n`)
fs.appendFileSync(process.env.GITHUB_OUTPUT, `tagForEcosystemTestsCheck=${tagForEcosystemTestsCheck}\n`)
fs.appendFileSync(process.env.GITHUB_OUTPUT, `prismaVersion=${prismaVersion}\n`)
}
if (!dryRun && args['--test']) {
if (onlyPackages || skipPackages) {
console.log(bold('\nTesting all packages was skipped because onlyPackages or skipPackages is set.'))
} else {
console.log(bold('\nTesting all packages...'))
await testPackages(packages, getPublishOrder(packages))
}
}
if (args['--publish'] || dryRun) {
if (args['--release']) {
if (!tagForEcosystemTestsCheck) {
throw new Error(`tagForEcosystemTestsCheck missing`)
}
const passing = await areEcosystemTestsPassing(tagForEcosystemTestsCheck)
if (!passing && !process.env.SKIP_ECOSYSTEMTESTS_CHECK) {
throw new Error(`We can't release, as the ecosystem-tests are not passing for the ${tag} npm tag!
Check them out at https://github.com/prisma/ecosystem-tests/actions?query=workflow%3Atest+branch%3A${tag}`)
}
}
const publishOrder = filterPublishOrder(getPublishOrder(packages), ['@prisma/integration-tests'])
if (!dryRun) {
console.log(`Let's first do a dry run!`)
await publishPackages(packages, publishOrder, true, prismaVersion, tag, args['--release'])
console.log(`Waiting 5 sec so you can check it out first...`)
await new Promise((r) => setTimeout(r, 5_000))
}
await publishPackages(packages, publishOrder, dryRun, prismaVersion, tag, args['--release'])
const enginesCommitHash = getEnginesCommitHash()
const enginesCommitInfo = await getCommitInfo('prisma-engines', enginesCommitHash)
const prismaCommitHash = await getLatestCommitHash('.')
const prismaCommitInfo = await getCommitInfo('prisma', prismaCommitHash)
if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `enginesCommitHash=${enginesCommitHash}\n`)
fs.appendFileSync(process.env.GITHUB_OUTPUT, `prismaCommitHash=${prismaCommitHash}\n`)
}
if (!args['--dry-run']) {
try {
await sendSlackMessage({
version: prismaVersion,
enginesCommitInfo,
prismaCommitInfo,
})
} catch (e) {
console.error(e)
}
}
}
}
function getEnginesCommitHash(): string {
const npmEnginesVersion = dependenciesPrismaEnginesPkg['@prisma/engines-version']
const sha1Pattern = /\b[0-9a-f]{5,40}\b/
const commitHash = npmEnginesVersion.match(sha1Pattern)![0]
return commitHash
}
/**
* Tests packages in "publishOrder"
* @param packages Packages
* @param publishOrder string[][]
*/
async function testPackages(packages: Packages, publishOrder: string[][]): Promise<void> {
const order = flatten(publishOrder)
console.log(bold(`\nRun ${cyan('tests')}. Testing order:`))
console.log(order)
for (const pkgName of order) {
const pkg = packages[pkgName]
if (pkg.packageJson.scripts.test) {
console.log(`\nTesting ${magenta(pkg.name)}`)
await run(path.dirname(pkg.path), 'pnpm run test')
} else {
console.log(`\nSkipping ${magenta(pkg.name)}, as it doesn't have tests`)
}
}
}
function flatten<T>(arr: T[][]): T[] {
return arr.reduce((acc, val) => acc.concat(val), [])
}
function intersection<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter((value) => arr2.includes(value))
}
// Thanks 🙏 to https://github.com/semver/semver/issues/232#issuecomment-405596809
const semverRegex =
/^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
function increaseMinor(version: string) {
const match = semverRegex.exec(version)
if (match?.groups) {
return `${match.groups.major}.${Number(match.groups.minor) + 1}.${match.groups.patch}`
}
return undefined
}
function filterPublishOrder(publishOrder: string[][], packages: string[]): string[][] {
return publishOrder.reduce<string[][]>((acc, curr) => {
if (Array.isArray(curr)) {
curr = curr.filter((pkg) => !packages.includes(pkg))
if (curr.length > 0) {
acc.push(curr)
}
} else if (!packages.includes(curr)) {
acc.push(curr)
}
return acc
}, [])
}
async function publishPackages(
packages: Packages,
// TODO: pnpm can calculate this for us when using `pnpm -r publish`
publishOrder: string[][],
dryRun: boolean,
prismaVersion: string,
tag: string,
releaseVersion?: string,
): Promise<void> {
// we need to release a new `prisma` CLI in all cases.
// if there is a change in prisma-client-js, it will also use this new version
const publishStr = dryRun ? `${bold('Dry publish')} ` : releaseVersion ? 'Releasing ' : 'Publishing '
if (releaseVersion) {
console.log(red(bold(`RELEASE. This will release ${underline(releaseVersion)} on latest!!!`)))
if (dryRun) {
console.log(red(bold(`But it's only a dry run, so don't worry.`)))
}
}
console.log(
blue(
`\n${bold(underline(prismaVersion))}: ${publishStr}(all) ${bold(
String(Object.values(packages).length),
)} packages. Publish order:`,
),
)
console.log(blue(publishOrder.map((o, i) => ` ${i + 1}. ${o.join(', ')}`).join('\n')))
if (releaseVersion) {
console.log(
red(
bold(
`\nThis will ${underline('release')} a new version of Prisma packages on latest: ${underline(prismaVersion)}`,
),
),
)
if (!dryRun) {
console.log(red('Are you sure you want to do this? We wait for 10s just in case...'))
await new Promise((r) => {
setTimeout(r, 10_000)
})
}
} else if (!dryRun) {
// For dev releases
console.log(`\nGiving you 5s to review the changes...`)
await new Promise((r) => {
setTimeout(r, 5_000)
})
}
for (const currentBatch of publishOrder) {
for (const pkgName of currentBatch) {
const pkg = packages[pkgName]
if (pkg.private) {
console.log(`Skipping ${magenta(pkg.name)} as it's private`)
continue
}
// @prisma/engines-version is published outside of this script
const packagesNotToPublish = ['@prisma/engines-version']
if (packagesNotToPublish.includes(pkgName)) {
continue
}
const pkgDir = path.dirname(pkg.path)
const newVersion = prismaVersion
console.log(`\nPublishing ${magenta(`${pkgName}@${newVersion}`)} ${dim(`on ${tag}`)}`)
// Why is this needed?
// Was introduced in the first version of this script on Apr 14, 2020
// https://github.com/prisma/prisma/commit/7d6a26c1777c59ee945356687673102de4b1fe55#diff-51cd3eaba5264dc956e45fabcc02d5d21d8a8c473bd1bd00a297f9f4550c115bR790-R797
const prismaDeps = [...pkg.uses, ...pkg.usesDev]
if (prismaDeps.length > 0) {
await pRetry(
async () => {
await run(pkgDir, `pnpm update ${prismaDeps.join(' ')} --filter "${pkgName}"`, dryRun)
},
{
retries: 6,
onFailedAttempt: (e) => {
console.error(e)
},
},
)
}
// set the version in package.json for current package
await writeVersion(pkgDir, newVersion, dryRun)
// For package `prisma`, get latest commit hash (that is being released)
// and put into `prisma.prismaCommit` in `package.json` before publishing
if (pkgName === 'prisma') {
const latestCommitHash = await getLatestCommitHash('.')
await writeToPkgJson(pkgDir, (pkg) => {
// Note: this is the only non-deprecated usage of `prisma` config in `package.json`.
// It's for internal usage only.
pkg.prisma.prismaCommit = latestCommitHash
})
}
if (!isSkipped(pkgName)) {
/*
* About `--no-git-checks`
* By default, `pnpm publish` will make some checks before actually publishing a new version of your package.
* The next checks will happen:
* - The current branch is your publish branch. The publish branch is `master` by default. This is configurable through the `publish-branch` setting.
* - Your working directory is clean (there are no uncommitted changes).
* - The branch is up-to-date.
*/
await run(pkgDir, `pnpm publish --no-git-checks --access public --tag ${tag}`, dryRun)
}
}
}
}
function isSkipped(pkgName) {
if (skipPackages && skipPackages.includes(pkgName)) {
return true
}
if (onlyPackages && !onlyPackages.includes(pkgName)) {
return true
}
return false
}
async function writeToPkgJson(pkgDir, cb: (pkg: any) => any, dryRun?: boolean) {
const pkgJsonPath = path.join(pkgDir, 'package.json')
const file = await fs.promises.readFile(pkgJsonPath, 'utf-8')
let packageJson = JSON.parse(file)
if (dryRun) {
console.log(`Would write to ${pkgJsonPath} from ${packageJson.version} now`)
} else {
const result = cb(packageJson)
if (result) {
packageJson = result
}
await fs.promises.writeFile(pkgJsonPath, JSON.stringify(packageJson, null, 2))
}
}
async function writeVersion(pkgDir: string, version: string, dryRun?: boolean) {
const pkgJsonPath = path.join(pkgDir, 'package.json')
const file = await fs.promises.readFile(pkgJsonPath, 'utf-8')
const packageJson = JSON.parse(file)
if (dryRun) {
console.log(`Would update ${pkgJsonPath} from ${packageJson.version} to ${version} now ${dim('(dry)')}`)
} else {
packageJson.version = version
await fs.promises.writeFile(pkgJsonPath, JSON.stringify(packageJson, null, 2))
}
}
async function getPrismaBranch(): Promise<string | undefined> {
if (process.env.GITHUB_REF_NAME) {
return process.env.GITHUB_REF_NAME
}
try {
// TODO: this can probably be simplified, we don't publish locally, remove?
return await runResult('.', 'git rev-parse --symbolic-full-name --abbrev-ref HEAD')
} catch (e) {}
return undefined
}
async function areEcosystemTestsPassing(tag: string): Promise<boolean> {
let svgUrl = 'https://github.com/prisma/ecosystem-tests/workflows/test/badge.svg?branch='
if (tag === 'patch-dev') {
svgUrl += tag
} else {
svgUrl += 'dev'
}
const res = await fetch(svgUrl).then((r) => r.text())
return res.includes('passing')
}
function getPatchBranch() {
if (process.env.GITHUB_REF_NAME) {
const versions = getSemverFromPatchBranch(process.env.GITHUB_REF_NAME)
console.debug('versions from patch branch:', versions)
if (versions !== undefined) {
return process.env.GITHUB_REF_NAME
}
}
return null
}
type CommitInfo = {
hash: string
message: string
author: string
}
type SlackMessageArgs = {
version: string
enginesCommitInfo: CommitInfo
prismaCommitInfo: CommitInfo
dryRun?: boolean
}
async function sendSlackMessage({ version, enginesCommitInfo, prismaCommitInfo, dryRun }: SlackMessageArgs) {
const webhook = new IncomingWebhook(process.env.SLACK_RELEASE_FEED_WEBHOOK!)
const dryRunStr = dryRun ? 'DRYRUN: ' : ''
const prismaLines = getLines(prismaCommitInfo.message)
const enginesLines = getLines(enginesCommitInfo.message)
const authoredByString = (author: string) => {
if (!author) return ''
return `Authored by ${prismaCommitInfo.author}`
}
await webhook.send(
`${dryRunStr}<https://www.npmjs.com/package/prisma/v/${version}|prisma@${version}> has just been released. Install via \`npm i -g prisma@${version}\` or \`npx prisma@${version}\`
What's shipped:
\`prisma/prisma\`
<https://github.com/prisma/prisma/commit/${prismaCommitInfo.hash}|${
prismaLines[0]
}\t\t\t\t- ${prismaCommitInfo.hash.slice(0, 7)}>
${prismaLines.join('\n')}${prismaLines.length > 1 ? '\n' : ''}${authoredByString(prismaCommitInfo.author)}
\`prisma/prisma-engines\`
<https://github.com/prisma/prisma-engines/commit/${enginesCommitInfo.hash}|${
enginesLines[0]
}\t\t\t\t- ${enginesCommitInfo.hash.slice(0, 7)}>
${enginesLines.join('\n')}${enginesLines.length > 1 ? '\n' : ''}${authoredByString(enginesCommitInfo.author)}`,
)
}
function getLines(str: string): string[] {
return str.split(/\r?\n|\r/)
}
type GitHubCommitInfo = {
sha: string
commit: {
author: {
name: string
email: string
date: string
} | null
committer: {
name: string
email: string
date: string
} | null
message: string
url: string
}
}
async function getCommitInfo(repo: string, hash: string): Promise<CommitInfo> {
// Example https://api.github.com/repos/prisma/prisma/commits/9d23845e98e34ec97f3013f5c2a3f85f57a828e2
// Doc https://docs.github.com/en/free-pro-team@latest/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
const response = await fetch(`https://api.github.com/repos/prisma/${repo}/commits/${hash}`)
const jsonData = (await response.json()) as GitHubCommitInfo
return {
message: jsonData.commit?.message || '',
author: jsonData.commit?.author?.name || '',
hash,
}
}
if (require.main === module) {
publish().catch((e) => {
console.error(red(bold('Error: ')) + (e.stack || e.message))
process.exit(1)
})
}