diff --git a/packages/cli/schemas/lerna-schema.json b/packages/cli/schemas/lerna-schema.json index a0d72504..e47e56fc 100644 --- a/packages/cli/schemas/lerna-schema.json +++ b/packages/cli/schemas/lerna-schema.json @@ -443,6 +443,9 @@ }, "continueIfNoMatch": { "$ref": "#/$defs/filters/continueIfNoMatch" + }, + "summaryFile": { + "$ref": "#/$defs/commandOptions/publish/summaryFile" } } }, @@ -1281,6 +1284,10 @@ "type": "boolean", "description": "During `lerna publish`, when true, do not verify package read-write access for current npm user." }, + "summaryFile": { + "anyOf": [{ "type": "string" }, { "type": "boolean" }], + "description": "Generate a json summary report after all packages have been successfully published, you can pass an optional path for where to save the file." + }, "verifyAccess": { "type": "boolean", "description": "During `lerna publish`, when true, verify package read-write access for current npm user." diff --git a/packages/cli/src/cli-commands/cli-publish-commands.ts b/packages/cli/src/cli-commands/cli-publish-commands.ts index 97558584..0915702d 100644 --- a/packages/cli/src/cli-commands/cli-publish-commands.ts +++ b/packages/cli/src/cli-commands/cli-publish-commands.ts @@ -127,6 +127,12 @@ export default { describe: 'Do not verify package read-write access for current npm user.', type: 'boolean', }, + 'summary-file': { + // generate lerna publish json output. + describe: + 'Generate a json summary report after all packages have been successfully published, you can pass an optional path for where to save the file.', + type: 'string', + }, 'verify-access': { describe: 'Verify package read-write access for current npm user.', type: 'boolean', diff --git a/packages/core/src/models/command-options.ts b/packages/core/src/models/command-options.ts index ba3dc7f5..9e0a7c74 100644 --- a/packages/core/src/models/command-options.ts +++ b/packages/core/src/models/command-options.ts @@ -168,6 +168,9 @@ export interface PublishCommandOption extends VersionCommandOption { /** Do not verify package read-write access for current npm user. */ noVerifyAccess?: boolean; + /** Generate a json summary report after all packages have been successfully published, you can pass an optional path for where to save the file. */ + summaryFile?: boolean | string; + /** proxy for `--no-verify-access` */ verifyAccess?: boolean; diff --git a/packages/publish/README.md b/packages/publish/README.md index 9eb0c770..9f91ba28 100644 --- a/packages/publish/README.md +++ b/packages/publish/README.md @@ -89,6 +89,7 @@ This is useful when a previous `lerna publish` failed to publish all packages to - [`--registry `](#--registry-url) - [`--tag-version-prefix`](#--tag-version-prefix) - [`--temp-tag`](#--temp-tag) + - [`--summary-file `](#--summary-file) - [`--verify-access`](#--verify-access) - [`--yes`](#--yes) - [`publishConfig` Overrides](#publishconfig-overrides) @@ -349,6 +350,30 @@ new version(s) to the dist-tag configured by [`--dist-tag`](#--dist-tag-tag) (de This is not generally necessary, as lerna will publish packages in topological order (all dependencies before dependents) by default. +### `--summary-file` + +```sh +# Will create a summary file in the root directory, i.e. `./lerna-publish-summary.json` +lerna publish --canary --yes --summary-file +# Will create a summary file in the provided directory, i.e. `./some/other/dir/lerna-publish-summary.json` +lerna publish --canary --yes --summary-file ./some/other/dir +``` + +When run with this flag, a json summary report will be generated after all packages have been successfully published (see below for an example). + +```json +[ + { + "packageName": "package1", + "version": "v1.0.1-alpha" + }, + { + "packageName": "package2", + "version": "v2.0.1-alpha" + } +] +``` + ### `--verify-access` Historically, `lerna` attempted to fast-fail on authorization/authentication issues by performing some preemptive npm API requests using the given token. These days, however, there are multiple types of tokens that npm supports and they have varying levels of access rights, so there is no one-size fits all solution for this preemptive check and it is more appropriate to allow requests to npm to simply fail with appropriate errors for the given token. For this reason, the legacy `--verify-access` behavior is disabled by default and will likely be removed in a future major version. diff --git a/packages/publish/src/__tests__/publish-command.spec.ts b/packages/publish/src/__tests__/publish-command.spec.ts index f9d760b1..cfb2b2d1 100644 --- a/packages/publish/src/__tests__/publish-command.spec.ts +++ b/packages/publish/src/__tests__/publish-command.spec.ts @@ -49,6 +49,7 @@ jest.mock('../lib/pack-directory', () => jest.requireActual('../lib/__mocks__/pa jest.mock('../lib/git-checkout'); import fs from 'fs-extra'; +import fsmain from 'fs'; import path from 'path'; // helpers @@ -493,6 +494,46 @@ describe('PublishCommand', () => { }); }); + describe('--summary-file', () => { + it('skips creating the summary file', async () => { + const cwd = await initFixture('normal'); + const fsSpy = jest.spyOn(fs, 'writeFileSync'); + await lernaPublish(cwd); + + expect(fsSpy).not.toHaveBeenCalled(); + }); + + it('creates the summary file within the provided directory', async () => { + const cwd = await initFixture('normal'); + const fsSpy = jest.spyOn(fsmain, 'writeFileSync'); + await lernaPublish(cwd)('--summary-file', './outputs'); + + const expectedJsonResponse = [ + { packageName: 'package-1', version: '1.0.1' }, + { packageName: 'package-2', version: '1.0.1' }, + { packageName: 'package-3', version: '1.0.1' }, + { packageName: 'package-4', version: '1.0.1' }, + ]; + expect(fsSpy).toHaveBeenCalled(); + expect(fsSpy).toHaveBeenCalledWith('./outputs/lerna-publish-summary.json', JSON.stringify(expectedJsonResponse)); + }); + + it('creates the summary file at the root when no custom directory is provided', async () => { + const cwd = await initFixture('normal'); + const fsSpy = jest.spyOn(fsmain, 'writeFileSync'); + await lernaPublish(cwd)('--summary-file'); + + const expectedJsonResponse = [ + { packageName: 'package-1', version: '1.0.1' }, + { packageName: 'package-2', version: '1.0.1' }, + { packageName: 'package-3', version: '1.0.1' }, + { packageName: 'package-4', version: '1.0.1' }, + ]; + expect(fsSpy).toHaveBeenCalled(); + expect(fsSpy).toHaveBeenCalledWith('./lerna-publish-summary.json', JSON.stringify(expectedJsonResponse)); + }); + }); + describe('--verify-access', () => { it("publishes packages after verifying the user's access to each package", async () => { const testDir = await initFixture('normal'); diff --git a/packages/publish/src/publish-command.ts b/packages/publish/src/publish-command.ts index f2d4019c..6a6c3e82 100644 --- a/packages/publish/src/publish-command.ts +++ b/packages/publish/src/publish-command.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import fs from 'fs'; import os from 'os'; import path from 'path'; import crypto from 'crypto'; @@ -296,7 +297,29 @@ export class PublishCommand extends Command { const message: string[] = this.packagesToPublish?.map((pkg) => ` - ${pkg.name}@${pkg.version}`) ?? []; logOutput('Successfully published:'); - logOutput(message.join(os.EOL)); + + if (this.options.summaryFile !== undefined) { + // create a json object and output it to a file location. + const filePath = this.options.summaryFile + ? `${this.options.summaryFile}/lerna-publish-summary.json` + : './lerna-publish-summary.json'; + const jsonObject = this.packagesToPublish.map((pkg) => { + return { + packageName: pkg.name, + version: pkg.version, + }; + }); + logOutput(jsonObject); + try { + fs.writeFileSync(filePath, JSON.stringify(jsonObject)); + logOutput('Publish summary created: ', filePath); + } catch (error) { + logOutput('Failed to create the summary report', error); + } + } else { + const message = this.packagesToPublish.map((pkg) => ` - ${pkg.name}@${pkg.version}`); + logOutput(message.join(os.EOL)); + } this.logger.success('published', '%d %s', count, count === 1 ? 'package' : 'packages'); }