/
githubmirror
/
tldraw
Обзор
Документация
Войти
/
githubmirror
/
tldraw
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
internal/scripts/lib/publishing.ts
277 строк
8 KB
Kostya Farber
fix(infra): restore workspace:* rewrite in published tarballs (#8954)
29 май 2026, 12:46
Не верифицирован
29 май 2026, 12:46
bb652ec
Код
Авторство
О чём код?
import { execSync } from 'child_process' import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs' import path, { join } from 'path' import { Octokit } from '@octokit/rest' import { fetch } from 'cross-fetch' import { glob } from 'glob' import { parse } from 'semver' import { exec } from './exec' import { REPO_ROOT } from './file' import { nicelog } from './nicelog' import { getAllWorkspacePackages } from './workspace' export interface PackageDetails { name: string dir: string localDeps: string[] version: string } async function getPackageDetails(dir: string): Promise<PackageDetails | null> { const packageJsonPath = path.join(dir, 'package.json') if (!existsSync(packageJsonPath)) { return null } const packageJson = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')) if (packageJson.private) { return null } const workspacePackages = await getAllWorkspacePackages() return { name: packageJson.name, dir, version: packageJson.version, localDeps: Object.keys(packageJson.dependencies ?? {}).filter((dep) => workspacePackages.some((p) => p.name === dep) ), } } export async function getAllPackageDetails(): Promise<Record<string, PackageDetails>> { const dirs = readdirSync(join(REPO_ROOT, 'packages')) const details = await Promise.all( dirs.map((dir) => getPackageDetails(path.join(REPO_ROOT, 'packages', dir))) ) const results = details.filter((x): x is PackageDetails => Boolean(x)) return Object.fromEntries(results.map((result) => [result.name, result])) } export async function setAllVersions(version: string, options?: { stageChanges?: boolean }) { const packages = await getAllPackageDetails() for (const packageDetails of Object.values(packages)) { const manifest = JSON.parse(readFileSync(path.join(packageDetails.dir, 'package.json'), 'utf8')) manifest.version = version writeFileSync( path.join(packageDetails.dir, 'package.json'), JSON.stringify(manifest, null, '\t') + '\n' ) } await exec('yarn', ['refresh-assets', '--force'], { env: { ALLOW_REFRESH_ASSETS_CHANGES: '1' } }) const lernaJson = JSON.parse(readFileSync('lerna.json', 'utf8')) lernaJson.version = version writeFileSync('lerna.json', JSON.stringify(lernaJson, null, '\t') + '\n') execSync('yarn') if (options?.stageChanges) { await stageAllPackageJsonChanges() } } async function stageAllPackageJsonChanges() { // stage the changes const packageJsonFilesToAdd = [] for (const workspace of await getAllWorkspacePackages()) { if (workspace.relativePath.startsWith('packages/')) { packageJsonFilesToAdd.push(`${workspace.relativePath}/package.json`) } } const versionFilesToAdd = glob.sync('**/*/version.ts', { ignore: ['node_modules/**'], follow: false, }) console.log('versionFilesToAdd', versionFilesToAdd) await exec('git', [ 'add', '--update', 'lerna.json', ...packageJsonFilesToAdd, ...versionFilesToAdd, ]) } function assertExists<T>(v: T | null | undefined): T { if (v === null || v === undefined) throw new Error('Expected value to exist') return v } export async function getLatestTldrawVersionFromNpm({ versionPrefix, }: { versionPrefix?: string } = {}) { if (!versionPrefix) return assertExists(parse((await exec('npm', ['show', 'tldraw', 'version'])).trim())) const versions = (await exec('npm', ['show', 'tldraw@~' + versionPrefix, 'version'])).trim() if (versions.startsWith('tldraw')) { return assertExists(parse(versions.split('\n').pop()?.split(' ')[1].replaceAll("'", ''))) } return assertExists(parse(versions)) } function topologicalSortPackages(packages: Record<string, PackageDetails>) { const sorted: PackageDetails[] = [] const visited = new Set<string>() function visit(packageName: string, path: string[]) { if (visited.has(packageName)) { return } visited.add(packageName) const packageDetails = packages[packageName] if (!packageDetails) { throw new Error(`Could not find package ${packageName}. path: ${path.join(' -> ')}`) } packageDetails.localDeps.forEach((dep) => visit(dep, [...path, dep])) sorted.push(packageDetails) } Object.keys(packages).forEach((packageName) => visit(packageName, [packageName])) return sorted } export async function publish(distTag?: string) { // Authentication uses npm's trusted publisher OIDC flow. The publish job in // CI must grant `permissions: id-token: write` so yarn (>= 4.10, which we are // on via `packageManager`) can exchange the GitHub-issued OIDC token for a // short-lived publish token automatically. // // We invoke `yarn npm publish` rather than `npm publish` directly so that // yarn rewrites `workspace:*` dependency specifiers in the published // tarball into the concrete sibling versions. `npm publish` has no concept // of yarn's workspace protocol and would ship `"workspace:*"` literally, // breaking installs for any consumer outside this monorepo. // See https://docs.npmjs.com/trusted-publishers const packages = await getAllPackageDetails() const publishOrder = topologicalSortPackages(packages) for (const packageDetails of publishOrder) { const tag = distTag ?? parse(packageDetails.version)?.prerelease[0] ?? 'latest' nicelog( `Publishing ${packageDetails.name} with version ${packageDetails.version} under tag @${tag}` ) await retry( async () => { let output = '' const publishStart = Date.now() nicelog( `[publish] ${packageDetails.name}@${packageDetails.version} starting npm publish...` ) try { await exec( `yarn`, [ 'npm', 'publish', '--tag', String(tag), '--tolerate-republish', '--provenance', '--access', 'public', ], { pwd: packageDetails.dir, processStdoutLine: (line) => { output += line + '\n' nicelog(line) }, processStderrLine: (line) => { output += line + '\n' nicelog(line) }, } ) } catch (e) { if ( output.includes('cannot publish over the previously published versions') || output.includes('You cannot publish over the previously published versions') ) { nicelog( `[publish] ${packageDetails.name}@${packageDetails.version} already published, skipping` ) return } throw e } const elapsed = ((Date.now() - publishStart) / 1000).toFixed(1) nicelog( `[publish] ${packageDetails.name}@${packageDetails.version} npm publish done (${elapsed}s)` ) }, { delay: 10_000, numAttempts: 5, } ) await retry( async ({ attempt, total }) => { nicelog('Waiting for package to be published... attempt', attempt, 'of', total) // fetch the new package directly from the npm registry const newVersion = packageDetails.version const url = `https://registry.npmjs.org/${packageDetails.name}/${newVersion}` nicelog('looking for package at url: ', url) const res = await fetch(url, { method: 'HEAD', }) if (res.status >= 400) { throw new Error(`Package not found: ${res.status}`) } }, { delay: 10000, numAttempts: 50, } ) } } function retry( fn: (args: { attempt: number; remaining: number; total: number }) => Promise<void>, opts: { numAttempts: number delay: number } ): Promise<void> { return new Promise((resolve, reject) => { let attempts = 0 function attempt() { fn({ attempt: attempts, remaining: opts.numAttempts - attempts, total: opts.numAttempts }) .then(resolve) .catch((err) => { attempts++ if (attempts >= opts.numAttempts) { reject(err) } else { setTimeout(attempt, opts.delay) } }) } attempt() }) } export async function publishProductionDocsAndExamplesAndBemo({ gitRef = 'HEAD', }: { gitRef?: string } = {}) { await exec('git', ['push', 'origin', `${gitRef}:docs-production`, `--force`]) await exec('git', ['push', 'origin', `${gitRef}:bemo-production`, `--force`]) } export async function triggerBumpVersionsWorkflow(ghToken: string) { const octokit = new Octokit({ auth: ghToken }) await octokit.rest.actions.createWorkflowDispatch({ owner: 'tldraw', repo: 'tldraw', workflow_id: 'bump-versions.yml', ref: 'main', }) }