From 78ad910672d62733273f65140ddf8f058550581c Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Wed, 26 Feb 2025 10:04:34 +0100 Subject: [PATCH] refactor(build): restructure build helpers to use classes MONGOSH-2007 (#2374) --- .../src/homebrew/publish-to-homebrew.spec.ts | 77 ++-- .../build/src/homebrew/publish-to-homebrew.ts | 134 ++++--- packages/build/src/npm-packages/bump.spec.ts | 44 ++- packages/build/src/npm-packages/bump.ts | 227 ++++++----- packages/build/src/npm-packages/index.ts | 6 +- packages/build/src/npm-packages/list.spec.ts | 66 ---- packages/build/src/npm-packages/list.ts | 18 - .../build/src/npm-packages/publish.spec.ts | 286 +++++++++++++- packages/build/src/npm-packages/publish.ts | 202 +++++++--- .../build/src/npm-packages/push-tags.spec.ts | 244 ------------ packages/build/src/npm-packages/push-tags.ts | 97 ----- packages/build/src/npm-packages/types.ts | 11 + packages/build/src/publish-auxiliary.ts | 8 +- packages/build/src/publish-mongosh.spec.ts | 372 ++++++------------ packages/build/src/publish-mongosh.ts | 298 +++++++------- packages/build/src/release.ts | 41 +- packages/build/src/run-draft.spec.ts | 25 +- packages/build/src/run-draft.ts | 16 +- 18 files changed, 989 insertions(+), 1183 deletions(-) delete mode 100644 packages/build/src/npm-packages/list.spec.ts delete mode 100644 packages/build/src/npm-packages/list.ts delete mode 100644 packages/build/src/npm-packages/push-tags.spec.ts delete mode 100644 packages/build/src/npm-packages/push-tags.ts create mode 100644 packages/build/src/npm-packages/types.ts diff --git a/packages/build/src/homebrew/publish-to-homebrew.spec.ts b/packages/build/src/homebrew/publish-to-homebrew.spec.ts index 203d28175b..c1ed2be1fd 100644 --- a/packages/build/src/homebrew/publish-to-homebrew.spec.ts +++ b/packages/build/src/homebrew/publish-to-homebrew.spec.ts @@ -1,11 +1,12 @@ import chai, { expect } from 'chai'; import sinon from 'sinon'; import type { GithubRepo } from '@mongodb-js/devtools-github-repo'; -import { publishToHomebrew } from './publish-to-homebrew'; +import type { HomebrewPublisherConfig } from './publish-to-homebrew'; +import { HomebrewPublisher } from './publish-to-homebrew'; chai.use(require('sinon-chai')); -describe('Homebrew publish-to-homebrew', function () { +describe('HomebrewPublisher', function () { let homebrewCore: GithubRepo; let homebrewCoreFork: GithubRepo; let createPullRequest: sinon.SinonStub; @@ -13,11 +14,12 @@ describe('Homebrew publish-to-homebrew', function () { let generateFormula: sinon.SinonStub; let updateHomebrewFork: sinon.SinonStub; - beforeEach(function () { + let testPublisher: HomebrewPublisher; + + const setupHomebrewPublisher = ( + config: Omit + ) => { createPullRequest = sinon.stub(); - httpsSha256 = sinon.stub(); - generateFormula = sinon.stub(); - updateHomebrewFork = sinon.stub(); homebrewCore = { repo: { @@ -32,9 +34,33 @@ describe('Homebrew publish-to-homebrew', function () { repo: 'homebrew-core', }, } as unknown as GithubRepo; + + testPublisher = new HomebrewPublisher({ + ...config, + homebrewCore, + homebrewCoreFork, + }); + + httpsSha256 = sinon.stub(testPublisher, 'httpsSha256'); + generateFormula = sinon.stub(testPublisher, 'generateFormula'); + updateHomebrewFork = sinon.stub(testPublisher, 'updateHomebrewFork'); + }; + + beforeEach(function () { + setupHomebrewPublisher({ + packageVersion: '1.0.0', + githubReleaseLink: 'githubRelease', + isDryRun: false, + }); }); it('creates and merges a PR on update and cleans up', async function () { + setupHomebrewPublisher({ + packageVersion: '1.0.0', + githubReleaseLink: 'githubRelease', + isDryRun: false, + }); + httpsSha256 .rejects() .withArgs( @@ -69,16 +95,7 @@ describe('Homebrew publish-to-homebrew', function () { ) .resolves({ prNumber: 42, url: 'url' }); - await publishToHomebrew( - homebrewCore, - homebrewCoreFork, - '1.0.0', - 'githubRelease', - false, - httpsSha256, - generateFormula, - updateHomebrewFork - ); + await testPublisher.publish(); expect(httpsSha256).to.have.been.called; expect(generateFormula).to.have.been.called; @@ -87,6 +104,12 @@ describe('Homebrew publish-to-homebrew', function () { }); it('does not try to push/merge when there is no formula update', async function () { + setupHomebrewPublisher({ + packageVersion: '1.0.0', + githubReleaseLink: 'githubRelease', + isDryRun: false, + }); + httpsSha256 .rejects() .withArgs( @@ -111,16 +134,7 @@ describe('Homebrew publish-to-homebrew', function () { }) .resolves(undefined); - await publishToHomebrew( - homebrewCore, - homebrewCoreFork, - '1.0.0', - 'githubRelease', - false, - httpsSha256, - generateFormula, - updateHomebrewFork - ); + await testPublisher.publish(); expect(httpsSha256).to.have.been.called; expect(generateFormula).to.have.been.called; @@ -163,16 +177,7 @@ describe('Homebrew publish-to-homebrew', function () { ) .resolves({ prNumber: 42, url: 'url' }); - await publishToHomebrew( - homebrewCore, - homebrewCoreFork, - '1.0.0', - 'githubRelease', - false, - httpsSha256, - generateFormula, - updateHomebrewFork - ); + await testPublisher.publish(); expect(httpsSha256).to.have.been.called; expect(generateFormula).to.have.been.called; diff --git a/packages/build/src/homebrew/publish-to-homebrew.ts b/packages/build/src/homebrew/publish-to-homebrew.ts index ca08a814aa..e09e74601f 100644 --- a/packages/build/src/homebrew/publish-to-homebrew.ts +++ b/packages/build/src/homebrew/publish-to-homebrew.ts @@ -1,60 +1,88 @@ import type { GithubRepo } from '@mongodb-js/devtools-github-repo'; -import { generateUpdatedFormula } from './generate-formula'; -import { updateHomebrewFork } from './update-homebrew-fork'; -import { httpsSha256 } from './utils'; - -export async function publishToHomebrew( - homebrewCore: GithubRepo, - homebrewCoreFork: GithubRepo, - packageVersion: string, - githubReleaseLink: string, - isDryRun: boolean, - httpsSha256Fn = httpsSha256, - generateFormulaFn = generateUpdatedFormula, - updateHomebrewForkFn = updateHomebrewFork -): Promise { - const cliReplPackageUrl = `https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-${packageVersion}.tgz`; - const packageSha = isDryRun - ? `dryRun-fakesha256-${Date.now()}` - : await httpsSha256Fn(cliReplPackageUrl); - - const homebrewFormula = await generateFormulaFn( - { version: packageVersion, sha: packageSha }, - homebrewCore, - isDryRun - ); - if (!homebrewFormula) { - console.warn('There are no changes to the homebrew formula'); - return; - } +import { generateUpdatedFormula as generateUpdatedFormulaFn } from './generate-formula'; +import { updateHomebrewFork as updateHomebrewForkFn } from './update-homebrew-fork'; +import { httpsSha256 as httpsSha256Fn } from './utils'; + +export type HomebrewPublisherConfig = { + homebrewCore: GithubRepo; + homebrewCoreFork: GithubRepo; + packageVersion: string; + githubReleaseLink: string; + isDryRun?: boolean; +}; - const forkBranch = await updateHomebrewForkFn({ - packageVersion, - packageSha, - homebrewFormula, - homebrewCore, - homebrewCoreFork, - isDryRun, - }); - if (!forkBranch) { - console.warn('There are no changes to the homebrew formula'); - return; +export class HomebrewPublisher { + readonly httpsSha256: typeof httpsSha256Fn; + readonly generateFormula: typeof generateUpdatedFormulaFn; + readonly updateHomebrewFork: typeof updateHomebrewForkFn; + + constructor( + public config: HomebrewPublisherConfig, + { + httpsSha256 = httpsSha256Fn, + generateFormula = generateUpdatedFormulaFn, + updateHomebrewFork = updateHomebrewForkFn, + } = {} + ) { + this.httpsSha256 = httpsSha256; + this.generateFormula = generateFormula; + this.updateHomebrewFork = updateHomebrewFork; } - const description = `This PR was created automatically and bumps \`mongosh\` to the latest published version \`${packageVersion}\`.\n\nFor additional details see ${githubReleaseLink}.`; + async publish(): Promise { + const { + isDryRun, + homebrewCore, + packageVersion, + homebrewCoreFork, + githubReleaseLink, + } = this.config; + + const cliReplPackageUrl = `https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-${packageVersion}.tgz`; + const packageSha = isDryRun + ? `dryRun-fakesha256-${Date.now()}` + : await this.httpsSha256(cliReplPackageUrl); + + const homebrewFormula = await this.generateFormula( + { version: packageVersion, sha: packageSha }, + homebrewCore, + isDryRun || false + ); + if (!homebrewFormula) { + console.warn('There are no changes to the homebrew formula'); + return; + } + + const forkBranch = await this.updateHomebrewFork({ + packageVersion, + packageSha, + homebrewFormula, + homebrewCore, + homebrewCoreFork, + isDryRun: isDryRun || false, + }); + if (!forkBranch) { + console.warn('There are no changes to the homebrew formula'); + return; + } + + const description = `This PR was created automatically and bumps \`mongosh\` to the latest published version \`${packageVersion}\`.\n\nFor additional details see ${githubReleaseLink}.`; - if (isDryRun) { - await homebrewCoreFork.deleteBranch(forkBranch); - console.warn('Deleted branch instead of creating homebrew PR'); - return; + if (isDryRun) { + await homebrewCoreFork.deleteBranch(forkBranch); + console.warn('Deleted branch instead of creating homebrew PR'); + return; + } + const pr = await homebrewCore.createPullRequest( + `mongosh ${packageVersion}`, + description, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${homebrewCoreFork.repo.owner}:${forkBranch}`, + 'master' + ); + console.info( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Created PR #${pr.prNumber} in ${homebrewCore.repo.owner}/${homebrewCore.repo.repo}: ${pr.url}` + ); } - const pr = await homebrewCore.createPullRequest( - `mongosh ${packageVersion}`, - description, - `${homebrewCoreFork.repo.owner}:${forkBranch}`, - 'master' - ); - console.info( - `Created PR #${pr.prNumber} in ${homebrewCore.repo.owner}/${homebrewCore.repo.repo}: ${pr.url}` - ); } diff --git a/packages/build/src/npm-packages/bump.spec.ts b/packages/build/src/npm-packages/bump.spec.ts index fc3a85c471..ea788ba58a 100644 --- a/packages/build/src/npm-packages/bump.spec.ts +++ b/packages/build/src/npm-packages/bump.spec.ts @@ -1,15 +1,12 @@ import { expect } from 'chai'; import type { SinonStub } from 'sinon'; import sinon from 'sinon'; -import { - bumpMongoshReleasePackages, - updateShellApiMongoshVersion, -} from './bump'; +import { PackageBumper } from './bump'; import { promises as fs } from 'fs'; import path from 'path'; import { PROJECT_ROOT } from './constants'; -describe('npm-packages bump', function () { +describe('PackageBumper', function () { let fsWriteFile: SinonStub; const shellApiSrc = path.join( PROJECT_ROOT, @@ -19,9 +16,22 @@ describe('npm-packages bump', function () { 'mongosh-version.ts' ); + let testBumper: PackageBumper; + let getPackagesInTopologicalOrder: sinon.SinonStub; + beforeEach(function () { fsWriteFile = sinon.stub(fs, 'writeFile'); fsWriteFile.resolves(); + + const spawnSync = sinon.stub(); + spawnSync.resolves(); + + getPackagesInTopologicalOrder = sinon.stub(); + + testBumper = new PackageBumper({ + spawnSync, + getPackagesInTopologicalOrder, + }); }); afterEach(function () { @@ -30,15 +40,13 @@ describe('npm-packages bump', function () { describe('bumpMongoshReleasePackages', function () { let fsReadFile: SinonStub; - let getPackagesInTopologicalOrder: sinon.SinonStub; beforeEach(function () { fsReadFile = sinon.stub(fs, 'readFile'); - getPackagesInTopologicalOrder = sinon.stub(); }); it('warns and does not run if version is not set', async function () { const consoleWarnSpy = sinon.spy(console, 'warn'); - await bumpMongoshReleasePackages(''); + await testBumper.bumpMongoshReleasePackages(''); expect(consoleWarnSpy).calledOnceWith( 'mongosh: Release version not specified. Skipping mongosh bump.' ); @@ -54,10 +62,6 @@ describe('npm-packages bump', function () { 'packages', 'autocomplete' ); - getPackagesInTopologicalOrder.resolves([ - { name: 'mongosh', location: mongoshPath }, - { name: '@mongosh/autocomplete', location: autocompletePath }, - ]); const rootProjectJson = path.join(PROJECT_ROOT, 'package.json'); const mongoshProjectJson = path.join(mongoshPath, 'package.json'); @@ -106,12 +110,14 @@ describe('npm-packages bump', function () { fsReadFile.withArgs(file, 'utf8').resolves(JSON.stringify(json)); } - const updateShellApiMongoshVersion = sinon.stub(); - await bumpMongoshReleasePackages( - '9.9.9', - getPackagesInTopologicalOrder, - updateShellApiMongoshVersion - ); + getPackagesInTopologicalOrder.resolves([ + { name: 'mongosh', location: mongoshPath }, + { name: '@mongosh/autocomplete', location: autocompletePath }, + ]); + + sinon.stub(testBumper, 'updateShellApiMongoshVersion').resolves(); + + await testBumper.bumpMongoshReleasePackages('9.9.9'); expect(fsWriteFile).callCount(3); expect( @@ -168,7 +174,7 @@ describe('npm-packages bump', function () { export const MONGOSH_VERSION = '2.3.8';`); const newVersion = '3.0.0'; - await updateShellApiMongoshVersion(newVersion); + await testBumper.updateShellApiMongoshVersion(newVersion); expect(fsWriteFile).calledWith( shellApiSrc, diff --git a/packages/build/src/npm-packages/bump.ts b/packages/build/src/npm-packages/bump.ts index fe2a4df559..68cfb73464 100644 --- a/packages/build/src/npm-packages/bump.ts +++ b/packages/build/src/npm-packages/bump.ts @@ -1,147 +1,144 @@ -import { spawnSync } from '../helpers'; import { EXCLUDE_RELEASE_PACKAGES, MONGOSH_RELEASE_PACKAGES, PROJECT_ROOT, } from './constants'; - import { promises as fs } from 'fs'; import path from 'path'; import { spawnSync as spawnSyncFn } from '../helpers'; import { getPackagesInTopologicalOrder as getPackagesInTopologicalOrderFn } from '@mongodb-js/monorepo-tools'; -/** Bumps only the main mongosh release packages to the set version. */ -export async function bumpMongoshReleasePackages( - version: string, - getPackagesInTopologicalOrder: typeof getPackagesInTopologicalOrderFn = getPackagesInTopologicalOrderFn, - updateShellApiMongoshVersionFn: typeof updateShellApiMongoshVersion = updateShellApiMongoshVersion -): Promise { - if (!version) { - console.warn( - 'mongosh: Release version not specified. Skipping mongosh bump.' - ); - return; +export class PackageBumper { + private readonly getPackagesInTopologicalOrder: typeof getPackagesInTopologicalOrderFn; + private readonly spawnSync: typeof spawnSyncFn; + + constructor({ + getPackagesInTopologicalOrder = getPackagesInTopologicalOrderFn, + spawnSync = spawnSyncFn, + } = {}) { + this.getPackagesInTopologicalOrder = getPackagesInTopologicalOrder; + this.spawnSync = spawnSync; } - console.info(`mongosh: Bumping mongosh release packages to ${version}`); - const monorepoRootPath = PROJECT_ROOT; - const packages = await getPackagesInTopologicalOrder(monorepoRootPath); + /** Bumps only the main mongosh release packages to the set version. */ + public async bumpMongoshReleasePackages(version: string): Promise { + if (!version) { + console.warn( + 'mongosh: Release version not specified. Skipping mongosh bump.' + ); + return; + } - const bumpedPackages = MONGOSH_RELEASE_PACKAGES; + console.info(`mongosh: Bumping mongosh release packages to ${version}`); + const monorepoRootPath = PROJECT_ROOT; + const packages = await this.getPackagesInTopologicalOrder(monorepoRootPath); - const locations = [monorepoRootPath, ...packages.map((p) => p.location)]; + const bumpedPackages = MONGOSH_RELEASE_PACKAGES; - for (const location of locations) { - const packageJsonPath = path.join(location, 'package.json'); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); + const locations = [monorepoRootPath, ...packages.map((p) => p.location)]; - if ( - bumpedPackages.includes(packageJson.name as string) && - location !== monorepoRootPath - ) { - packageJson.version = version; - } - for (const grouping of [ - 'dependencies', - 'devDependencies', - 'optionalDependencies', - 'peerDependencies', - ]) { - if (!packageJson[grouping]) { - continue; - } + for (const location of locations) { + const packageJsonPath = path.join(location, 'package.json'); + const packageJson = JSON.parse( + await fs.readFile(packageJsonPath, 'utf8') + ); - for (const name of Object.keys(packageJson[grouping])) { - if (!bumpedPackages.includes(name)) { + if ( + bumpedPackages.includes(packageJson.name as string) && + location !== monorepoRootPath + ) { + packageJson.version = version; + } + for (const grouping of [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + ]) { + if (!packageJson[grouping]) { continue; } - packageJson[grouping][name] = version; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + for (const name of Object.keys(packageJson[grouping])) { + if (!bumpedPackages.includes(name)) { + continue; + } + packageJson[grouping][name] = version; + } } + + await fs.writeFile( + packageJsonPath, + JSON.stringify(packageJson, null, 2) + '\n' + ); } - await fs.writeFile( - packageJsonPath, - JSON.stringify(packageJson, null, 2) + '\n' - ); + await this.updateShellApiMongoshVersion(version); + + // Update package-lock.json + this.spawnSync('npm', ['install', '--package-lock-only'], { + stdio: 'inherit', + cwd: monorepoRootPath, + encoding: 'utf8', + }); } - await updateShellApiMongoshVersionFn(version); + /** Updates the shell-api constant to match the mongosh version. */ + public async updateShellApiMongoshVersion(version: string) { + const shellApiVersionFilePath = path.join( + PROJECT_ROOT, + 'packages', + 'shell-api', + 'src', + 'mongosh-version.ts' + ); - // Update package-lock.json - spawnSync('npm', ['install', '--package-lock-only'], { - stdio: 'inherit', - cwd: monorepoRootPath, - encoding: 'utf8', - }); -} + const versionFileContent = await fs.readFile( + shellApiVersionFilePath, + 'utf-8' + ); -/** Updates the shell-api constant to match the mongosh version. */ -export async function updateShellApiMongoshVersion(version: string) { - const shellApiVersionFilePath = path.join( - PROJECT_ROOT, - 'packages', - 'shell-api', - 'src', - 'mongosh-version.ts' - ); - - const versionFileContent = await fs.readFile( - shellApiVersionFilePath, - 'utf-8' - ); - - // Write the updated content back to the mongosh-version file - await fs.writeFile( - shellApiVersionFilePath, - // Replace the version inside MONGOSH_VERSION = '...' - versionFileContent.replace( - /MONGOSH_VERSION = '.*'/, - `MONGOSH_VERSION = '${version}'` - ), - 'utf-8' - ); -} + // Write the updated content back to the mongosh-version file + await fs.writeFile( + shellApiVersionFilePath, + // Replace the version inside MONGOSH_VERSION = '...' + versionFileContent.replace( + /MONGOSH_VERSION = '.*'/, + `MONGOSH_VERSION = '${version}'` + ), + 'utf-8' + ); + } -/** Bumps auxiliary packages without setting a new version of mongosh. */ -export function bumpAuxiliaryPackages() { - spawnSync('bump-monorepo-packages', [], { - stdio: 'inherit', - cwd: PROJECT_ROOT, - encoding: 'utf8', - env: { - ...process.env, - LAST_BUMP_COMMIT_MESSAGE: 'chore(release): bump packages', - SKIP_BUMP_PACKAGES: [ - ...EXCLUDE_RELEASE_PACKAGES, - ...MONGOSH_RELEASE_PACKAGES, - ].join(','), - }, - }); -} + /** Bumps auxiliary packages without setting a new version of mongosh. */ + public bumpAuxiliaryPackages() { + this.spawnSync('bump-monorepo-packages', [], { + stdio: 'inherit', + cwd: PROJECT_ROOT, + encoding: 'utf8', + env: { + ...process.env, + LAST_BUMP_COMMIT_MESSAGE: 'chore(release): bump packages', + SKIP_BUMP_PACKAGES: [ + ...EXCLUDE_RELEASE_PACKAGES, + ...MONGOSH_RELEASE_PACKAGES, + ].join(','), + }, + }); + } -export function commitBumpedPackages( - { useAuxiliaryPackagesOnly }: { useAuxiliaryPackagesOnly: boolean }, - spawnSync: typeof spawnSyncFn = spawnSyncFn -) { - spawnSync('git', ['add', '.'], { - stdio: 'inherit', - cwd: PROJECT_ROOT, - encoding: 'utf8', - }); - - spawnSync( - 'git', - [ - 'commit', - '-m', - `chore(release): bump packages for ${ - useAuxiliaryPackagesOnly ? 'auxiliary' : 'mongosh' - } release`, - ], - { + public commitBumpedPackages() { + this.spawnSync('git', ['add', '.'], { stdio: 'inherit', cwd: PROJECT_ROOT, encoding: 'utf8', - } - ); + }); + + this.spawnSync('git', ['commit', '-m', 'chore(release): bump packages'], { + stdio: 'inherit', + cwd: PROJECT_ROOT, + encoding: 'utf8', + }); + } } diff --git a/packages/build/src/npm-packages/index.ts b/packages/build/src/npm-packages/index.ts index 15076eea34..fcb5c606fb 100644 --- a/packages/build/src/npm-packages/index.ts +++ b/packages/build/src/npm-packages/index.ts @@ -1,3 +1,3 @@ -export { bumpAuxiliaryPackages, bumpMongoshReleasePackages } from './bump'; -export { publishToNpm } from './publish'; -export { pushTags } from './push-tags'; +export { PackageBumper } from './bump'; +export { PackagePublisher } from './publish'; +export { PackagePublisherConfig } from './types'; diff --git a/packages/build/src/npm-packages/list.spec.ts b/packages/build/src/npm-packages/list.spec.ts deleted file mode 100644 index d8ab29600d..0000000000 --- a/packages/build/src/npm-packages/list.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { expect } from 'chai'; -import path from 'path'; -import type { SinonStub } from 'sinon'; -import sinon from 'sinon'; -import type { LernaPackageDescription } from './list'; -import { listNpmPackages } from './list'; -import { markBumpedFilesAsAssumeUnchanged } from './publish'; - -describe('npm-packages list', function () { - before(function () { - if (process.version.startsWith('v16.')) return this.skip(); - }); - - describe('listNpmPackages', function () { - it('lists packages', function () { - const packages = listNpmPackages(); - expect(packages.length).to.be.greaterThan(1); - for (const { name, version } of packages) { - expect(name).to.be.a('string'); - expect(version).to.be.a('string'); - } - }); - }); - - describe('markBumpedFilesAsAssumeUnchanged', function () { - let packages: LernaPackageDescription[]; - let expectedFiles: string[]; - let spawnSync: SinonStub; - - beforeEach(function () { - expectedFiles = [ - path.resolve(__dirname, '..', '..', '..', '..', 'lerna.json'), - path.resolve(__dirname, '..', '..', '..', '..', 'package.json'), - path.resolve(__dirname, '..', '..', '..', '..', 'package-lock.json'), - ]; - packages = listNpmPackages(); - for (const { location } of packages) { - expectedFiles.push(path.resolve(location, 'package.json')); - } - - spawnSync = sinon.stub(); - }); - - it('marks files with --assume-unchanged', function () { - markBumpedFilesAsAssumeUnchanged(packages, true, spawnSync); - expectedFiles.forEach((f) => { - expect(spawnSync).to.have.been.calledWith( - 'git', - ['update-index', '--assume-unchanged', f], - sinon.match.any - ); - }); - }); - - it('marks files with --no-assume-unchanged', function () { - markBumpedFilesAsAssumeUnchanged(packages, false, spawnSync); - expectedFiles.forEach((f) => { - expect(spawnSync).to.have.been.calledWith( - 'git', - ['update-index', '--no-assume-unchanged', f], - sinon.match.any - ); - }); - }); - }); -}); diff --git a/packages/build/src/npm-packages/list.ts b/packages/build/src/npm-packages/list.ts deleted file mode 100644 index 1b4b896b6b..0000000000 --- a/packages/build/src/npm-packages/list.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { LERNA_BIN, PROJECT_ROOT } from './constants'; -import { spawnSync } from '../helpers/spawn-sync'; - -export interface LernaPackageDescription { - name: string; - version: string; - private: boolean; - location: string; -} - -export function listNpmPackages(): LernaPackageDescription[] { - const lernaListOutput = spawnSync(LERNA_BIN, ['list', '--json', '--all'], { - cwd: PROJECT_ROOT, - encoding: 'utf8', - }); - - return JSON.parse(lernaListOutput.stdout); -} diff --git a/packages/build/src/npm-packages/publish.spec.ts b/packages/build/src/npm-packages/publish.spec.ts index a682238e55..c19e9989c8 100644 --- a/packages/build/src/npm-packages/publish.spec.ts +++ b/packages/build/src/npm-packages/publish.spec.ts @@ -2,10 +2,16 @@ import { expect } from 'chai'; import path from 'path'; import type { SinonStub } from 'sinon'; import sinon from 'sinon'; -import { publishToNpm } from './publish'; +import { PackagePublisher } from './publish'; +import { MONGOSH_RELEASE_PACKAGES } from './constants'; +import type { PackagePublisherConfig } from './types'; -describe('npm-packages publishToNpm', function () { +describe('PackagePublisher', function () { let spawnSync: SinonStub; + let listNpmPackages: SinonStub; + let existsTag: SinonStub; + let testPublisher: PackagePublisher; + const lernaBin = path.resolve( __dirname, '..', @@ -17,25 +23,267 @@ describe('npm-packages publishToNpm', function () { 'lerna' ); - beforeEach(function () { + const mongoshVersion = '1.2.0'; + const allReleasablePackages = [ + { name: 'packageA', version: '0.7.0' }, + { name: 'packageB', version: '1.7.0' }, + { name: 'packageC', version: '1.3.0' }, + { name: 'mongosh', version: mongoshVersion }, + { name: '@mongosh/cli-repl', version: mongoshVersion }, + ]; + const auxiliaryPackages = allReleasablePackages.filter( + (p) => !MONGOSH_RELEASE_PACKAGES.includes(p.name) + ); + const mongoshReleasePackages = allReleasablePackages.filter((p) => + MONGOSH_RELEASE_PACKAGES.includes(p.name) + ); + + function setupTestPublisher( + config: PackagePublisherConfig, + { + existsTagStub = sinon.stub(), + }: { + existsTagStub?: SinonStub | null; + } = {} + ) { spawnSync = sinon.stub(); + spawnSync.returns(undefined); + + testPublisher = new PackagePublisher(config, { spawnSync }); + + listNpmPackages = sinon.stub(testPublisher, 'listNpmPackages'); + listNpmPackages.returns(allReleasablePackages); + + if (existsTagStub) { + console.log('setting up...'); + sinon.replace(testPublisher, 'existsTag', existsTagStub); + existsTag = existsTagStub; + existsTagStub.returns(false); + } + } + + describe('publish()', function () { + beforeEach(function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: false }); + }); + + it('calls lerna to publish packages for a real version', function () { + testPublisher.publishToNpm(); + + expect(spawnSync).to.have.been.calledWith( + lernaBin, + [ + 'publish', + 'from-package', + '--no-private', + '--no-changelog', + '--exact', + '--yes', + '--no-verify-access', + ], + sinon.match.any + ); + }); }); - it('calls lerna to publish packages for a real version', function () { - publishToNpm({ isDryRun: false }, spawnSync); - - expect(spawnSync).to.have.been.calledWith( - lernaBin, - [ - 'publish', - 'from-package', - '--no-private', - '--no-changelog', - '--exact', - '--yes', - '--no-verify-access', - ], - sinon.match.any - ); + describe('pushTags()', function () { + describe('with specific configurations', function () { + it('skips tag push if it is a dry run', function () { + setupTestPublisher({ + isDryRun: true, + useAuxiliaryPackagesOnly: false, + }); + + existsTag.withArgs(`v${mongoshVersion}`).returns(true); + + testPublisher.pushTags(); + + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `v${mongoshVersion}`, + '-m', + `v${mongoshVersion}`, + ]); + + expect(spawnSync).not.calledWith('git', ['push', '--tags']); + }); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('throws if mongosh is not existent when publishing all', function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: false }); + + const packages = [{ name: 'packageA', version: '0.7.0' }]; + listNpmPackages.returns(packages); + + expect(() => testPublisher.pushTags()).throws( + 'mongosh package not found' + ); + }); + + it('takes mongosh version and pushes tags when releasing', function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: false }); + + testPublisher.pushTags(); + + for (const packageInfo of allReleasablePackages) { + expect(spawnSync).calledWith('git', [ + 'tag', + '-a', + `${packageInfo.name}@${packageInfo.version}`, + '-m', + `${packageInfo.name}@${packageInfo.version}`, + ]); + } + + expect(spawnSync).calledWith('git', [ + 'tag', + '-a', + `v${mongoshVersion}`, + '-m', + `v${mongoshVersion}`, + ]); + expect(spawnSync).calledWith('git', ['push', '--tags']); + }); + + it('pushes only package tags when using auxiliary packages', function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: true }); + + testPublisher.pushTags(); + + for (const packageInfo of auxiliaryPackages) { + expect(spawnSync).calledWith('git', [ + 'tag', + '-a', + `${packageInfo.name}@${packageInfo.version}`, + '-m', + `${packageInfo.name}@${packageInfo.version}`, + ]); + } + + for (const packageInfo of mongoshReleasePackages) { + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `${packageInfo.name}@${packageInfo.version}`, + '-m', + `${packageInfo.name}@${packageInfo.version}`, + ]); + } + + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `v${mongoshVersion}`, + '-m', + `v${mongoshVersion}`, + ]); + expect(spawnSync).calledWith('git', ['push', '--tags']); + }); + + it('skips pushing version tags which already exist', function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: true }); + + const packagesToSkip = [ + allReleasablePackages[0], + allReleasablePackages[1], + ]; + + for (const packageInfo of packagesToSkip) { + existsTag + .withArgs(`${packageInfo.name}@${packageInfo.version}`) + .returns(true); + } + + testPublisher.pushTags(); + + for (const packageInfo of auxiliaryPackages.filter( + (p) => !packagesToSkip.includes(p) + )) { + expect(spawnSync).calledWith('git', [ + 'tag', + '-a', + `${packageInfo.name}@${packageInfo.version}`, + '-m', + `${packageInfo.name}@${packageInfo.version}`, + ]); + } + + for (const packageInfo of [ + ...mongoshReleasePackages, + ...packagesToSkip, + ]) { + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `${packageInfo.name}@${packageInfo.version}`, + '-m', + `${packageInfo.name}@${packageInfo.version}`, + ]); + } + + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `v${mongoshVersion}`, + '-m', + `v${mongoshVersion}`, + ]); + expect(spawnSync).calledWith('git', ['push', '--tags']); + }); + + it('skips mongosh release tag push if it exists', function () { + setupTestPublisher({ isDryRun: false, useAuxiliaryPackagesOnly: false }); + + existsTag.withArgs(`v${mongoshVersion}`).returns(true); + + testPublisher.pushTags(); + + expect(spawnSync).not.calledWith('git', [ + 'tag', + '-a', + `v${mongoshVersion}`, + '-m', + `v${mongoshVersion}`, + ]); + expect(spawnSync).calledWith('git', ['push', '--tags']); + }); + }); + + describe('existsTag()', function () { + beforeEach(function () { + setupTestPublisher({}, { existsTagStub: null }); + }); + + it('returns true with existing tags', function () { + spawnSync.returns({ status: 0 }); + expect(testPublisher.existsTag('v1.0.0')).equals(true); + }); + + it('return false with tags that do not exist', function () { + spawnSync.returns({ status: 1 }); + expect(testPublisher.existsTag('this-tag-will-never-exist-12345')).equals( + false + ); + }); + }); + + describe('listNpmPackages()', function () { + before(function () { + if (process.version.startsWith('v16.')) return this.skip(); + }); + + it('lists packages', function () { + const packages = testPublisher.listNpmPackages(); + expect(packages.length).to.be.greaterThan(1); + for (const { name, version } of packages) { + expect(name).to.be.a('string'); + expect(version).to.be.a('string'); + } + }); }); }); diff --git a/packages/build/src/npm-packages/publish.ts b/packages/build/src/npm-packages/publish.ts index dd320b85b0..5ade8454f6 100644 --- a/packages/build/src/npm-packages/publish.ts +++ b/packages/build/src/npm-packages/publish.ts @@ -1,76 +1,152 @@ -import path from 'path'; -import { LERNA_BIN, PROJECT_ROOT } from './constants'; -import type { LernaPackageDescription } from './list'; +import { + EXCLUDE_RELEASE_PACKAGES, + LERNA_BIN, + MONGOSH_RELEASE_PACKAGES, + PROJECT_ROOT, +} from './constants'; import { spawnSync as spawnSyncFn } from '../helpers/spawn-sync'; -import type { SpawnSyncOptionsWithStringEncoding } from 'child_process'; - -export function publishToNpm( - { isDryRun = false }, - spawnSync: typeof spawnSyncFn = spawnSyncFn -): void { - const commandOptions: SpawnSyncOptionsWithStringEncoding = { - stdio: 'inherit', - cwd: PROJECT_ROOT, - encoding: 'utf8', - env: { - ...process.env, - ...(isDryRun ? { npm_config_dry_run: 'true' } : {}), - }, - }; - - // There seems to be a bug where lerna does not run prepublish topologically - // during the publish step, causing build errors. This ensures all packages are topologically - // compiled beforehand. - spawnSync(LERNA_BIN, ['run', 'prepublish', '--sort'], commandOptions); - - // Lerna requires a clean repository for a publish from-package - // we use git update-index --assume-unchanged on files we know have been bumped - spawnSync( - LERNA_BIN, - [ - 'publish', - 'from-package', - '--no-private', - '--no-changelog', - '--exact', - '--yes', - '--no-verify-access', - ], - commandOptions - ); -} +import { type SpawnSyncOptionsWithStringEncoding } from 'child_process'; +import type { LernaPackageDescription, PackagePublisherConfig } from './types'; + +export class PackagePublisher { + readonly config: PackagePublisherConfig; + private readonly spawnSync: typeof spawnSyncFn; -export function markBumpedFilesAsAssumeUnchanged( - packages: LernaPackageDescription[], - assumeUnchanged: boolean, - spawnSync: typeof spawnSyncFn = spawnSyncFn -): void { - const filesToAssume = [ - path.resolve(PROJECT_ROOT, 'lerna.json'), - path.resolve(PROJECT_ROOT, 'package.json'), - path.resolve(PROJECT_ROOT, 'package-lock.json'), - ]; - for (const { location } of packages) { - filesToAssume.push(path.resolve(location, 'package.json')); + constructor( + config: PackagePublisherConfig, + { spawnSync = spawnSyncFn } = {} + ) { + this.config = config; + this.spawnSync = spawnSync; } - for (const f of filesToAssume) { - spawnSync( - 'git', + public publishToNpm(): void { + const commandOptions: SpawnSyncOptionsWithStringEncoding = { + stdio: 'inherit', + cwd: PROJECT_ROOT, + encoding: 'utf8', + env: { + ...process.env, + ...(this.config.isDryRun ? { npm_config_dry_run: 'true' } : {}), + }, + }; + + // There seems to be a bug where lerna does not run prepublish topologically + // during the publish step, causing build errors. This ensures all packages are topologically + // compiled beforehand. + this.spawnSync(LERNA_BIN, ['run', 'prepublish', '--sort'], commandOptions); + + // Lerna requires a clean repository for a publish from-package + // we use git update-index --assume-unchanged on files we know have been bumped + this.spawnSync( + LERNA_BIN, [ - 'update-index', - assumeUnchanged ? '--assume-unchanged' : '--no-assume-unchanged', - f, + 'publish', + 'from-package', + '--no-private', + '--no-changelog', + '--exact', + '--yes', + '--no-verify-access', ], + commandOptions + ); + } + + public listNpmPackages(): LernaPackageDescription[] { + const lernaListOutput = this.spawnSync( + LERNA_BIN, + ['list', '--json', '--all'], { - stdio: 'inherit', cwd: PROJECT_ROOT, encoding: 'utf8', - }, - true + } ); - console.info( - `File ${f} is now ${assumeUnchanged ? '' : 'NOT '}assumed to be unchanged` + + return JSON.parse(lernaListOutput.stdout); + } + + public pushTags() { + const allReleasablePackages = this.listNpmPackages().filter( + (packageConfig) => !EXCLUDE_RELEASE_PACKAGES.includes(packageConfig.name) ); + + const packages: LernaPackageDescription[] = this.config + .useAuxiliaryPackagesOnly + ? allReleasablePackages.filter( + (packageConfig) => + !MONGOSH_RELEASE_PACKAGES.includes(packageConfig.name) + ) + : allReleasablePackages; + + const commandOptions: SpawnSyncOptionsWithStringEncoding = { + stdio: 'inherit', + cwd: PROJECT_ROOT, + encoding: 'utf8', + env: { + ...process.env, + }, + }; + + for (const packageInfo of packages) { + const { name, version } = packageInfo; + const newTag = `${name}@${version}`; + + if (this.existsTag(newTag)) { + console.warn(`${newTag} tag already exists. Skipping...`); + continue; + } + this.spawnSync( + 'git', + ['tag', '-a', newTag, '-m', newTag], + commandOptions + ); + } + + if (!this.config.useAuxiliaryPackagesOnly) { + const mongoshVersion = packages.find( + (packageConfig) => packageConfig.name === 'mongosh' + )?.version; + + if (!mongoshVersion) { + throw new Error('mongosh package not found'); + } + + const newVersionTag = `v${mongoshVersion}`; + + if (!this.existsTag(newVersionTag)) { + console.info(`Creating v${mongoshVersion} tag...`); + this.spawnSync( + 'git', + ['tag', '-a', newVersionTag, '-m', newVersionTag], + commandOptions + ); + } else { + console.warn(`${newVersionTag} tag already exists. Skipping...`); + } + } + + if (!this.config.isDryRun) { + this.spawnSync('git', ['push', '--tags'], commandOptions); + } + } + /** Returns true if the tag exists in the remote repository. */ + public existsTag(tag: string): boolean { + // rev-parse will return the hash of tagged commit + // if it exists or throw otherwise. + try { + const revParseResult = this.spawnSync( + 'git', + ['rev-parse', '--quiet', '--verify', `refs/tags/${tag}`], + { + cwd: PROJECT_ROOT, + encoding: 'utf8', + stdio: 'pipe', + } + ); + return revParseResult.status === 0; + } catch (error) { + return false; + } } } diff --git a/packages/build/src/npm-packages/push-tags.spec.ts b/packages/build/src/npm-packages/push-tags.spec.ts deleted file mode 100644 index 0d135e0655..0000000000 --- a/packages/build/src/npm-packages/push-tags.spec.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { expect } from 'chai'; -import { existsTag, pushTags } from './push-tags'; -import sinon from 'sinon'; -import { MONGOSH_RELEASE_PACKAGES } from './constants'; - -describe('pushing tags', function () { - let spawnSync: sinon.SinonStub; - let listNpmPackages: sinon.SinonStub; - - describe('existsTag', function () { - it('returns true with existing tags', function () { - expect(existsTag('v1.0.0')).equals(true); - }); - - it('return false with tags that do not exist', function () { - expect(existsTag('this-tag-will-never-exist-12345')).equals(false); - }); - }); - - describe('pushTags', function () { - const mongoshVersion = '1.2.0'; - const allReleasablePackages = [ - { name: 'packageA', version: '0.7.0' }, - { name: 'packageB', version: '1.7.0' }, - { name: 'packageC', version: '1.3.0' }, - { name: 'mongosh', version: mongoshVersion }, - { name: '@mongosh/cli-repl', version: mongoshVersion }, - ]; - const auxiliaryPackages = allReleasablePackages.filter( - (p) => !MONGOSH_RELEASE_PACKAGES.includes(p.name) - ); - const mongoshReleasePackages = allReleasablePackages.filter((p) => - MONGOSH_RELEASE_PACKAGES.includes(p.name) - ); - let existsVersionTag: sinon.SinonStub; - - beforeEach(function () { - spawnSync = sinon.stub(); - spawnSync.returns(undefined); - - listNpmPackages = sinon.stub(); - listNpmPackages.returns(allReleasablePackages); - existsVersionTag = sinon.stub(); - existsVersionTag.returns(false); - }); - - afterEach(function () { - sinon.restore(); - }); - - it('throws if mongosh is not existent when publishing all', function () { - const packages = [{ name: 'packageA', version: '0.7.0' }]; - listNpmPackages.returns(packages); - - expect(() => - pushTags( - { - isDryRun: false, - useAuxiliaryPackagesOnly: false, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ) - ).throws('mongosh package not found'); - }); - - it('takes mongosh version and pushes tags when releasing', function () { - pushTags( - { - isDryRun: false, - useAuxiliaryPackagesOnly: false, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ); - - for (const packageInfo of allReleasablePackages) { - expect(spawnSync).calledWith('git', [ - 'tag', - '-a', - `${packageInfo.name}@${packageInfo.version}`, - '-m', - `${packageInfo.name}@${packageInfo.version}`, - ]); - } - - expect(spawnSync).calledWith('git', [ - 'tag', - '-a', - `v${mongoshVersion}`, - '-m', - `v${mongoshVersion}`, - ]); - expect(spawnSync).calledWith('git', ['push', '--tags']); - }); - - it('pushes only package tags when using auxiliary packages', function () { - pushTags( - { - isDryRun: false, - useAuxiliaryPackagesOnly: true, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ); - - for (const packageInfo of auxiliaryPackages) { - expect(spawnSync).calledWith('git', [ - 'tag', - '-a', - `${packageInfo.name}@${packageInfo.version}`, - '-m', - `${packageInfo.name}@${packageInfo.version}`, - ]); - } - - for (const packageInfo of mongoshReleasePackages) { - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `${packageInfo.name}@${packageInfo.version}`, - '-m', - `${packageInfo.name}@${packageInfo.version}`, - ]); - } - - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `v${mongoshVersion}`, - '-m', - `v${mongoshVersion}`, - ]); - expect(spawnSync).calledWith('git', ['push', '--tags']); - }); - - it('skips pushing version tags which already exist', function () { - const packagesToSkip = [ - allReleasablePackages[0], - allReleasablePackages[1], - ]; - - for (const packageInfo of packagesToSkip) { - existsVersionTag - .withArgs(`${packageInfo.name}@${packageInfo.version}`) - .returns(true); - } - - pushTags( - { - isDryRun: false, - useAuxiliaryPackagesOnly: true, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ); - - for (const packageInfo of auxiliaryPackages.filter( - (p) => !packagesToSkip.includes(p) - )) { - expect(spawnSync).calledWith('git', [ - 'tag', - '-a', - `${packageInfo.name}@${packageInfo.version}`, - '-m', - `${packageInfo.name}@${packageInfo.version}`, - ]); - } - - for (const packageInfo of [ - ...mongoshReleasePackages, - ...packagesToSkip, - ]) { - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `${packageInfo.name}@${packageInfo.version}`, - '-m', - `${packageInfo.name}@${packageInfo.version}`, - ]); - } - - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `v${mongoshVersion}`, - '-m', - `v${mongoshVersion}`, - ]); - expect(spawnSync).calledWith('git', ['push', '--tags']); - }); - - it('skips mongosh release tag push if it exists', function () { - existsVersionTag.withArgs(`v${mongoshVersion}`).returns(true); - - pushTags( - { - useAuxiliaryPackagesOnly: false, - isDryRun: false, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ); - - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `v${mongoshVersion}`, - '-m', - `v${mongoshVersion}`, - ]); - expect(spawnSync).calledWith('git', ['push', '--tags']); - }); - - it('skips tag push if it is a dry run', function () { - existsVersionTag.withArgs(`v${mongoshVersion}`).returns(true); - - pushTags( - { - useAuxiliaryPackagesOnly: false, - isDryRun: true, - }, - listNpmPackages, - existsVersionTag, - spawnSync - ); - - expect(spawnSync).not.calledWith('git', [ - 'tag', - '-a', - `v${mongoshVersion}`, - '-m', - `v${mongoshVersion}`, - ]); - - expect(spawnSync).not.calledWith('git', ['push', '--tags']); - }); - }); -}); diff --git a/packages/build/src/npm-packages/push-tags.ts b/packages/build/src/npm-packages/push-tags.ts deleted file mode 100644 index 1a20a05157..0000000000 --- a/packages/build/src/npm-packages/push-tags.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { SpawnSyncOptionsWithStringEncoding } from 'child_process'; -import { - EXCLUDE_RELEASE_PACKAGES, - MONGOSH_RELEASE_PACKAGES, - PROJECT_ROOT, -} from './constants'; -import type { LernaPackageDescription } from './list'; -import { listNpmPackages as listNpmPackagesFn } from './list'; -import { spawnSync as spawnSyncFn } from '../helpers/spawn-sync'; - -export function pushTags( - { - useAuxiliaryPackagesOnly, - isDryRun, - }: { useAuxiliaryPackagesOnly: boolean; isDryRun: boolean }, - listNpmPackages: typeof listNpmPackagesFn = listNpmPackagesFn, - existsVersionTag: typeof existsTag = existsTag, - spawnSync: typeof spawnSyncFn = spawnSyncFn -) { - const allReleasablePackages = listNpmPackages().filter( - (packageConfig) => !EXCLUDE_RELEASE_PACKAGES.includes(packageConfig.name) - ); - - const packages: LernaPackageDescription[] = useAuxiliaryPackagesOnly - ? allReleasablePackages.filter( - (packageConfig) => - !MONGOSH_RELEASE_PACKAGES.includes(packageConfig.name) - ) - : allReleasablePackages; - - const commandOptions: SpawnSyncOptionsWithStringEncoding = { - stdio: 'inherit', - cwd: PROJECT_ROOT, - encoding: 'utf8', - env: { - ...process.env, - }, - }; - - for (const packageInfo of packages) { - const { name, version } = packageInfo; - const newTag = `${name}@${version}`; - - if (existsVersionTag(newTag)) { - console.warn(`${newTag} tag already exists. Skipping...`); - continue; - } - spawnSync('git', ['tag', '-a', newTag, '-m', newTag], commandOptions); - } - - if (!useAuxiliaryPackagesOnly) { - const mongoshVersion = packages.find( - (packageConfig) => packageConfig.name === 'mongosh' - )?.version; - - if (!mongoshVersion) { - throw new Error('mongosh package not found'); - } - - const newVersionTag = `v${mongoshVersion}`; - - if (!existsVersionTag(newVersionTag)) { - console.info(`Creating v${mongoshVersion} tag...`); - spawnSync( - 'git', - ['tag', '-a', newVersionTag, '-m', newVersionTag], - commandOptions - ); - } else { - console.warn(`${newVersionTag} tag already exists. Skipping...`); - } - } - - if (!isDryRun) { - spawnSync('git', ['push', '--tags'], commandOptions); - } -} - -/** Returns true if the tag exists in the remote repository. */ -export function existsTag(tag: string): boolean { - // rev-parse will return the hash of tagged commit - // if it exists or throw otherwise. - try { - const revParseResult = spawnSyncFn( - 'git', - ['rev-parse', '--quiet', '--verify', `refs/tags/${tag}`], - { - cwd: PROJECT_ROOT, - encoding: 'utf8', - stdio: 'pipe', - } - ); - return revParseResult.status === 0; - } catch (error) { - return false; - } -} diff --git a/packages/build/src/npm-packages/types.ts b/packages/build/src/npm-packages/types.ts new file mode 100644 index 0000000000..dca4c69157 --- /dev/null +++ b/packages/build/src/npm-packages/types.ts @@ -0,0 +1,11 @@ +export type PackagePublisherConfig = { + isDryRun?: boolean; + useAuxiliaryPackagesOnly?: boolean; +}; + +export interface LernaPackageDescription { + name: string; + version: string; + private: boolean; + location: string; +} diff --git a/packages/build/src/publish-auxiliary.ts b/packages/build/src/publish-auxiliary.ts index 8be5a20fb2..f7681a4b51 100644 --- a/packages/build/src/publish-auxiliary.ts +++ b/packages/build/src/publish-auxiliary.ts @@ -1,5 +1,5 @@ import type { Config } from './config'; -import { publishToNpm, pushTags } from './npm-packages'; +import { PackagePublisher } from './npm-packages'; export function publishAuxiliaryPackages(config: Config) { if (!config.useAuxiliaryPackagesOnly) { @@ -7,9 +7,11 @@ export function publishAuxiliaryPackages(config: Config) { 'This should only be used when publishing auxiliary packages' ); } - pushTags({ + const publisher = new PackagePublisher({ useAuxiliaryPackagesOnly: true, isDryRun: config.isDryRun || false, }); - publishToNpm(config); + + publisher.pushTags(); + publisher.publishToNpm(); } diff --git a/packages/build/src/publish-mongosh.spec.ts b/packages/build/src/publish-mongosh.spec.ts index d1014b21cf..3902556709 100644 --- a/packages/build/src/publish-mongosh.spec.ts +++ b/packages/build/src/publish-mongosh.spec.ts @@ -1,25 +1,12 @@ import chai, { expect } from 'chai'; import sinon from 'sinon'; -import type { writeBuildInfo as writeBuildInfoType } from './build-info'; import { Barque } from './barque'; -import type { - Config, - shouldDoPublicRelease as shouldDoPublicReleaseFn, -} from './config'; -import type { createAndPublishDownloadCenterConfig as createAndPublishDownloadCenterConfigFn } from './download-center'; +import { type Config } from './config'; import { GithubRepo } from '@mongodb-js/devtools-github-repo'; -import type { publishToHomebrew as publishToHomebrewType } from './homebrew'; -import { - type publishToNpm as publishToNpmType, - type pushTags as pushTagsType, -} from './npm-packages'; -import type { - bumpMongoshReleasePackages as bumpMongoshReleasePackagesFn, - bumpAuxiliaryPackages as bumpAuxiliaryPackagesFn, -} from './npm-packages'; -import { publishMongosh } from './publish-mongosh'; +import { HomebrewPublisher } from './homebrew'; +import { PackageBumper, PackagePublisher } from './npm-packages'; +import { MongoshPublisher } from './publish-mongosh'; import { dummyConfig } from '../test/helpers'; -import { getArtifactUrl } from './evergreen'; chai.use(require('sinon-chai')); @@ -34,49 +21,98 @@ function createStubBarque(overrides?: any): Barque { return sinon.createStubInstance(Barque, overrides) as unknown as Barque; } -describe('publishMongosh', function () { +describe('MongoshPublisher', function () { let config: Config; - let createAndPublishDownloadCenterConfig: typeof createAndPublishDownloadCenterConfigFn; - let publishToNpm: typeof publishToNpmType; - let writeBuildInfo: typeof writeBuildInfoType; - let publishToHomebrew: typeof publishToHomebrewType; - let shouldDoPublicRelease: typeof shouldDoPublicReleaseFn; - let bumpMongoshReleasePackages: typeof bumpMongoshReleasePackagesFn; - let bumpAuxiliaryPackages: typeof bumpAuxiliaryPackagesFn; - let githubRepo: GithubRepo; - let mongoHomebrewCoreForkRepo: GithubRepo; - let homebrewCoreRepo: GithubRepo; + let createAndPublishDownloadCenterConfig: sinon.SinonStub; + let publishToNpm: sinon.SinonStub; + let writeBuildInfo: sinon.SinonStub; + let publishToHomebrew: sinon.SinonStub; let barque: Barque; - let pushTags: typeof pushTagsType; - const getEvergreenArtifactUrl = getArtifactUrl; let spawnSync: sinon.SinonStub; + let shouldDoPublicRelease: sinon.SinonStub; - beforeEach(function () { - config = { ...dummyConfig }; + let testPublisher: MongoshPublisher; - createAndPublishDownloadCenterConfig = sinon.spy(); - publishToNpm = sinon.spy(); - writeBuildInfo = sinon.spy(); - publishToHomebrew = sinon.spy(); - shouldDoPublicRelease = sinon.spy(); - pushTags = sinon.spy(); - bumpMongoshReleasePackages = sinon.spy(); - bumpAuxiliaryPackages = sinon.spy(); - spawnSync = sinon.stub().resolves(); - - githubRepo = createStubRepo(); - mongoHomebrewCoreForkRepo = createStubRepo(); - homebrewCoreRepo = createStubRepo(); + const setupMongoshPublisher = ( + config: Config, + { + githubRepo = createStubRepo(), + homebrewCore = createStubRepo(), + homebrewCoreFork = createStubRepo(), + } = {} + ) => { barque = createStubBarque({ releaseToBarque: sinon.stub().resolves(['package-url']), waitUntilPackagesAreAvailable: sinon.stub().resolves(), }); + + spawnSync = sinon.stub().resolves(); + + const packagePublisher = new PackagePublisher(config, { + spawnSync, + }); + publishToNpm = sinon.stub(packagePublisher, 'publishToNpm'); + + const packageBumper = new PackageBumper({ + spawnSync, + }); + + const bumpMongoshReleasePackages = sinon.stub( + packageBumper, + 'bumpMongoshReleasePackages' + ); + bumpMongoshReleasePackages.resolves(); + + const bumpAuxiliaryPackages = sinon.stub( + packageBumper, + 'bumpAuxiliaryPackages' + ); + bumpAuxiliaryPackages.resolves(); + + const homebrewPublisher = new HomebrewPublisher({ + ...config, + homebrewCore, + homebrewCoreFork, + packageVersion: config.version, + githubReleaseLink: 'test-link', + }); + publishToHomebrew = sinon.stub(homebrewPublisher, 'publish'); + + writeBuildInfo = sinon.stub(); + writeBuildInfo.resolves(); + + shouldDoPublicRelease = sinon.stub(); + shouldDoPublicRelease.returns(true); + + createAndPublishDownloadCenterConfig = sinon.stub(); + + testPublisher = new MongoshPublisher( + config, + barque, + githubRepo, + packagePublisher, + packageBumper, + homebrewPublisher, + { + writeBuildInfo, + shouldDoPublicRelease, + createAndPublishDownloadCenterConfig, + } + ); + + const pushTags = sinon.stub(packagePublisher, 'pushTags'); + pushTags.resolves(); + }; + + beforeEach(function () { + config = { ...dummyConfig }; }); context('if is a public release', function () { + let githubRepo: GithubRepo; + beforeEach(function () { config.triggeringGitTag = 'v0.7.0'; - shouldDoPublicRelease = sinon.stub().returns(true); githubRepo = createStubRepo({ getMostRecentDraftTagForRelease: sinon .stub() @@ -88,31 +124,20 @@ describe('publishMongosh', function () { repo: 'mongosh', }, }); + + setupMongoshPublisher(config, { githubRepo }); + + shouldDoPublicRelease.returns(true); }); context('validates configuration', function () { it('fails if no draft tag is found', async function () { - githubRepo = createStubRepo({ + const githubRepo = createStubRepo({ getMostRecentDraftTagForRelease: sinon.stub().resolves(undefined), }); + setupMongoshPublisher(config, { githubRepo }); try { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); } catch (e: any) { return expect(e.message).to.contain('Could not find prior draft tag'); } @@ -120,29 +145,14 @@ describe('publishMongosh', function () { }); it('fails if draft tag SHA does not match revision', async function () { - githubRepo = createStubRepo({ + const githubRepo = createStubRepo({ getMostRecentDraftTagForRelease: sinon .stub() .resolves({ name: 'v0.7.0-draft.42', sha: 'wrong' }), }); + setupMongoshPublisher(config, { githubRepo }); try { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); } catch (e: any) { return expect(e.message).to.contain('Version mismatch'); } @@ -151,23 +161,7 @@ describe('publishMongosh', function () { }); it('publishes artifacts to barque', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(barque.releaseToBarque).to.have.been.callCount(26); expect(barque.releaseToBarque).to.have.been.calledWith( @@ -186,23 +180,7 @@ describe('publishMongosh', function () { }); it('updates the download center config', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(createAndPublishDownloadCenterConfig).to.have.been.calledWith( config.outputDir, @@ -213,191 +191,67 @@ describe('publishMongosh', function () { }); it('promotes the release in github', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(githubRepo.promoteRelease).to.have.been.calledWith(config); }); it('writes analytics config and then publishes NPM packages', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(writeBuildInfo).to.have.been.calledOnceWith(config); expect(publishToNpm).to.have.been.calledWith(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument expect(publishToNpm).to.have.been.calledAfter(writeBuildInfo as any); }); it('publishes to homebrew', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); - expect(publishToHomebrew).to.have.been.calledWith( - homebrewCoreRepo, - mongoHomebrewCoreForkRepo, - config.version - ); - expect(publishToHomebrew).to.have.been.calledAfter( + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(testPublisher.homebrewPublisher.publish).to.have.been.called; + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(testPublisher.homebrewPublisher.publish).to.have.been.calledAfter( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument githubRepo.promoteRelease as any ); }); }); context('if is not a public release', function () { + let githubRepo: GithubRepo; beforeEach(function () { - shouldDoPublicRelease = sinon.stub().returns(false); + githubRepo = createStubRepo(); + + setupMongoshPublisher(config, { githubRepo }); + shouldDoPublicRelease.returns(false); }); it('does not update the download center config', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(createAndPublishDownloadCenterConfig).not.to.have.been.called; }); it('does not promote the release in github', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(githubRepo.promoteRelease).not.to.have.been.called; }); it('does not publish npm packages', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(publishToNpm).not.to.have.been.called; }); it('does not publish to homebrew', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(publishToHomebrew).not.to.have.been.called; }); it('does not release to barque', async function () { - await publishMongosh( - config, - githubRepo, - mongoHomebrewCoreForkRepo, - homebrewCoreRepo, - barque, - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew, - shouldDoPublicRelease, - getEvergreenArtifactUrl, - bumpMongoshReleasePackages, - bumpAuxiliaryPackages, - spawnSync - ); + await testPublisher.publish(); expect(barque.releaseToBarque).not.to.have.been.called; }); diff --git a/packages/build/src/publish-mongosh.ts b/packages/build/src/publish-mongosh.ts index 6bd27ee6d8..79472f1372 100644 --- a/packages/build/src/publish-mongosh.ts +++ b/packages/build/src/publish-mongosh.ts @@ -1,164 +1,194 @@ -import type { writeBuildInfo as writeBuildInfoType } from './build-info'; -import type { Barque } from './barque'; +import { writeBuildInfo as writeBuildInfoFn } from './build-info'; +import { Barque } from './barque'; import type { Config } from './config'; import { ALL_PACKAGE_VARIANTS, getReleaseVersionFromTag, shouldDoPublicRelease as shouldDoPublicReleaseFn, } from './config'; -import type { createAndPublishDownloadCenterConfig as createAndPublishDownloadCenterConfigFn } from './download-center'; +import { createAndPublishDownloadCenterConfig as createAndPublishDownloadCenterConfigFn } from './download-center'; import { getArtifactUrl as getArtifactUrlFn } from './evergreen'; -import type { GithubRepo } from '@mongodb-js/devtools-github-repo'; -import type { publishToHomebrew as publishToHomebrewType } from './homebrew'; -import type { pushTags as pushTagsType } from './npm-packages'; -import { type publishToNpm as publishToNpmType } from './npm-packages'; +import { GithubRepo } from '@mongodb-js/devtools-github-repo'; import type { PackageInformationProvider } from './packaging'; import { getPackageFile } from './packaging'; -import { - bumpMongoshReleasePackages as bumpMongoshReleasePackagesFn, - bumpAuxiliaryPackages as bumpAuxiliaryPackagesFn, -} from './npm-packages'; -import { commitBumpedPackages } from './npm-packages/bump'; -import { spawnSync as spawnSyncFn } from './helpers'; - -export async function publishMongosh( - config: Config, - mongoshGithubRepo: GithubRepo, - mongodbHomebrewForkGithubRepo: GithubRepo, - homebrewCoreGithubRepo: GithubRepo, - barque: Barque, - createAndPublishDownloadCenterConfig: typeof createAndPublishDownloadCenterConfigFn, - publishToNpm: typeof publishToNpmType, - pushTags: typeof pushTagsType, - writeBuildInfo: typeof writeBuildInfoType, - publishToHomebrew: typeof publishToHomebrewType, - shouldDoPublicRelease: typeof shouldDoPublicReleaseFn = shouldDoPublicReleaseFn, - getEvergreenArtifactUrl: typeof getArtifactUrlFn = getArtifactUrlFn, - bumpMongoshReleasePackages: typeof bumpMongoshReleasePackagesFn = bumpMongoshReleasePackagesFn, - bumpAuxiliaryPackages: typeof bumpAuxiliaryPackagesFn = bumpAuxiliaryPackagesFn, - spawnSync: typeof spawnSyncFn = spawnSyncFn -): Promise { - if (!shouldDoPublicRelease(config)) { - console.warn( - 'mongosh: Not triggering publish - configuration does not match a public release!' - ); - return; - } +import { PackagePublisher } from './npm-packages'; +import { PackageBumper } from './npm-packages/bump'; +import { HomebrewPublisher } from './homebrew'; +import type { Octokit } from '@octokit/rest'; - if (config.isDryRun) { - console.warn('Performing dry-run publish only'); - } - - const releaseVersion = getReleaseVersionFromTag(config.triggeringGitTag); - const latestDraftTag = - await mongoshGithubRepo.getMostRecentDraftTagForRelease(releaseVersion); - if (!latestDraftTag || !releaseVersion) { - throw new Error( - `Could not find prior draft tag for release version: ${releaseVersion}` - ); - } - if (latestDraftTag.sha !== config.revision) { - throw new Error( - `Version mismatch - latest draft tag was for revision ${latestDraftTag.sha}, current revision is ${config.revision}` - ); - } - - console.info( - 'mongosh: Re-using artifacts from most recent draft tag', - latestDraftTag.name +export async function publishMongosh(config: Config, octokit: Octokit) { + const githubRepo = new GithubRepo(config.repo, octokit); + const homebrewCoreRepo = new GithubRepo( + { owner: 'Homebrew', repo: 'homebrew-core' }, + octokit + ); + const mongoHomebrewForkRepo = new GithubRepo( + { owner: 'mongodb-js', repo: 'homebrew-core' }, + octokit ); - bumpAuxiliaryPackages(); - await bumpMongoshReleasePackages(releaseVersion); - commitBumpedPackages({ useAuxiliaryPackagesOnly: false }, spawnSync); - pushTags({ - useAuxiliaryPackagesOnly: false, + const homebrewPublisher = new HomebrewPublisher({ + homebrewCore: homebrewCoreRepo, + homebrewCoreFork: mongoHomebrewForkRepo, + packageVersion: config.version, + githubReleaseLink: `https://github.com/${githubRepo.repo.owner}/${githubRepo.repo.repo}/releases/tag/v${config.version}`, isDryRun: config.isDryRun || false, }); - await publishArtifactsToBarque( - barque, - config.project as string, - releaseVersion, - latestDraftTag.name, - config.packageInformation as PackageInformationProvider, - !!config.isDryRun, - getEvergreenArtifactUrl + const publisher = new MongoshPublisher( + config, + new Barque(config), + githubRepo, + new PackagePublisher(config), + new PackageBumper(), + homebrewPublisher ); - await createAndPublishDownloadCenterConfig( - config.outputDir, - config.packageInformation as PackageInformationProvider, - config.downloadCenterAwsKey || '', - config.downloadCenterAwsSecret || '', - config.injectedJsonFeedFile || '', - !!config.isDryRun, - config.ctaConfig - ); + await publisher.publish(); +} - await mongoshGithubRepo.promoteRelease(config); +export class MongoshPublisher { + private readonly getEvergreenArtifactUrl: typeof getArtifactUrlFn; + private readonly writeBuildInfo: typeof writeBuildInfoFn; + private readonly createAndPublishDownloadCenterConfig: typeof createAndPublishDownloadCenterConfigFn; + private readonly shouldDoPublicRelease: typeof shouldDoPublicReleaseFn; - // ensures the segment api key to be present in the published packages - await writeBuildInfo(config, 'packaged'); + constructor( + public config: Config, + public barque: Barque, + public mongoshGithubRepo: GithubRepo, + public packagePublisher: PackagePublisher, + public packageBumper: PackageBumper, + public homebrewPublisher: HomebrewPublisher, + { + getEvergreenArtifactUrl = getArtifactUrlFn, + writeBuildInfo = writeBuildInfoFn, + createAndPublishDownloadCenterConfig = createAndPublishDownloadCenterConfigFn, + shouldDoPublicRelease = shouldDoPublicReleaseFn, + } = {} + ) { + this.getEvergreenArtifactUrl = getEvergreenArtifactUrl; + this.writeBuildInfo = writeBuildInfo; + this.createAndPublishDownloadCenterConfig = + createAndPublishDownloadCenterConfig; + this.shouldDoPublicRelease = shouldDoPublicRelease; + } - publishToNpm({ - isDryRun: config.isDryRun, - }); + async publish(): Promise { + const { config, mongoshGithubRepo } = this; - await publishToHomebrew( - homebrewCoreGithubRepo, - mongodbHomebrewForkGithubRepo, - config.version, - `https://github.com/${mongoshGithubRepo.repo.owner}/${mongoshGithubRepo.repo.repo}/releases/tag/v${config.version}`, - !!config.isDryRun - ); + if (!this.shouldDoPublicRelease(config)) { + console.warn( + 'mongosh: Not triggering publish - configuration does not match a public release!' + ); + return; + } - console.info('mongosh: finished release process.'); -} + if (config.isDryRun) { + console.warn('Performing dry-run publish only'); + } + + const releaseVersion = getReleaseVersionFromTag(config.triggeringGitTag); + const latestDraftTag = + await this.mongoshGithubRepo.getMostRecentDraftTagForRelease( + releaseVersion + ); + if (!latestDraftTag || !releaseVersion) { + throw new Error( + `Could not find prior draft tag for release version: ${releaseVersion}` + ); + } + if (latestDraftTag.sha !== config.revision) { + throw new Error( + `Version mismatch - latest draft tag was for revision ${latestDraftTag.sha}, current revision is ${config.revision}` + ); + } -async function publishArtifactsToBarque( - barque: Barque, - project: string, - releaseVersion: string, - mostRecentDraftTag: string, - packageInformation: PackageInformationProvider, - isDryRun: boolean, - getEvergreenArtifactUrl: typeof getArtifactUrlFn -): Promise { - const publishedPackages: string[] = []; - for await (const variant of ALL_PACKAGE_VARIANTS) { - const variantPackageInfo = packageInformation(variant); - const packageFile = getPackageFile(variant, () => ({ - ...variantPackageInfo, - metadata: { - ...variantPackageInfo.metadata, - version: releaseVersion, - }, - })); - const packageUrl = getEvergreenArtifactUrl( - project, - mostRecentDraftTag, - packageFile.path - ); console.info( - `mongosh: Considering publishing ${variant} artifact to barque ${packageUrl}` + 'mongosh: Re-using artifacts from most recent draft tag', + latestDraftTag.name ); - const packageUrls = await barque.releaseToBarque( - variant, - packageUrl, - isDryRun + + this.packageBumper.bumpAuxiliaryPackages(); + await this.packageBumper.bumpMongoshReleasePackages(releaseVersion); + this.packageBumper.commitBumpedPackages(); + + this.packagePublisher.pushTags(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await this.publishArtifactsToBarque(releaseVersion, latestDraftTag.name); + + await this.createAndPublishDownloadCenterConfig( + config.outputDir, + config.packageInformation as PackageInformationProvider, + config.downloadCenterAwsKey || '', + config.downloadCenterAwsSecret || '', + config.injectedJsonFeedFile || '', + !!config.isDryRun, + config.ctaConfig ); - for (const url of packageUrls) { - console.info(` -> ${url}`); - } - publishedPackages.push(...packageUrls); - } - if (isDryRun) { - console.warn('Not waiting for package availability in dry run...'); - } else { - await barque.waitUntilPackagesAreAvailable(publishedPackages, 300); + await mongoshGithubRepo.promoteRelease(config); + + // ensures the segment api key to be present in the published packages + await this.writeBuildInfo(config, 'packaged'); + + this.packagePublisher.publishToNpm(); + + await this.homebrewPublisher.publish(); + + console.info('mongosh: finished release process.'); } - console.info('mongosh: Submitting to barque complete'); + async publishArtifactsToBarque( + releaseVersion: string, + mostRecentDraftTag: string + ): Promise { + const { barque } = this; + const { project, isDryRun, packageInformation } = this.config; + + if (!project) { + throw new Error('project not specified'); + } + if (!packageInformation) { + throw new Error('packageInformation not specified'); + } + + const publishedPackages: string[] = []; + for await (const variant of ALL_PACKAGE_VARIANTS) { + const variantPackageInfo = packageInformation(variant); + const packageFile = getPackageFile(variant, () => ({ + ...variantPackageInfo, + metadata: { + ...variantPackageInfo.metadata, + version: releaseVersion, + }, + })); + const packageUrl = this.getEvergreenArtifactUrl( + project, + mostRecentDraftTag, + packageFile.path + ); + console.info( + `mongosh: Considering publishing ${variant} artifact to barque ${packageUrl}` + ); + const packageUrls = await barque.releaseToBarque( + variant, + packageUrl, + isDryRun || false + ); + for (const url of packageUrls) { + console.info(` -> ${url}`); + } + publishedPackages.push(...packageUrls); + } + + if (isDryRun) { + console.warn('Not waiting for package availability in dry run...'); + } else { + await barque.waitUntilPackagesAreAvailable(publishedPackages, 300); + } + + console.info('mongosh: Submitting to barque complete'); + } } diff --git a/packages/build/src/release.ts b/packages/build/src/release.ts index 22817df415..dddea420f6 100644 --- a/packages/build/src/release.ts +++ b/packages/build/src/release.ts @@ -1,20 +1,12 @@ import { Octokit } from '@octokit/rest'; -import { writeBuildInfo } from './build-info'; -import { Barque } from './barque'; import { runCompile } from './compile'; import type { Config } from './config'; import { getReleaseVersionFromTag, redactConfig } from './config'; -import { - createAndPublishDownloadCenterConfig, - uploadArtifactToDownloadCenter, -} from './download-center'; +import { uploadArtifactToDownloadCenter } from './download-center'; import { downloadArtifactFromEvergreen, uploadArtifactToEvergreen, } from './evergreen'; -import { GithubRepo } from '@mongodb-js/devtools-github-repo'; -import { publishToHomebrew } from './homebrew'; -import { bumpAuxiliaryPackages, publishToNpm, pushTags } from './npm-packages'; import { runPackage } from './packaging'; import { runDraft } from './run-draft'; import { publishMongosh } from './publish-mongosh'; @@ -22,8 +14,9 @@ import { runUpload } from './run-upload'; import { runSign } from './packaging/run-sign'; import { runDownloadAndListArtifacts } from './run-download-and-list-artifacts'; import { runDownloadCryptLibrary } from './packaging/run-download-crypt-library'; -import { bumpMongoshReleasePackages } from './npm-packages/bump'; +import { PackageBumper } from './npm-packages/bump'; import { publishAuxiliaryPackages } from './publish-auxiliary'; +import { GithubRepo } from '@mongodb-js/devtools-github-repo'; export type ReleaseCommand = | 'bump' @@ -57,9 +50,10 @@ export async function release( ); if (command === 'bump') { - bumpAuxiliaryPackages(); + const packageBumper = new PackageBumper(); + packageBumper.bumpAuxiliaryPackages(); if (!config.useAuxiliaryPackagesOnly) { - await bumpMongoshReleasePackages(config.version); + await packageBumper.bumpMongoshReleasePackages(config.version); } return; } @@ -83,16 +77,7 @@ export async function release( }; }); } - const githubRepo = new GithubRepo(config.repo, octokit); - const homebrewCoreRepo = new GithubRepo( - { owner: 'Homebrew', repo: 'homebrew-core' }, - octokit - ); - const mongoHomebrewForkRepo = new GithubRepo( - { owner: 'mongodb-js', repo: 'homebrew-core' }, - octokit - ); if (command === 'compile') { await runCompile(config); @@ -108,6 +93,7 @@ export async function release( await runDraft( config, githubRepo, + new PackageBumper(), uploadArtifactToDownloadCenter, downloadArtifactFromEvergreen ); @@ -117,18 +103,7 @@ export async function release( if (config.useAuxiliaryPackagesOnly) { publishAuxiliaryPackages(config); } else { - await publishMongosh( - config, - githubRepo, - mongoHomebrewForkRepo, - homebrewCoreRepo, - new Barque(config), - createAndPublishDownloadCenterConfig, - publishToNpm, - pushTags, - writeBuildInfo, - publishToHomebrew - ); + await publishMongosh(config, octokit); } } else { throw new Error(`Unknown command: ${command}`); diff --git a/packages/build/src/run-draft.spec.ts b/packages/build/src/run-draft.spec.ts index 2f290b63dc..593fde76ca 100644 --- a/packages/build/src/run-draft.spec.ts +++ b/packages/build/src/run-draft.spec.ts @@ -11,6 +11,7 @@ import { runDraft, } from './run-draft'; import { dummyConfig } from '../test/helpers'; +import { PackageBumper } from './npm-packages'; chai.use(require('sinon-chai')); @@ -37,8 +38,7 @@ describe('draft', function () { }); describe('runDraft', function () { - const bumpMongoshReleasePackages = sinon.spy(); - const bumpMongoshAuxiliaryPackages = sinon.spy(); + let packageBumper: PackageBumper; let ensureGithubReleaseExistsAndUpdateChangelog: typeof ensureGithubReleaseExistsAndUpdateChangelogFn; beforeEach(function () { @@ -49,6 +49,12 @@ describe('draft', function () { let uploadReleaseAsset: sinon.SinonStub; beforeEach(async function () { + packageBumper = new PackageBumper({ + spawnSync: sinon.stub().resolves(), + }); + sinon.stub(packageBumper, 'bumpAuxiliaryPackages').resolves(); + sinon.stub(packageBumper, 'bumpMongoshReleasePackages').resolves(); + uploadReleaseAsset = sinon.stub(); githubRepo = createStubRepo({ uploadReleaseAsset, @@ -58,11 +64,10 @@ describe('draft', function () { await runDraft( config, githubRepo, + packageBumper, uploadArtifactToDownloadCenter, downloadArtifactFromEvergreen, - ensureGithubReleaseExistsAndUpdateChangelog, - bumpMongoshReleasePackages, - bumpMongoshAuxiliaryPackages + ensureGithubReleaseExistsAndUpdateChangelog ); }); @@ -104,11 +109,10 @@ describe('draft', function () { await runDraft( config, githubRepo, + packageBumper, uploadArtifactToDownloadCenter, downloadArtifactFromEvergreen, - ensureGithubReleaseExistsAndUpdateChangelog, - bumpMongoshReleasePackages, - bumpMongoshAuxiliaryPackages + ensureGithubReleaseExistsAndUpdateChangelog ); expect(ensureGithubReleaseExistsAndUpdateChangelog).to.not.have.been .called; @@ -129,11 +133,10 @@ describe('draft', function () { await runDraft( config, githubRepo, + packageBumper, uploadArtifactToDownloadCenter, downloadArtifactFromEvergreen, - ensureGithubReleaseExistsAndUpdateChangelog, - bumpMongoshReleasePackages, - bumpMongoshAuxiliaryPackages + ensureGithubReleaseExistsAndUpdateChangelog ); } catch (e: any) { expect(e.message).to.contain('Missing package information from config'); diff --git a/packages/build/src/run-draft.ts b/packages/build/src/run-draft.ts index dbf65dfd86..ac4ea7fcd6 100644 --- a/packages/build/src/run-draft.ts +++ b/packages/build/src/run-draft.ts @@ -5,21 +5,17 @@ import { ALL_PACKAGE_VARIANTS, getReleaseVersionFromTag } from './config'; import { uploadArtifactToDownloadCenter as uploadArtifactToDownloadCenterFn } from './download-center'; import { downloadArtifactFromEvergreen as downloadArtifactFromEvergreenFn } from './evergreen'; import { generateChangelog as generateChangelogFn } from './git'; -import type { GithubRepo } from '@mongodb-js/devtools-github-repo'; import { getPackageFile } from './packaging'; -import { - bumpAuxiliaryPackages as bumpAuxiliaryPackagesFn, - bumpMongoshReleasePackages as bumpMongoshReleasePackagesFn, -} from './npm-packages/bump'; +import type { PackageBumper } from './npm-packages'; +import type { GithubRepo } from '@mongodb-js/devtools-github-repo'; export async function runDraft( config: Config, githubRepo: GithubRepo, + packageBumper: PackageBumper, uploadToDownloadCenter: typeof uploadArtifactToDownloadCenterFn = uploadArtifactToDownloadCenterFn, downloadArtifactFromEvergreen: typeof downloadArtifactFromEvergreenFn = downloadArtifactFromEvergreenFn, - ensureGithubReleaseExistsAndUpdateChangelog: typeof ensureGithubReleaseExistsAndUpdateChangelogFn = ensureGithubReleaseExistsAndUpdateChangelogFn, - bumpMongoshReleasePackages: typeof bumpMongoshReleasePackagesFn = bumpMongoshReleasePackagesFn, - bumpAuxiliaryPackages: typeof bumpAuxiliaryPackagesFn = bumpAuxiliaryPackagesFn + ensureGithubReleaseExistsAndUpdateChangelog: typeof ensureGithubReleaseExistsAndUpdateChangelogFn = ensureGithubReleaseExistsAndUpdateChangelogFn ): Promise { const { triggeringGitTag } = config; const draftReleaseVersion = getReleaseVersionFromTag(triggeringGitTag); @@ -51,8 +47,8 @@ export async function runDraft( ); await fs.mkdir(tmpDir, { recursive: true }); - bumpAuxiliaryPackages(); - await bumpMongoshReleasePackages(draftReleaseVersion); + packageBumper.bumpAuxiliaryPackages(); + await packageBumper.bumpMongoshReleasePackages(draftReleaseVersion); for await (const variant of ALL_PACKAGE_VARIANTS) { const tarballFile = getPackageFile(variant, config.packageInformation);