From 1f2351e3a8ce016f8ff57674709e559963e82a65 Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 15 Jul 2024 12:25:03 -0400 Subject: [PATCH] improvement(build), stop the build as soon as a tag-blocker component-issue is found (#9031) Allow to ignore the issue by `--ignore-issues` flag. (similar to `bit tag` and `bit snap`). --- .../peer-dependencies.e2e.3.ts | 2 +- e2e/harmony/preview.e2e.ts | 2 +- .../snapping/snapping.main.runtime.ts | 26 +------- scopes/component/snapping/snapping.spec.ts | 3 +- scopes/pipelines/builder/build.cmd.ts | 11 ++++ .../pipelines/builder/builder.main.runtime.ts | 64 +++++++++++++++++-- .../exceptions}/components-have-issues.ts | 2 +- 7 files changed, 74 insertions(+), 36 deletions(-) rename scopes/{component/snapping => pipelines/builder/exceptions}/components-have-issues.ts (91%) diff --git a/e2e/functionalities/peer-dependencies.e2e.3.ts b/e2e/functionalities/peer-dependencies.e2e.3.ts index a81c25ceed6..50817b1c9f7 100644 --- a/e2e/functionalities/peer-dependencies.e2e.3.ts +++ b/e2e/functionalities/peer-dependencies.e2e.3.ts @@ -91,7 +91,7 @@ describe('peer-dependencies functionality', function () { helper.workspaceJsonc.addPolicyToDependencyResolver({ peerDependencies: { [`@${helper.scopes.remote}/comp2`]: '*' }, }); - helper.command.build(); + helper.command.build(undefined, '--ignore-issues="DuplicateComponentAndPackage"'); workspaceCapsulesRootDir = helper.command.capsuleListParsed().workspaceCapsulesRootDir; }); it('should save the peer dependency in the model', () => { diff --git a/e2e/harmony/preview.e2e.ts b/e2e/harmony/preview.e2e.ts index ffdc2a04d6d..508ac804f1f 100644 --- a/e2e/harmony/preview.e2e.ts +++ b/e2e/harmony/preview.e2e.ts @@ -38,7 +38,7 @@ describe('preview feature (during build)', function () { ); helper.fs.outputFile('index.js', `export { Button } from './button';`); helper.command.addComponent('button'); - helper.command.install(); + helper.command.install('--add-missing-deps'); helper.command.compile(); }); it('bit build should run successfully without preview errors', () => { diff --git a/scopes/component/snapping/snapping.main.runtime.ts b/scopes/component/snapping/snapping.main.runtime.ts index 83507b9e097..8a62c72da3f 100644 --- a/scopes/component/snapping/snapping.main.runtime.ts +++ b/scopes/component/snapping/snapping.main.runtime.ts @@ -46,7 +46,6 @@ import Version, { DepEdge, DepEdgeType, Log } from '@teambit/legacy/dist/scope/m import { SnapCmd } from './snap-cmd'; import { SnappingAspect } from './snapping.aspect'; import { TagCmd } from './tag-cmd'; -import { ComponentsHaveIssues } from './components-have-issues'; import ResetCmd from './reset-cmd'; import { tagModelComponent, updateComponentsVersions, BasicTagParams, BasicTagSnapParams } from './tag-model-component'; import { TagDataPerCompRaw, TagFromScopeCmd } from './tag-from-scope.cmd'; @@ -767,7 +766,7 @@ in case you're unsure about the pattern syntax, use "bit pattern [--help]"`); const componentsToCheck = components.filter((c) => !c.isDeleted()); const consumerComponents = componentsToCheck.map((c) => c.state._consumer) as ConsumerComponent[]; await this.throwForLegacyDependenciesInsideHarmony(consumerComponents); - await this.throwForComponentIssues(componentsToCheck, ignoreIssues); + await this.builder.throwForComponentIssues(componentsToCheck, ignoreIssues); this.throwForPendingImport(consumerComponents); } @@ -1028,29 +1027,6 @@ another option, in case this dependency is not in main yet is to remove all refe return this.workspace.getMany(ids.map((id) => id.changeVersion(undefined))); } - private async throwForComponentIssues(components: Component[], ignoreIssues?: string) { - if (ignoreIssues === '*') { - // ignore all issues - return; - } - const issuesToIgnoreFromFlag = ignoreIssues?.split(',').map((issue) => issue.trim()) || []; - const issuesToIgnoreFromConfig = this.issues.getIssuesToIgnoreGlobally(); - const issuesToIgnore = [...issuesToIgnoreFromFlag, ...issuesToIgnoreFromConfig]; - await this.issues.triggerAddComponentIssues(components, issuesToIgnore); - this.issues.removeIgnoredIssuesFromComponents(components, issuesToIgnore); - const legacyComponents = components.map((c) => c.state._consumer) as ConsumerComponent[]; - const componentsWithBlockingIssues = legacyComponents.filter((component) => component.issues?.shouldBlockTagging()); - if (componentsWithBlockingIssues.length) { - throw new ComponentsHaveIssues(componentsWithBlockingIssues); - } - - const workspaceIssues = this.workspace.getWorkspaceIssues(); - if (workspaceIssues.length) { - const issuesStr = workspaceIssues.map((issueErr) => issueErr.message).join('\n'); - throw new BitError(`the workspace has the following issues:\n${issuesStr}`); - } - } - private throwForPendingImport(components: ConsumerComponent[]) { const componentsMissingFromScope = components .filter((c) => !c.componentFromModel && this.scope.isExported(c.id)) diff --git a/scopes/component/snapping/snapping.spec.ts b/scopes/component/snapping/snapping.spec.ts index d1d635e47e5..714f9186a60 100644 --- a/scopes/component/snapping/snapping.spec.ts +++ b/scopes/component/snapping/snapping.spec.ts @@ -20,7 +20,6 @@ import { Ref } from '@teambit/legacy/dist/scope/objects'; import { mockComponents } from '@teambit/component.testing.mock-components'; import { SnappingMain } from './snapping.main.runtime'; import { SnappingAspect } from './snapping.aspect'; -import { ComponentsHaveIssues } from './components-have-issues'; import { SnapDataPerCompRaw } from './snap-from-scope.cmd'; describe('Snapping aspect', function () { @@ -44,7 +43,7 @@ describe('Snapping aspect', function () { try { await snapping.tag({ ids: ['comp1'] }); } catch (err: any) { - expect(err.constructor.name).to.equal(ComponentsHaveIssues.name); + expect(err.constructor.name).to.equal('ComponentsHaveIssues'); } }); // @todo: this test fails during "bit build" for some reason. It passes on "bit test"; diff --git a/scopes/pipelines/builder/build.cmd.ts b/scopes/pipelines/builder/build.cmd.ts index 095ee0f9147..f8a2ac0e8e8 100644 --- a/scopes/pipelines/builder/build.cmd.ts +++ b/scopes/pipelines/builder/build.cmd.ts @@ -5,6 +5,7 @@ import { OutsideWorkspaceError, Workspace } from '@teambit/workspace'; import { COMPONENT_PATTERN_HELP } from '@teambit/legacy/dist/constants'; import chalk from 'chalk'; import { BuilderMain } from './builder.main.runtime'; +import { IssuesClasses } from '@teambit/component-issues'; type BuildOpts = { unmodified?: boolean; @@ -21,6 +22,7 @@ type BuildOpts = { failFast?: boolean; includeSnap?: boolean; includeTag?: boolean; + ignoreIssues?: string; }; export class BuilderCmd implements Command { @@ -81,6 +83,13 @@ specify the task-name (e.g. "TypescriptCompiler") or the task-aspect-id (e.g. te 'include-tag', 'EXPERIMENTAL. include tag pipeline tasks. Warning: this may deploy/publish if you have such tasks', ], + [ + 'i', + 'ignore-issues ', + `ignore component issues (shown in "bit status" as "issues found"), issues to ignore: +[${Object.keys(IssuesClasses).join(', ')}] +to ignore multiple issues, separate them by a comma and wrap with quotes. to ignore all issues, specify "*".`, + ], ] as CommandOptions; constructor(private builder: BuilderMain, private workspace: Workspace, private logger: Logger) {} @@ -101,6 +110,7 @@ specify the task-name (e.g. "TypescriptCompiler") or the task-aspect-id (e.g. te failFast, includeSnap, includeTag, + ignoreIssues, }: BuildOpts ): Promise { if (rewrite && !reuseCapsules) throw new Error('cannot use --rewrite without --reuse-capsules'); @@ -141,6 +151,7 @@ specify the task-name (e.g. "TypescriptCompiler") or the task-aspect-id (e.g. te { includeSnap, includeTag, + ignoreIssues, } ); this.logger.console(`build output can be found in path: ${envsExecutionResults.capsuleRootDir}`); diff --git a/scopes/pipelines/builder/builder.main.runtime.ts b/scopes/pipelines/builder/builder.main.runtime.ts index 24bd618c868..aec7593a8c8 100644 --- a/scopes/pipelines/builder/builder.main.runtime.ts +++ b/scopes/pipelines/builder/builder.main.runtime.ts @@ -1,4 +1,5 @@ import { cloneDeep } from 'lodash'; +import ConsumerComponent from '@teambit/legacy/dist/consumer/component/consumer-component'; import { ArtifactVinyl, ArtifactFiles, ArtifactObject } from '@teambit/component.sources'; import { AspectLoaderAspect, AspectLoaderMain } from '@teambit/aspect-loader'; import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli'; @@ -16,6 +17,8 @@ import { getBitVersion } from '@teambit/bit.get-bit-version'; import { findDuplications } from '@teambit/toolbox.array.duplications-finder'; import { GeneratorAspect, GeneratorMain } from '@teambit/generator'; import { UIAspect, UiMain, BundleUiTask } from '@teambit/ui'; +import { IssuesAspect, IssuesMain } from '@teambit/issues'; +import { BitError } from '@teambit/bit-error'; import { Artifact, ArtifactList, FsArtifact } from './artifact'; import { ArtifactFactory } from './artifact/artifact-factory'; // it gets undefined when importing it from './artifact' import { BuilderAspect } from './builder.aspect'; @@ -31,6 +34,7 @@ import { TaskMetadata } from './types'; import { ArtifactsCmd } from './artifact/artifacts.cmd'; import { buildTaskTemplate } from './templates/build-task'; import { BuilderRoute } from './builder.route'; +import { ComponentsHaveIssues } from './exceptions/components-have-issues'; export type TaskSlot = SlotRegistry; export type OnTagResults = { builderDataMap: ComponentMap; pipeResults: TaskResultsList[] }; @@ -74,7 +78,8 @@ export class BuilderMain { private buildTaskSlot: TaskSlot, private tagTaskSlot: TaskSlot, private snapTaskSlot: TaskSlot, - private logger: Logger + private logger: Logger, + private issues: IssuesMain ) {} private async storeArtifacts(tasksResults: TaskResults[]) { @@ -124,7 +129,8 @@ export class BuilderMain { ...builderOptions, // even when build is skipped (in case of tag-from-scope), the pre-build/post-build and teambit.harmony/aspect tasks are needed tasks: populateArtifactsFrom ? [AspectAspect.id] : undefined, - } + }, + { ignoreIssues: '*' } ); if (throwOnError && !forceDeploy) buildEnvsExecutionResults.throwErrorsIfExist(); allTasksResults.push(...buildEnvsExecutionResults.tasksResults); @@ -366,8 +372,9 @@ export class BuilderMain { components: Component[], isolateOptions?: IsolateComponentsOptions, builderOptions?: BuilderServiceOptions, - extraOptions?: { includeTag?: boolean; includeSnap?: boolean } + extraOptions?: { includeTag?: boolean; includeSnap?: boolean; ignoreIssues?: string } ): Promise { + await this.throwForVariousIssues(components, extraOptions?.ignoreIssues); const ids = components.map((c) => c.id); const capsulesBaseDir = this.buildService.getComponentsCapsulesBaseDir(); const baseIsolateOpts = { @@ -465,6 +472,34 @@ export class BuilderMain { return `/api/${componentId}/~aspect/builder/${taskId}/${path ? `${FILE_PATH_PARAM_DELIM}${path}` : ''}`; } + private async throwForVariousIssues(components: Component[], ignoreIssues?: string) { + const componentsToCheck = components.filter((c) => !c.isDeleted()); + await this.throwForComponentIssues(componentsToCheck, ignoreIssues); + } + + async throwForComponentIssues(components: Component[], ignoreIssues?: string) { + if (ignoreIssues === '*') { + // ignore all issues + return; + } + const issuesToIgnoreFromFlag = ignoreIssues?.split(',').map((issue) => issue.trim()) || []; + const issuesToIgnoreFromConfig = this.issues.getIssuesToIgnoreGlobally(); + const issuesToIgnore = [...issuesToIgnoreFromFlag, ...issuesToIgnoreFromConfig]; + await this.issues.triggerAddComponentIssues(components, issuesToIgnore); + this.issues.removeIgnoredIssuesFromComponents(components, issuesToIgnore); + const legacyComponents = components.map((c) => c.state._consumer) as ConsumerComponent[]; + const componentsWithBlockingIssues = legacyComponents.filter((component) => component.issues?.shouldBlockTagging()); + if (componentsWithBlockingIssues.length) { + throw new ComponentsHaveIssues(componentsWithBlockingIssues); + } + + const workspaceIssues = this.workspace.getWorkspaceIssues(); + if (workspaceIssues.length) { + const issuesStr = workspaceIssues.map((issueErr) => issueErr.message).join('\n'); + throw new BitError(`the workspace has the following issues:\n${issuesStr}`); + } + } + static slots = [Slot.withType(), Slot.withType(), Slot.withType()]; static runtime = MainRuntime; @@ -481,10 +516,25 @@ export class BuilderMain { ComponentAspect, UIAspect, GlobalConfigAspect, + IssuesAspect, ]; static async provider( - [cli, envs, workspace, scope, isolator, loggerExt, aspectLoader, graphql, generator, component, ui, globalConfig]: [ + [ + cli, + envs, + workspace, + scope, + isolator, + loggerExt, + aspectLoader, + graphql, + generator, + component, + ui, + globalConfig, + issues, + ]: [ CLIMain, EnvsMain, Workspace, @@ -496,7 +546,8 @@ export class BuilderMain { GeneratorMain, ComponentMain, UiMain, - GlobalConfigMain + GlobalConfigMain, + IssuesMain ], config, [buildTaskSlot, tagTaskSlot, snapTaskSlot]: [TaskSlot, TaskSlot, TaskSlot] @@ -548,7 +599,8 @@ export class BuilderMain { buildTaskSlot, tagTaskSlot, snapTaskSlot, - logger + logger, + issues ); builder.registerBuildTasks([new BundleUiTask(ui, logger)]); component.registerRoute([new BuilderRoute(builder, scope, logger)]); diff --git a/scopes/component/snapping/components-have-issues.ts b/scopes/pipelines/builder/exceptions/components-have-issues.ts similarity index 91% rename from scopes/component/snapping/components-have-issues.ts rename to scopes/pipelines/builder/exceptions/components-have-issues.ts index cc4a9197d7f..10d5842461f 100644 --- a/scopes/component/snapping/components-have-issues.ts +++ b/scopes/pipelines/builder/exceptions/components-have-issues.ts @@ -17,7 +17,7 @@ ${issuesColored} to get the list of component-issues names and suggestions how to resolve them, run "bit component-issues". while highly not recommended, it's possible to ignore issues in two ways: -1) temporarily ignore for this tag/snap command by entering "--ignore-issues" flag, e.g. \`bit tag --ignore-issues "${allIssueNames.join( +1) temporarily ignore for this tag/snap/build command by entering "--ignore-issues" flag, e.g. \`bit tag --ignore-issues "${allIssueNames.join( ', ' )}" \` 2) ignore the issue completely by configuring it in the workspace.jsonc file. e.g: