diff --git a/scripts/deploy/publish-npm.ts b/scripts/deploy/publish-npm.ts index 4d1300f815..676ae827c4 100644 --- a/scripts/deploy/publish-npm.ts +++ b/scripts/deploy/publish-npm.ts @@ -12,6 +12,15 @@ runMain(() => { }, }) + printLog('Building') + // Usually we don't need to build packages before publishing them, because yarn will call each + // `prepack` script to build packages during "yarn npm publish". + // + // But when things go wrong and some packages fail to be published, and we want to retry the job, + // yarn will skip already published packages (--tolerate-republish) so if any unpublished package + // depends on an already published one for their build, the build will fail. + command`yarn build`.withEnvironment({ BUILD_MODE: 'release' }).run() + printLog(dryRun ? 'Publishing (dry run)' : 'Publishing') command`yarn workspaces foreach --verbose --all --topological --no-private npm publish --tolerate-republish --access public ${dryRun ? ['--dry-run'] : []}` .withEnvironment({ diff --git a/scripts/release/renew-token.ts b/scripts/release/renew-token.ts index 0b3e2ee08a..e0c3a3e68c 100644 --- a/scripts/release/renew-token.ts +++ b/scripts/release/renew-token.ts @@ -3,7 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import { Writable } from 'node:stream' -import { printError, printLog, runMain } from '../lib/executionUtils.ts' +import { printError, printLog, printWarning, runMain } from '../lib/executionUtils.ts' import { findPackageJsonFiles } from '../lib/filesUtils.ts' import { command } from '../lib/command.ts' @@ -21,6 +21,27 @@ runMain(async () => { process.exit(1) } + const publishablePackages = findPublisheablePackages() + const missingPackages = await findMissingPackages(publishablePackages) + + if (missingPackages.length > 0) { + printWarning("The following packages don't exist yet on the npm registry:") + for (const packageName of missingPackages) { + printWarning(` - ${packageName}`) + } + printWarning( + 'Renewing the granular token with non-existing packages will fail. Placeholder packages must be published first.' + ) + + const answer = await prompt({ question: 'Publish placeholder packages now? [y/N] ' }) + if (answer.toLowerCase() !== 'y') { + printError('Aborting. Either publish the missing packages or mark them as private in their package.json.') + process.exit(1) + } + + publishPlaceholderPackages(missingPackages) + } + const password = await prompt({ question: 'npmjs.com password: ', showOutput: false }) const otp = await prompt({ question: 'npmjs.com OTP: ' }) @@ -53,7 +74,7 @@ runMain(async () => { token_description: 'Token used to publish Browser SDK packages (@datadog/browser-*) from the CI.', expires: 90, bypass_2fa: true, - packages: findPublisheablePackages(), + packages: publishablePackages, packages_and_scopes_permission: 'read-write', }, otp, @@ -123,6 +144,48 @@ async function callNpmApi({ return responseBody as T } +async function findMissingPackages(packageNames: string[]): Promise { + const results = await Promise.all( + packageNames.map(async (packageName) => { + const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`) + return response.status === 404 ? [packageName] : [] + }) + ) + return results.flat() +} + +function publishPlaceholderPackages(packageNames: string[]): void { + for (const packageName of packageNames) { + publishPlaceholderPackage(packageName) + } +} + +function publishPlaceholderPackage(packageName: string): void { + printLog(`Publishing placeholder for ${packageName}...`) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browser-sdk-placeholder-')) + try { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify( + { + name: packageName, + version: '0.0.0', + publishConfig: { access: 'public' }, + }, + null, + 2 + ) + ) + + fs.writeFileSync(path.join(tmpDir, 'README.md'), `Placeholder for package ${packageName}\n`) + + command`npm publish`.withCurrentWorkingDirectory(tmpDir).withLogs().run() + printLog(`Published placeholder for ${packageName}`) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } +} + function findPublisheablePackages() { const files = findPackageJsonFiles() return files