diff --git a/.yarn/versions/e6b25f51.yml b/.yarn/versions/e6b25f51.yml new file mode 100644 index 000000000000..54b48fcd17c5 --- /dev/null +++ b/.yarn/versions/e6b25f51.yml @@ -0,0 +1,34 @@ +releases: + "@yarnpkg/builder": patch + "@yarnpkg/cli": minor + "@yarnpkg/core": minor + "@yarnpkg/doctor": patch + "@yarnpkg/extensions": patch + "@yarnpkg/nm": patch + "@yarnpkg/plugin-catalog": patch + "@yarnpkg/plugin-compat": patch + "@yarnpkg/plugin-constraints": patch + "@yarnpkg/plugin-dlx": patch + "@yarnpkg/plugin-essentials": minor + "@yarnpkg/plugin-exec": patch + "@yarnpkg/plugin-file": patch + "@yarnpkg/plugin-git": patch + "@yarnpkg/plugin-github": patch + "@yarnpkg/plugin-http": patch + "@yarnpkg/plugin-init": patch + "@yarnpkg/plugin-interactive-tools": patch + "@yarnpkg/plugin-jsr": patch + "@yarnpkg/plugin-link": patch + "@yarnpkg/plugin-nm": patch + "@yarnpkg/plugin-npm": patch + "@yarnpkg/plugin-npm-cli": patch + "@yarnpkg/plugin-pack": patch + "@yarnpkg/plugin-patch": patch + "@yarnpkg/plugin-pnp": patch + "@yarnpkg/plugin-pnpm": patch + "@yarnpkg/plugin-stage": patch + "@yarnpkg/plugin-typescript": patch + "@yarnpkg/plugin-version": patch + "@yarnpkg/plugin-workspace-tools": patch + "@yarnpkg/pnpify": patch + "@yarnpkg/sdks": patch diff --git a/packages/plugin-essentials/sources/commands/install.ts b/packages/plugin-essentials/sources/commands/install.ts index 0a0bca3ac818..effd5f1e0d90 100644 --- a/packages/plugin-essentials/sources/commands/install.ts +++ b/packages/plugin-essentials/sources/commands/install.ts @@ -367,6 +367,7 @@ export default class YarnCommand extends BaseCommand { const report = await StreamReport.start({ configuration, + failOnWarnings: this.context.failOnWarnings, json: this.json, stdout: this.context.stdout, forceSectionAlignment: true, diff --git a/packages/yarnpkg-cli/sources/lib.ts b/packages/yarnpkg-cli/sources/lib.ts index 2ff6c8148025..a67c8dabe16e 100644 --- a/packages/yarnpkg-cli/sources/lib.ts +++ b/packages/yarnpkg-cli/sources/lib.ts @@ -21,6 +21,7 @@ function getBaseCli({cwd, pluginConfiguration}: {cwd: PortablePath, pluginConfig ...Cli.defaultContext, cwd, plugins: pluginConfiguration, + failOnWarnings: false, quiet: false, stdin: process.stdin, stdout: process.stdout, diff --git a/packages/yarnpkg-cli/sources/tools/BaseCommand.ts b/packages/yarnpkg-cli/sources/tools/BaseCommand.ts index 81c4c4133478..ee42c1d082e7 100644 --- a/packages/yarnpkg-cli/sources/tools/BaseCommand.ts +++ b/packages/yarnpkg-cli/sources/tools/BaseCommand.ts @@ -4,12 +4,20 @@ import {Command, Option, UsageError} from 'clipanion'; export abstract class BaseCommand extends Command { cwd = Option.String(`--cwd`, {hidden: true}); + failOnWarnings = Option.Boolean(`--fail-on-warnings`, false, { + description: `Exit with a non-zero status code when warnings are reported`, + hidden: true, + }); + abstract execute(): Promise; validateAndExecute(): Promise { if (typeof this.cwd !== `undefined`) throw new UsageError(`The --cwd option is ambiguous when used anywhere else than the very first parameter provided in the command line, before even the command path`); + if (this.failOnWarnings) + this.context.failOnWarnings = true; + return super.validateAndExecute(); } } diff --git a/packages/yarnpkg-core/sources/Plugin.ts b/packages/yarnpkg-core/sources/Plugin.ts index 1dc93934d303..b425d1945557 100644 --- a/packages/yarnpkg-core/sources/Plugin.ts +++ b/packages/yarnpkg-core/sources/Plugin.ts @@ -15,6 +15,7 @@ import {Locator, Descriptor} export type CommandContext = { cwd: PortablePath; env: Record; + failOnWarnings: boolean; plugins: PluginConfiguration; quiet: boolean; stdin: Readable; diff --git a/packages/yarnpkg-core/sources/StreamReport.ts b/packages/yarnpkg-core/sources/StreamReport.ts index 7a6bcdd0fd60..6bef4d1266c1 100644 --- a/packages/yarnpkg-core/sources/StreamReport.ts +++ b/packages/yarnpkg-core/sources/StreamReport.ts @@ -11,6 +11,7 @@ import * as formatUtils export type StreamReportOptions = { configuration: Configuration; + failOnWarnings?: boolean; forceSectionAlignment?: boolean; includeFooter?: boolean; includeInfos?: boolean; @@ -185,6 +186,7 @@ export class StreamReport extends Report { } private configuration: Configuration; + private failOnWarnings: boolean; private forceSectionAlignment: boolean; private includeNames: boolean; private includePrefix: boolean; @@ -225,6 +227,7 @@ export class StreamReport extends Report { configuration, stdout, json = false, + failOnWarnings = false, forceSectionAlignment = false, includeNames = true, includePrefix = true, @@ -238,6 +241,7 @@ export class StreamReport extends Report { formatUtils.addLogFilterSupport(this, {configuration}); this.configuration = configuration; + this.failOnWarnings = failOnWarnings; this.forceSectionAlignment = forceSectionAlignment; this.includeNames = includeNames; this.includePrefix = includePrefix; @@ -264,8 +268,18 @@ export class StreamReport extends Report { return this.errorCount > 0; } + hasWarnings() { + return this.warningCount > 0; + } + exitCode() { - return this.hasErrors() ? 1 : 0; + if (this.hasErrors()) + return 1; + + if (this.failOnWarnings && this.hasWarnings()) + return 1; + + return 0; } getRecommendedLength() { diff --git a/packages/yarnpkg-core/tests/StreamReport.test.ts b/packages/yarnpkg-core/tests/StreamReport.test.ts new file mode 100644 index 000000000000..c94a81062857 --- /dev/null +++ b/packages/yarnpkg-core/tests/StreamReport.test.ts @@ -0,0 +1,34 @@ +import {PortablePath} from '@yarnpkg/fslib'; +import {PassThrough} from 'stream'; + +import {Configuration} from '../sources/Configuration'; +import {MessageName} from '../sources/MessageName'; +import {StreamReport} from '../sources/StreamReport'; + +const configuration = Configuration.create(PortablePath.root); + +describe(`StreamReport`, () => { + it.each<[string, {failOnWarnings?: boolean, reports: Array<`error` | `warning`>}, {exitCode: number, hasWarnings: boolean}]>([ + [`no errors or warnings`, {reports: []}, {exitCode: 0, hasWarnings: false}], + [`errors only`, {reports: [`error`]}, {exitCode: 1, hasWarnings: false}], + [`warnings, failOnWarnings=false`, {failOnWarnings: false, reports: [`warning`]}, {exitCode: 0, hasWarnings: true}], + [`warnings, failOnWarnings=true`, {failOnWarnings: true, reports: [`warning`]}, {exitCode: 1, hasWarnings: true}], + [`errors, failOnWarnings=false`, {failOnWarnings: false, reports: [`error`]}, {exitCode: 1, hasWarnings: false}], + [`errors and warnings`, {reports: [`error`, `warning`]}, {exitCode: 1, hasWarnings: true}], + ])(`%s`, async (_label, {failOnWarnings, reports}, expected) => { + const stdout = new PassThrough(); + + const report = await StreamReport.start({configuration, stdout, failOnWarnings}, async report => { + for (const type of reports) { + if (type === `error`) { + report.reportError(MessageName.UNNAMED, `test error`); + } else { + report.reportWarning(MessageName.UNNAMED, `test warning`); + } + } + }); + + expect(report.exitCode()).toEqual(expected.exitCode); + expect(report.hasWarnings()).toEqual(expected.hasWarnings); + }); +});