From 707bb8f167b50e3783aabd302ee1d94890159fda Mon Sep 17 00:00:00 2001 From: Christian Dupuis Date: Mon, 3 Sep 2018 00:45:45 +0200 Subject: [PATCH] Auto merge pull request #504 from atomist/sdm * Allow goals to take implementations, side effects and callbacks This requires more polish * Autofix: TypeScript header [atomist:generated] [atomist:autofix=TypeScript header] * Minor update * Autofix: TypeScript header [atomist:generated] [atomist:autofix=TypeScript header] * Fix registration lifecycle * Autofix: TypeScript header [atomist:generated] [atomist:autofix=TypeScript header] * Autofix: tslint [atomist:generated] [atomist:autofix=tslint] * Not quite yet * Autofix: TypeScript header [atomist:generated] [atomist:autofix=TypeScript header] * Autofix: tslint [atomist:generated] [atomist:autofix=tslint] * Clean out registrables that got registered * Allow to reset the registrable tracking for tests * Don't pass entire SDM; only the config * Add well known goals to not break backwards compatibility * Remove well known goals * Fix approvalRequired message not being used * Fix spelling error --- package-lock.json | 8 +- package.json | 2 +- ....ts => DefaultGoalImplementationMapper.ts} | 38 ++-- src/api-helper/goal/chooseAndSetGoals.ts | 23 +-- src/api-helper/goal/executeGoal.ts | 2 +- src/api-helper/goal/storeGoals.ts | 9 +- .../listener/executeAutoInspects.ts | 10 +- src/api-helper/listener/executeAutofixes.ts | 16 +- .../listener/executeFingerprinting.ts | 63 +++++++ .../listener/executePushReactions.ts | 7 +- .../AbstractSoftwareDeliveryMachine.ts | 15 +- src/api-helper/test/fakeGoalInvocation.ts | 8 +- .../{githubTeamVote.ts => githubTeamVoter.ts} | 2 +- src/api/goal/GoalInvocation.ts | 3 + src/api/goal/GoalWithFulfillment.ts | 165 ++++++++++++++++++ src/api/goal/Goals.ts | 2 +- src/api/goal/SdmGoalMessage.ts | 6 +- src/api/goal/common/Autofix.ts | 42 +++++ src/api/goal/common/CodeInspects.ts | 42 +++++ src/api/goal/common/Fingerprint.ts | 43 +++++ src/api/goal/common/PushImpact.ts | 40 +++++ ...nMapper.ts => GoalImplementationMapper.ts} | 9 +- src/api/machine/GoalDrivenMachine.ts | 8 +- src/api/machine/SoftwareDeliveryMachine.ts | 2 - .../machine/SoftwareDeliveryMachineOptions.ts | 6 + src/api/machine/registrable.ts | 57 ++++++ src/index.ts | 8 +- .../well-known-goals/addWellKnownGoals.ts | 63 +++++++ src/pack/well-known-goals/commonGoals.ts | 113 ++++++++++++ src/pack/well-known-goals/httpServiceGoals.ts | 79 +++++++++ src/pack/well-known-goals/libraryGoals.ts | 31 ++++ .../listener/executeAutoInspect.test.ts | 24 ++- .../listener/executeAutofixes.test.ts | 46 +++-- .../listener/executePushReactions.test.ts | 15 +- 34 files changed, 890 insertions(+), 117 deletions(-) rename src/api-helper/goal/{SdmGoalImplementationMapperImpl.ts => DefaultGoalImplementationMapper.ts} (75%) create mode 100644 src/api-helper/listener/executeFingerprinting.ts rename src/api-helper/voter/{githubTeamVote.ts => githubTeamVoter.ts} (95%) create mode 100644 src/api/goal/GoalWithFulfillment.ts create mode 100644 src/api/goal/common/Autofix.ts create mode 100644 src/api/goal/common/CodeInspects.ts create mode 100644 src/api/goal/common/Fingerprint.ts create mode 100644 src/api/goal/common/PushImpact.ts rename src/api/goal/support/{SdmGoalImplementationMapper.ts => GoalImplementationMapper.ts} (86%) create mode 100644 src/api/machine/registrable.ts create mode 100644 src/pack/well-known-goals/addWellKnownGoals.ts create mode 100644 src/pack/well-known-goals/commonGoals.ts create mode 100644 src/pack/well-known-goals/httpServiceGoals.ts create mode 100644 src/pack/well-known-goals/libraryGoals.ts diff --git a/package-lock.json b/package-lock.json index 3a351d5bc..8c8ccb0d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@atomist/automation-client": { - "version": "1.0.0-master.20180901185845", - "resolved": "https://registry.npmjs.org/@atomist/automation-client/-/automation-client-1.0.0-master.20180901185845.tgz", - "integrity": "sha512-dHo7sEQXBVDGdd5ZYJ6l79yTgqFNaN4KRLMR+AXscFkJMAPqavPgUl9FZu2Z0ruoyt8RKepYDcRK0/fAMOj+xg==", + "version": "1.0.0-master.20180902103822", + "resolved": "https://registry.npmjs.org/@atomist/automation-client/-/automation-client-1.0.0-master.20180902103822.tgz", + "integrity": "sha512-mAp+mprK3nX/wFiBqvA6y46RdSdKnOr3OvTMA/zCCYDfnUOlvh4L8XHlKMLJAfeqQMkuMxDN4qRlIuHIrZP98Q==", "dev": true, "requires": { "@atomist/microgrammar": "^0.8.1", @@ -3326,7 +3326,7 @@ }, "invariant": { "version": "2.2.4", - "resolved": "", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { diff --git a/package.json b/package.json index 5793604da..eec5c0242 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@atomist/automation-client": "*" }, "devDependencies": { - "@atomist/automation-client": "1.0.0-master.20180901185845", + "@atomist/automation-client": "1.0.0-master.20180902103822", "@types/mocha": "^5.2.5", "@types/power-assert": "^1.4.29", "axios-mock-adapter": "^1.15.0", diff --git a/src/api-helper/goal/SdmGoalImplementationMapperImpl.ts b/src/api-helper/goal/DefaultGoalImplementationMapper.ts similarity index 75% rename from src/api-helper/goal/SdmGoalImplementationMapperImpl.ts rename to src/api-helper/goal/DefaultGoalImplementationMapper.ts index ccbadcda8..65652c93b 100644 --- a/src/api-helper/goal/SdmGoalImplementationMapperImpl.ts +++ b/src/api-helper/goal/DefaultGoalImplementationMapper.ts @@ -16,51 +16,47 @@ import { Goal } from "../../api/goal/Goal"; import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; -import { IsolatedGoalLauncher } from "../../api/goal/support/IsolatedGoalLauncher"; import { GoalFulfillment, GoalFulfillmentCallback, GoalImplementation, + GoalImplementationMapper, GoalSideEffect, - SdmGoalImplementationMapper, -} from "../../api/goal/support/SdmGoalImplementationMapper"; +} from "../../api/goal/support/GoalImplementationMapper"; import { PushListenerInvocation } from "../../api/listener/PushListener"; /** - * Concrete implementation of SdmGoalImplementationMapper + * Concrete implementation of GoalImplementationMapper */ -export class SdmGoalImplementationMapperImpl implements SdmGoalImplementationMapper { +export class DefaultGoalImplementationMapper implements GoalImplementationMapper { private readonly implementations: GoalImplementation[] = []; private readonly sideEffects: GoalSideEffect[] = []; private readonly callbacks: GoalFulfillmentCallback[] = []; - constructor(private readonly goalLauncher: IsolatedGoalLauncher) { - } - - public async findImplementationBySdmGoal(goal: SdmGoalEvent, inv: PushListenerInvocation): Promise { + public findImplementationBySdmGoal(goal: SdmGoalEvent): GoalImplementation { const matchedNames = this.implementations.filter(m => m.implementationName === goal.fulfillment.name && m.goal.context === goal.externalKey); - const matchedGoalImplementations = []; - for (const implementation of matchedNames) { - if (await implementation.pushTest.mapping(inv)) { - matchedGoalImplementations.push(implementation); - } - } - - if (matchedGoalImplementations.length > 1) { + if (matchedNames.length > 1) { throw new Error("Multiple mappings for name " + goal.fulfillment.name); } - if (matchedGoalImplementations.length === 0) { + if (matchedNames.length === 0) { throw new Error(`No implementation found with name '${goal.fulfillment.name}': ` + `Found ${this.implementations.map(impl => impl.implementationName)}`); } - return matchedGoalImplementations[0]; + return matchedNames[0]; } public addImplementation(implementation: GoalImplementation): this { + if (this.implementations.some(i => + i.implementationName === implementation.implementationName && + i.goal.name === implementation.goal.name && + i.goal.environment === implementation.goal.environment)) { + throw new Error(`Implementation with name '${implementation.implementationName + }' already registered for goal '${implementation.goal.name}'`); + } this.implementations.push(implementation); return this; } @@ -100,8 +96,4 @@ export class SdmGoalImplementationMapperImpl implements SdmGoalImplementationMap (c.goal.definition.environment.slice(0, -1) === g.environment || c.goal.definition.environment === g.environment)); } - - public getIsolatedGoalLauncher(): IsolatedGoalLauncher { - return this.goalLauncher; - } } diff --git a/src/api-helper/goal/chooseAndSetGoals.ts b/src/api-helper/goal/chooseAndSetGoals.ts index c81565f52..0d520b8d4 100644 --- a/src/api-helper/goal/chooseAndSetGoals.ts +++ b/src/api-helper/goal/chooseAndSetGoals.ts @@ -35,13 +35,14 @@ import { ExecuteGoal } from "../../api/goal/GoalInvocation"; import { Goals } from "../../api/goal/Goals"; import { SdmGoalFulfillment, + SdmGoalFulfillmentMethod, SdmGoalMessage, } from "../../api/goal/SdmGoalMessage"; import { + GoalImplementationMapper, isGoalImplementation, - isSideEffect, - SdmGoalImplementationMapper, -} from "../../api/goal/support/SdmGoalImplementationMapper"; + isGoalSideEffect, +} from "../../api/goal/support/GoalImplementationMapper"; import { GoalsSetListener, GoalsSetListenerInvocation, @@ -73,7 +74,7 @@ export interface ChooseAndSetGoalsRules { goalSetter: GoalSetter; - implementationMapping: SdmGoalImplementationMapper; + implementationMapping: GoalImplementationMapper; } /** @@ -121,7 +122,7 @@ export async function determineGoals(rules: { projectLoader: ProjectLoader, repoRefResolver: RepoRefResolver, goalSetter: GoalSetter, - implementationMapping: SdmGoalImplementationMapper, + implementationMapping: GoalImplementationMapper, }, circumstances: { credentials: ProjectOperationCredentials, @@ -155,7 +156,7 @@ export async function determineGoals(rules: { } -async function sdmGoalsFromGoals(implementationMapping: SdmGoalImplementationMapper, +async function sdmGoalsFromGoals(implementationMapping: GoalImplementationMapper, repoRefResolver: RepoRefResolver, pli: PushListenerInvocation, determinedGoals: Goals, @@ -173,19 +174,19 @@ async function sdmGoalsFromGoals(implementationMapping: SdmGoalImplementationMap } async function fulfillment(rules: { - implementationMapping: SdmGoalImplementationMapper, + implementationMapping: GoalImplementationMapper, }, g: Goal, inv: PushListenerInvocation): Promise { const { implementationMapping } = rules; const plan = await implementationMapping.findFulfillmentByPush(g, inv); if (isGoalImplementation(plan)) { return constructSdmGoalImplementation(plan); } - if (isSideEffect(plan)) { - return { method: "side-effect", name: plan.sideEffectName }; + if (isGoalSideEffect(plan)) { + return { method: SdmGoalFulfillmentMethod.SideEffect, name: plan.sideEffectName }; } - logger.warn("FYI, no implementation found for '%s'", g.name); - return { method: "other", name: "unspecified-yo" }; + logger.warn("No implementation found for '%s'", g.name); + return { method: SdmGoalFulfillmentMethod.Other, name: "unknown" }; } /** diff --git a/src/api-helper/goal/executeGoal.ts b/src/api-helper/goal/executeGoal.ts index 0e36f0014..50e58b3b2 100644 --- a/src/api-helper/goal/executeGoal.ts +++ b/src/api-helper/goal/executeGoal.ts @@ -249,7 +249,7 @@ export function markStatus(parameters: { }) { const { context, sdmGoal, goal, result, error, progressLogUrl } = parameters; const newState = result.code !== 0 ? SdmGoalState.failure : - result.requireApproval ? SdmGoalState.waiting_for_approval : SdmGoalState.success; + (result.requireApproval || goal.definition.approvalRequired ? SdmGoalState.waiting_for_approval : SdmGoalState.success); return updateGoal( context, diff --git a/src/api-helper/goal/storeGoals.ts b/src/api-helper/goal/storeGoals.ts index af12aa971..c567d184d 100644 --- a/src/api-helper/goal/storeGoals.ts +++ b/src/api-helper/goal/storeGoals.ts @@ -30,11 +30,12 @@ import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; import { GoalRootType, SdmGoalFulfillment, + SdmGoalFulfillmentMethod, SdmGoalKey, SdmGoalMessage, SdmProvenance, } from "../../api/goal/SdmGoalMessage"; -import { GoalImplementation } from "../../api/goal/support/SdmGoalImplementationMapper"; +import { GoalImplementation } from "../../api/goal/support/GoalImplementationMapper"; import { OnAnyRequestedSdmGoal, OnPushToAnyBranch, @@ -100,7 +101,7 @@ export function goalCorrespondsToSdmGoal(goal: Goal, sdmGoal: SdmGoalKey): boole export function constructSdmGoalImplementation(gi: GoalImplementation): SdmGoalFulfillment { return { - method: "SDM fulfill on requested", + method: SdmGoalFulfillmentMethod.Sdm, name: gi.implementationName, }; } @@ -115,8 +116,8 @@ export function constructSdmGoal(ctx: HandlerContext, parameters: { url?: string, fulfillment?: SdmGoalFulfillment, }): SdmGoalMessage { - const {goalSet, goal, goalSetId, state, id, providerId, url} = parameters; - const fulfillment = parameters.fulfillment || {method: "other", name: "unspecified"}; + const { goalSet, goal, goalSetId, state, id, providerId, url } = parameters; + const fulfillment = parameters.fulfillment || { method: SdmGoalFulfillmentMethod.Other, name: "unknown" }; if (!id.branch) { throw new Error(sprintf("Please provide a branch in the RemoteRepoRef %j", parameters)); diff --git a/src/api-helper/listener/executeAutoInspects.ts b/src/api-helper/listener/executeAutoInspects.ts index e62fb9486..54aba62aa 100644 --- a/src/api-helper/listener/executeAutoInspects.ts +++ b/src/api-helper/listener/executeAutoInspects.ts @@ -34,26 +34,22 @@ import { ReviewerError, } from "../../api/registration/ReviewerError"; import { ReviewListenerRegistration } from "../../api/registration/ReviewListenerRegistration"; -import { ProjectLoader } from "../../spi/project/ProjectLoader"; import { createPushImpactListenerInvocation } from "./createPushImpactListenerInvocation"; import { relevantCodeActions } from "./relevantCodeActions"; /** * Execute auto inspections and route or react to review results using review listeners - * @param {ProjectLoader} projectLoader * @param autoInspectRegistrations - * @param {ReviewListener[]} reviewListeners * @return {ExecuteGoal} */ -export function executeAutoInspects(projectLoader: ProjectLoader, - autoInspectRegistrations: Array>, +export function executeAutoInspects(autoInspectRegistrations: Array>, reviewListeners: ReviewListenerRegistration[]): ExecuteGoal { return async (goalInvocation: GoalInvocation) => { - const { credentials, id, addressChannels } = goalInvocation; + const { configuration, credentials, id, addressChannels } = goalInvocation; try { if (autoInspectRegistrations.length > 0) { logger.info("Planning inspection of %j with %d AutoInspects", id, autoInspectRegistrations.length); - return projectLoader.doWithProject({ credentials, id, readOnly: true }, async project => { + return configuration.sdm.projectLoader.doWithProject({ credentials, id, readOnly: true }, async project => { const cri = await createPushImpactListenerInvocation(goalInvocation, project); const relevantAutoInspects = await relevantCodeActions(autoInspectRegistrations, cri); logger.info("Executing review of %j with %d relevant AutoInspects: [%s] of [%s]", diff --git a/src/api-helper/listener/executeAutofixes.ts b/src/api-helper/listener/executeAutofixes.ts index bd7816cc0..32f9f1ce5 100644 --- a/src/api-helper/listener/executeAutofixes.ts +++ b/src/api-helper/listener/executeAutofixes.ts @@ -31,8 +31,6 @@ import { import { PushImpactListenerInvocation } from "../../api/listener/PushImpactListener"; import { AutofixRegistration } from "../../api/registration/AutofixRegistration"; import { ProgressLog } from "../../spi/log/ProgressLog"; -import { ProjectLoader } from "../../spi/project/ProjectLoader"; -import { RepoRefResolver } from "../../spi/repo-ref/RepoRefResolver"; import { confirmEditedness } from "../command/transform/confirmEditedness"; import { toScalarProjectEditor } from "../machine/handlerRegistrations"; import { createPushImpactListenerInvocation } from "./createPushImpactListenerInvocation"; @@ -41,24 +39,20 @@ import { relevantCodeActions } from "./relevantCodeActions"; /** * Execute autofixes against this push * Throw an error on failure - * @param projectLoader use to load projects * @param {AutofixRegistration[]} registrations - * @param repoRefResolver RepoRefResolver - * @return GoalExecutor + * @return ExecuteGoal */ -export function executeAutofixes(projectLoader: ProjectLoader, - registrations: AutofixRegistration[], - repoRefResolver: RepoRefResolver): ExecuteGoal { +export function executeAutofixes(registrations: AutofixRegistration[]): ExecuteGoal { return async (goalInvocation: GoalInvocation): Promise => { - const { sdmGoal, credentials, context, progressLog } = goalInvocation; + const { configuration, sdmGoal, credentials, context, progressLog } = goalInvocation; progressLog.write(sprintf("Executing %d autofixes", registrations.length)); try { if (registrations.length === 0) { return Success; } const push = sdmGoal.push; - const editableRepoRef = repoRefResolver.toRemoteRepoRef(sdmGoal.push.repo, { branch: push.branch }); - const editResult = await projectLoader.doWithProject({ + const editableRepoRef = configuration.sdm.repoRefResolver.toRemoteRepoRef(sdmGoal.push.repo, { branch: push.branch }); + const editResult = await configuration.sdm.projectLoader.doWithProject({ credentials, id: editableRepoRef, context, diff --git a/src/api-helper/listener/executeFingerprinting.ts b/src/api-helper/listener/executeFingerprinting.ts new file mode 100644 index 000000000..d914f5065 --- /dev/null +++ b/src/api-helper/listener/executeFingerprinting.ts @@ -0,0 +1,63 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + logger, + Success, +} from "@atomist/automation-client"; +import { Fingerprint } from "@atomist/automation-client/project/fingerprint/Fingerprint"; +import { + ExecuteGoal, + GoalInvocation, +} from "../../api/goal/GoalInvocation"; +import { FingerprintListener } from "../../api/listener/FingerprintListener"; +import { FingerprinterRegistration } from "../../api/registration/FingerprinterRegistration"; +import { computeFingerprints } from "./computeFingerprints"; +import { createPushImpactListenerInvocation } from "./createPushImpactListenerInvocation"; +import { relevantCodeActions } from "./relevantCodeActions"; + +/** + * Execute fingerprinting and send fingerprints to Atomist + * @param {FingerprinterRegistration} fingerprinters + * @param listeners listeners to fingerprints + */ +export function executeFingerprinting(fingerprinters: FingerprinterRegistration[], + listeners: FingerprintListener[]): ExecuteGoal { + return async (goalInvocation: GoalInvocation) => { + const { configuration, id, credentials, context } = goalInvocation; + if (fingerprinters.length === 0) { + return Success; + } + + logger.debug("About to fingerprint %j using %d fingerprinters", id, fingerprinters.length); + await configuration.sdm.projectLoader.doWithProject({ credentials, id, readOnly: true }, async project => { + const cri = await createPushImpactListenerInvocation(goalInvocation, project); + const relevantFingerprinters: FingerprinterRegistration[] = await relevantCodeActions(fingerprinters, cri); + logger.info("Will invoke %d eligible fingerprinters of %d to %j", + relevantFingerprinters.length, fingerprinters.length, cri.project.id); + const fingerprints: Fingerprint[] = await computeFingerprints(cri, relevantFingerprinters.map(fp => fp.action)); + await Promise.all(listeners.map(l => + Promise.all(fingerprints.map(fingerprint => l({ + id, + context, + credentials, + addressChannels: cri.addressChannels, + fingerprint, + }))))); + }); + return Success; + }; +} diff --git a/src/api-helper/listener/executePushReactions.ts b/src/api-helper/listener/executePushReactions.ts index 00efa23d3..4c7f3c60b 100644 --- a/src/api-helper/listener/executePushReactions.ts +++ b/src/api-helper/listener/executePushReactions.ts @@ -39,15 +39,14 @@ import { relevantCodeActions } from "./relevantCodeActions"; * @param {PushImpactListenerRegistration[]} registrations * @return {ExecuteGoal} */ -export function executePushReactions(projectLoader: ProjectLoader, - registrations: PushImpactListenerRegisterable[]): ExecuteGoal { +export function executePushReactions(registrations: PushImpactListenerRegisterable[]): ExecuteGoal { return async (goalInvocation: GoalInvocation) => { if (registrations.length === 0) { return Success; } - const {credentials, id, context} = goalInvocation; - return projectLoader.doWithProject({credentials, id, context, readOnly: true}, async project => { + const { configuration, credentials, id, context } = goalInvocation; + return configuration.sdm.projectLoader.doWithProject({ credentials, id, context, readOnly: true }, async project => { const cri: PushImpactListenerInvocation = await createPushImpactListenerInvocation(goalInvocation, project); const regs = registrations.map(toPushReactionRegistration); const relevantCodeReactions: PushImpactListenerRegistration[] = await relevantCodeActions(regs, cri); diff --git a/src/api-helper/machine/AbstractSoftwareDeliveryMachine.ts b/src/api-helper/machine/AbstractSoftwareDeliveryMachine.ts index 003621a89..42fe93149 100644 --- a/src/api-helper/machine/AbstractSoftwareDeliveryMachine.ts +++ b/src/api-helper/machine/AbstractSoftwareDeliveryMachine.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { HandleCommand, HandleEvent, logger } from "@atomist/automation-client"; +import { + HandleCommand, + HandleEvent, + logger, +} from "@atomist/automation-client"; import { toStringArray } from "@atomist/automation-client/internal/util/string"; import { RemoteRepoRef } from "@atomist/automation-client/operations/common/RepoId"; import { NoParameters } from "@atomist/automation-client/SmartParameters"; @@ -28,6 +32,7 @@ import { Goals } from "../../api/goal/Goals"; import { ReportProgress } from "../../api/goal/progress/ReportProgress"; import { CommandListenerInvocation } from "../../api/listener/CommandListener"; import { ExtensionPack } from "../../api/machine/ExtensionPack"; +import { registrableManager } from "../../api/machine/registrable"; import { SoftwareDeliveryMachine } from "../../api/machine/SoftwareDeliveryMachine"; import { SoftwareDeliveryMachineConfiguration } from "../../api/machine/SoftwareDeliveryMachineOptions"; import { StagingEndpointGoal, StagingVerifiedGoal } from "../../api/machine/wellKnownGoals"; @@ -53,6 +58,7 @@ import { EnforceableProjectInvariantRegistration, InvarianceAssessment, } from "../../api/registration/ProjectInvariantRegistration"; +import { WellKnownGoals } from "../../pack/well-known-goals/addWellKnownGoals"; import { InterpretLog } from "../../spi/log/InterpretedLog"; import { executeVerifyEndpoint, @@ -217,8 +223,9 @@ export abstract class AbstractSoftwareDeliveryMachine) { super(); // If we didn't get any goal setters don't register a mapping + registrableManager().register(this); + if (goalSetters.length > 0) { this.pushMap = new PushRules("Goal setters", _.flatten(goalSetters)); } diff --git a/src/api-helper/test/fakeGoalInvocation.ts b/src/api-helper/test/fakeGoalInvocation.ts index 820b66cca..566ebf611 100644 --- a/src/api-helper/test/fakeGoalInvocation.ts +++ b/src/api-helper/test/fakeGoalInvocation.ts @@ -22,6 +22,7 @@ import { import { LoggingProgressLog } from "../../api-helper/log/LoggingProgressLog"; import { GoalInvocation } from "../../api/goal/GoalInvocation"; import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; +import { SoftwareDeliveryMachineOptions } from "../../api/machine/SoftwareDeliveryMachineOptions"; import { SdmGoalState } from "../../typings/types"; import { fakeContext } from "./fakeContext"; @@ -30,7 +31,7 @@ import { fakeContext } from "./fakeContext"; * @param {RemoteRepoRef} id * @return {GoalInvocation} */ -export function fakeGoalInvocation(id: RemoteRepoRef): GoalInvocation { +export function fakeGoalInvocation(id: RemoteRepoRef, options?: SoftwareDeliveryMachineOptions): GoalInvocation { return { credentials: { token: "foobar" }, context: fakeContext("T1111"), @@ -40,6 +41,11 @@ export function fakeGoalInvocation(id: RemoteRepoRef): GoalInvocation { }, progressLog: new LoggingProgressLog("fake"), sdmGoal: fakeSdmGoal(id), + configuration: { + sdm: { + ...options, + }, + } as any, }; } diff --git a/src/api-helper/voter/githubTeamVote.ts b/src/api-helper/voter/githubTeamVoter.ts similarity index 95% rename from src/api-helper/voter/githubTeamVote.ts rename to src/api-helper/voter/githubTeamVoter.ts index ce8fc1ae2..664ac0757 100644 --- a/src/api-helper/voter/githubTeamVote.ts +++ b/src/api-helper/voter/githubTeamVoter.ts @@ -28,7 +28,7 @@ import { GitHubLogin } from "../../typings/types"; * person who is requesting the approval. * @param {string} team */ -export function gitHubTeamVote(team: string = "atomist-automation"): GoalApprovalRequestVoter { +export function githubTeamVoter(team: string = "atomist-automation"): GoalApprovalRequestVoter { return async gai => { const approval = gai.goal.approval; const repo = gai.goal.repo; diff --git a/src/api/goal/GoalInvocation.ts b/src/api/goal/GoalInvocation.ts index 8c2a28311..45843c777 100644 --- a/src/api/goal/GoalInvocation.ts +++ b/src/api/goal/GoalInvocation.ts @@ -15,6 +15,7 @@ */ import { GitProject } from "@atomist/automation-client/project/git/GitProject"; +import { SoftwareDeliveryMachineConfiguration } from "../.."; import { ProgressLog } from "../../spi/log/ProgressLog"; import { RepoContext } from "../context/SdmContext"; import { ExecuteGoalResult } from "./ExecuteGoalResult"; @@ -26,6 +27,8 @@ export type PrepareForGoalExecution = (p: GitProject, r: GoalInvocation) => Prom export interface GoalInvocation extends RepoContext { + configuration: SoftwareDeliveryMachineConfiguration; + sdmGoal: SdmGoalEvent; progressLog: ProgressLog; diff --git a/src/api/goal/GoalWithFulfillment.ts b/src/api/goal/GoalWithFulfillment.ts new file mode 100644 index 000000000..19245b4e1 --- /dev/null +++ b/src/api/goal/GoalWithFulfillment.ts @@ -0,0 +1,165 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InterpretLog } from "../../spi/log/InterpretedLog"; +import { + registerRegistrable, + Registrable, +} from "../machine/registrable"; +import { SoftwareDeliveryMachine } from "../machine/SoftwareDeliveryMachine"; +import { PushTest } from "../mapping/PushTest"; +import { AnyPush } from "../mapping/support/commonPushTests"; +import { + GoalDefinition, + GoalWithPrecondition, +} from "./Goal"; +import { ExecuteGoal } from "./GoalInvocation"; +import { ReportProgress } from "./progress/ReportProgress"; +import { GoalFulfillmentCallback } from "./support/GoalImplementationMapper"; + +export type Fulfillment = Implementation | SideEffect; + +export interface Implementation { + name: string; + goalExecutor: ExecuteGoal; + logInterpreter: InterpretLog; + progressReporter?: ReportProgress; + pushTest?: PushTest; +} + +export function isImplementation(f: Fulfillment): f is Implementation { + return !!f && !!(f as Implementation).goalExecutor && true; +} + +export interface SideEffect { + name: string; + pushTest?: PushTest; +} + +export function isSideEffect(f: Fulfillment): f is SideEffect { + return !isImplementation(f); +} + +/** + * Goal that registers goal implementations, side effects and callbacks on the + * current SDM. No additional registration with the SDM is needed. + */ +export abstract class FulfillableGoal extends GoalWithPrecondition implements Registrable { + + private readonly fulfillments: Fulfillment[] = []; + private readonly callbacks: GoalFulfillmentCallback[] = []; + private sdm: SoftwareDeliveryMachine; + + constructor(public definition: GoalDefinition) { + super(definition); + registerRegistrable(this); + } + + public register(sdm: SoftwareDeliveryMachine): void { + this.sdm = sdm; + this.fulfillments.forEach(fulfillment => { + this.registerFulfillment(fulfillment); + }); + this.callbacks.forEach(cb => this.registerCallback(cb)); + } + + protected addFulfillmentCallback(cb: GoalFulfillmentCallback): this { + if (this.sdm) { + this.registerCallback(cb); + } + this.callbacks.push(cb); + return this; + } + + protected addFulfillment(fulfillment: Fulfillment): this { + if (this.sdm) { + this.registerFulfillment(fulfillment); + } + this.fulfillments.push(fulfillment); + return this; + } + + private registerFulfillment(fulfillment: Fulfillment): void { + if (isImplementation(fulfillment)) { + this.sdm.addGoalImplementation( + fulfillment.name, + this, + fulfillment.goalExecutor, + { + pushTest: fulfillment.pushTest || AnyPush, + progressReporter: fulfillment.progressReporter, + logInterpreter: fulfillment.logInterpreter, + }); + } else if (isSideEffect(fulfillment)) { + this.sdm.addGoalSideEffect(this, fulfillment.name, fulfillment.pushTest); + } + } + + private registerCallback(cb: GoalFulfillmentCallback): void { + this.sdm.goalFulfillmentMapper.addFulfillmentCallback(cb); + } +} + +/** + * Goal that accepts registrations of R. + */ +export abstract class FulfillableGoalWithRegistrations extends FulfillableGoal { + + protected registrations: R[] = []; + + constructor(public definition: GoalDefinition) { + super(definition); + } + + public with(registration: R): this { + this.registrations.push(registration); + return this; + } +} + +/** + * Goal that accepts registrations of R and listeners of L. + */ +export abstract class FulfillableGoalWithRegistrationsAndListeners extends FulfillableGoalWithRegistrations { + + protected listeners: L[] = []; + + constructor(public definition: GoalDefinition) { + super(definition); + } + + public withListener(listener: L): this { + this.listeners.push(listener); + return this; + } +} + +/** + * Generic goal that can be used with a GoalDefinition. + * Register goal implementations or side effects to this goal instance. + */ +export class GoalWithFulfillment extends FulfillableGoal { + + public withCallback(cb: GoalFulfillmentCallback): this { + this.addFulfillmentCallback(cb); + return this; + } + + public with(fulfillment: Fulfillment): this { + this.addFulfillment(fulfillment); + return this; + } +} diff --git a/src/api/goal/Goals.ts b/src/api/goal/Goals.ts index 9157c496c..b505f4e1b 100644 --- a/src/api/goal/Goals.ts +++ b/src/api/goal/Goals.ts @@ -95,7 +95,7 @@ export interface GoalsAndPreConditionBuilder extends GoalsBuilder { * * const simpleGoals = goals("Simple Goals") * .plan(CodeInspectionGoal) - * .plan(BuildGoal, AutofixGoal).after(CodeInspectionGoal) + * .plan(BuildGoal, Autofix).after(CodeInspectionGoal) * .plan(StagingEndpointGoal).after(BuildGoal) * .plan(ProductionDeploymentGoal).after(BuildGoal, StagingEndpointGoal); * diff --git a/src/api/goal/SdmGoalMessage.ts b/src/api/goal/SdmGoalMessage.ts index a27bca744..983237c33 100644 --- a/src/api/goal/SdmGoalMessage.ts +++ b/src/api/goal/SdmGoalMessage.ts @@ -18,7 +18,11 @@ import { SdmGoalState } from "../../typings/types"; export const GoalRootType = "SdmGoal"; -export type SdmGoalFulfillmentMethod = "SDM fulfill on requested" | "side-effect" | "other"; +export enum SdmGoalFulfillmentMethod { + Sdm = "sdm", + SideEffect = "side-effect", + Other = "other", +} export interface SdmGoalFulfillment { method: SdmGoalFulfillmentMethod; diff --git a/src/api/goal/common/Autofix.ts b/src/api/goal/common/Autofix.ts new file mode 100644 index 000000000..6b8c891f1 --- /dev/null +++ b/src/api/goal/common/Autofix.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { executeAutofixes } from "../../../api-helper/listener/executeAutofixes"; +import { LogSuppressor } from "../../../api-helper/log/logInterpreters"; +import { AutofixGoal } from "../../machine/wellKnownGoals"; +import { AutofixRegistration } from "../../registration/AutofixRegistration"; +import { FulfillableGoalWithRegistrations } from "../GoalWithFulfillment"; + +/** + * Goal that performs autofixes: For example, linting and adding license headers. + */ +export class Autofix extends FulfillableGoalWithRegistrations { + + constructor(private readonly uniqueName: string) { + + super({ + ...AutofixGoal.definition, + uniqueName, + orderedName: `0.2-${uniqueName.toLowerCase()}`, + }); + + this.addFulfillment({ + name: `Autofix-${this.uniqueName}`, + logInterpreter: LogSuppressor, + goalExecutor: executeAutofixes(this.registrations), + }); + } +} diff --git a/src/api/goal/common/CodeInspects.ts b/src/api/goal/common/CodeInspects.ts new file mode 100644 index 000000000..83a17e06e --- /dev/null +++ b/src/api/goal/common/CodeInspects.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { executeAutoInspects } from "../../../api-helper/listener/executeAutoInspects"; +import { CodeInspectionGoal } from "../../machine/wellKnownGoals"; +import { CodeInspectionRegistration } from "../../registration/CodeInspectionRegistration"; +import { ReviewListenerRegistration } from "../../registration/ReviewListenerRegistration"; +import { FulfillableGoalWithRegistrationsAndListeners } from "../GoalWithFulfillment"; + +/** + * Goal that runs code inspections + */ +export class CodeInspects + extends FulfillableGoalWithRegistrationsAndListeners, ReviewListenerRegistration> { + + constructor(private readonly uniqueName: string) { + + super({ + ...CodeInspectionGoal.definition, + uniqueName, + orderedName: `1-${uniqueName.toLowerCase()}`, + }); + + this.addFulfillment({ + name: `Inspect-${this.uniqueName}`, + goalExecutor: executeAutoInspects(this.registrations, this.listeners), + }); + } +} diff --git a/src/api/goal/common/Fingerprint.ts b/src/api/goal/common/Fingerprint.ts new file mode 100644 index 000000000..7d9e079f3 --- /dev/null +++ b/src/api/goal/common/Fingerprint.ts @@ -0,0 +1,43 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { executeFingerprinting } from "../../../api-helper/listener/executeFingerprinting"; +import { FingerprintListener } from "../../listener/FingerprintListener"; +import { FingerprintGoal } from "../../machine/wellKnownGoals"; +import { FingerprinterRegistration } from "../../registration/FingerprinterRegistration"; +import { FulfillableGoalWithRegistrationsAndListeners } from "../GoalWithFulfillment"; + +/** + * Goal that performs fingerprinting. Typically invoked early in a delivery flow. + */ +export class Fingerprint + extends FulfillableGoalWithRegistrationsAndListeners { + + constructor(private readonly uniqueName: string) { + + super({ + ...FingerprintGoal.definition, + uniqueName, + orderedName: `0.1-${uniqueName.toLowerCase()}`, + }); + + this.addFulfillment({ + name: `Fingerprint-${this.uniqueName}`, + goalExecutor: executeFingerprinting(this.registrations, + this.listeners), + }); + } +} diff --git a/src/api/goal/common/PushImpact.ts b/src/api/goal/common/PushImpact.ts new file mode 100644 index 000000000..31caaea34 --- /dev/null +++ b/src/api/goal/common/PushImpact.ts @@ -0,0 +1,40 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { executePushReactions } from "../../../api-helper/listener/executePushReactions"; +import { PushReactionGoal } from "../../machine/wellKnownGoals"; +import { PushImpactListenerRegistration } from "../../registration/PushImpactListenerRegistration"; +import { FulfillableGoalWithRegistrations } from "../GoalWithFulfillment"; + +/** + * Goal that performs fingerprinting. Typically invoked early in a delivery flow. + */ +export class PushImpact extends FulfillableGoalWithRegistrations { + + constructor(private readonly uniqueName: string) { + + super({ + ...PushReactionGoal.definition, + uniqueName, + orderedName: `1.5-${uniqueName.toLowerCase()}`, + }); + + this.addFulfillment({ + name: `PushImpact-${this.uniqueName}`, + goalExecutor: executePushReactions(this.registrations), + }); + } +} diff --git a/src/api/goal/support/SdmGoalImplementationMapper.ts b/src/api/goal/support/GoalImplementationMapper.ts similarity index 86% rename from src/api/goal/support/SdmGoalImplementationMapper.ts rename to src/api/goal/support/GoalImplementationMapper.ts index da8e89eed..50edf9d5e 100644 --- a/src/api/goal/support/SdmGoalImplementationMapper.ts +++ b/src/api/goal/support/GoalImplementationMapper.ts @@ -22,7 +22,6 @@ import { Goal } from "../Goal"; import { ExecuteGoal } from "../GoalInvocation"; import { ReportProgress } from "../progress/ReportProgress"; import { SdmGoalEvent } from "../SdmGoalEvent"; -import { IsolatedGoalLauncher } from "./IsolatedGoalLauncher"; export type GoalFulfillment = GoalImplementation | GoalSideEffect; @@ -45,7 +44,7 @@ export interface GoalSideEffect { pushTest: PushTest; } -export function isSideEffect(f: GoalFulfillment): f is GoalSideEffect { +export function isGoalSideEffect(f: GoalFulfillment): f is GoalSideEffect { return !!f && (f as GoalSideEffect).sideEffectName && true; } @@ -62,15 +61,13 @@ export interface GoalFulfillmentCallback { /** * Registers and looks up goal implementations */ -export interface SdmGoalImplementationMapper { +export interface GoalImplementationMapper { addSideEffect(sideEffect: GoalSideEffect): this; addFulfillmentCallback(callback: GoalFulfillmentCallback): this; - findImplementationBySdmGoal(goal: SdmGoalEvent, inv: PushListenerInvocation): Promise; - - getIsolatedGoalLauncher(): IsolatedGoalLauncher; + findImplementationBySdmGoal(goal: SdmGoalEvent): GoalImplementation; findFulfillmentByPush(goal: Goal, inv: PushListenerInvocation): Promise; diff --git a/src/api/machine/GoalDrivenMachine.ts b/src/api/machine/GoalDrivenMachine.ts index 4a923e34a..e1ba1c240 100644 --- a/src/api/machine/GoalDrivenMachine.ts +++ b/src/api/machine/GoalDrivenMachine.ts @@ -19,7 +19,7 @@ import { Goal } from "../goal/Goal"; import { ExecuteGoal } from "../goal/GoalInvocation"; import { Goals } from "../goal/Goals"; import { ReportProgress } from "../goal/progress/ReportProgress"; -import { SdmGoalImplementationMapper } from "../goal/support/SdmGoalImplementationMapper"; +import { GoalImplementationMapper } from "../goal/support/GoalImplementationMapper"; import { GoalSetter } from "../mapping/GoalSetter"; import { PushMapping } from "../mapping/PushMapping"; import { PushTest } from "../mapping/PushTest"; @@ -70,10 +70,10 @@ export interface GoalDrivenMachine; + /** + * Strategy for launching goals in different infrastructure + */ + goalLauncher?: IsolatedGoalLauncher; + } /** diff --git a/src/api/machine/registrable.ts b/src/api/machine/registrable.ts new file mode 100644 index 000000000..4b2684b14 --- /dev/null +++ b/src/api/machine/registrable.ts @@ -0,0 +1,57 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SoftwareDeliveryMachine } from "./SoftwareDeliveryMachine"; + +export interface Registrable { + register(sdm: SoftwareDeliveryMachine): void; +} + +class RegistrableManager implements Registrable { + + public readonly registrables: Registrable[] = []; + public sdm: SoftwareDeliveryMachine; + + public addRegistrable(registrable: Registrable): void { + if (this.sdm) { + registrable.register(this.sdm); + } else { + this.registrables.push(registrable); + } + } + + public register(sdm: SoftwareDeliveryMachine): void { + this.registrables.forEach(r => { + r.register(sdm); + this.registrables.splice(this.registrables.indexOf(r), 1); + }); + this.sdm = sdm; + } +} + +(global as any).__registrable = new RegistrableManager(); + +export function resetRegistrableManager(): void { + (global as any).__registrable = new RegistrableManager(); +} + +export function registrableManager(): Registrable { + return (global as any).__registrable; +} + +export function registerRegistrable(registrable: Registrable): void { + (registrableManager() as any).addRegistrable(registrable); +} diff --git a/src/index.ts b/src/index.ts index 333fa2be3..8c79c23c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,14 +19,19 @@ export * from "./api/goal/ExecuteGoalResult"; export * from "./api/goal/GitHubContext"; export * from "./api/goal/Goal"; export * from "./api/goal/GoalInvocation"; +export * from "./api/goal/GoalWithFulfillment"; export * from "./api/goal/Goals"; export * from "./api/goal/SdmGoalEvent"; export * from "./api/goal/SdmGoalMessage"; +export * from "./api/goal/common/Autofix"; +export * from "./api/goal/common/CodeInspects"; +export * from "./api/goal/common/Fingerprint"; export * from "./api/goal/common/GenericGoal"; export * from "./api/goal/common/MessageGoal"; +export * from "./api/goal/common/PushImpact"; export * from "./api/goal/progress/ReportProgress"; +export * from "./api/goal/support/GoalImplementationMapper"; export * from "./api/goal/support/IsolatedGoalLauncher"; -export * from "./api/goal/support/SdmGoalImplementationMapper"; export * from "./api/goal/support/environment"; export * from "./api/goal/support/executeSendMessageToSlack"; export * from "./api/listener/ArtifactListener"; @@ -63,6 +68,7 @@ export * from "./api/machine/MachineConfigurer"; export * from "./api/machine/RepoTargets"; export * from "./api/machine/SoftwareDeliveryMachine"; export * from "./api/machine/SoftwareDeliveryMachineOptions"; +export * from "./api/machine/registrable"; export * from "./api/machine/wellKnownGoals"; export * from "./api/mapping/GoalSetter"; export * from "./api/mapping/Mapping"; diff --git a/src/pack/well-known-goals/addWellKnownGoals.ts b/src/pack/well-known-goals/addWellKnownGoals.ts new file mode 100644 index 000000000..c9da1703b --- /dev/null +++ b/src/pack/well-known-goals/addWellKnownGoals.ts @@ -0,0 +1,63 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { executeImmaterial } from "../../api-helper/goal/chooseAndSetGoals"; +import { executeAutofixes } from "../../api-helper/listener/executeAutofixes"; +import { executeAutoInspects } from "../../api-helper/listener/executeAutoInspects"; +import { executeFingerprinting } from "../../api-helper/listener/executeFingerprinting"; +import { executePushReactions } from "../../api-helper/listener/executePushReactions"; +import { LogSuppressor } from "../../api-helper/log/logInterpreters"; +import { metadata } from "../../api-helper/misc/extensionPack"; +import { ExtensionPack } from "../../api/machine/ExtensionPack"; +import { SoftwareDeliveryMachine } from "../../api/machine/SoftwareDeliveryMachine"; +import { + ArtifactGoal, + AutofixGoal, + CodeInspectionGoal, + FingerprintGoal, + NoGoal, + PushReactionGoal, +} from "../../api/machine/wellKnownGoals"; +import { AnyPush } from "../../api/mapping/support/commonPushTests"; + +/** + * Add well known goals to the given SDM + * @param {SoftwareDeliveryMachine} sdm + */ +export const WellKnownGoals: ExtensionPack = { + ...metadata("well-known-goals"), + configure, +}; + +function configure(sdm: SoftwareDeliveryMachine) { + sdm.addGoalImplementation("Autofix", AutofixGoal, + executeAutofixes(sdm.autofixRegistrations), + { + // Autofix errors should not be reported to the user + logInterpreter: LogSuppressor, + }) + .addGoalImplementation("DoNothing", NoGoal, executeImmaterial) + .addGoalImplementation("FingerprinterRegistration", FingerprintGoal, + executeFingerprinting( + sdm.fingerprinterRegistrations, + sdm.fingerprintListeners)) + .addGoalImplementation("CodeReactions", PushReactionGoal, + executePushReactions(sdm.pushImpactListenerRegistrations)) + .addGoalImplementation("CodeInspections", CodeInspectionGoal, + executeAutoInspects(sdm.autoInspectRegistrations, sdm.reviewListenerRegistrations)) + .addVerifyImplementation(); + sdm.addGoalSideEffect(ArtifactGoal, sdm.configuration.name, AnyPush); +} diff --git a/src/pack/well-known-goals/commonGoals.ts b/src/pack/well-known-goals/commonGoals.ts new file mode 100644 index 000000000..7131f55a5 --- /dev/null +++ b/src/pack/well-known-goals/commonGoals.ts @@ -0,0 +1,113 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Goal, + GoalWithPrecondition, +} from "../../api/goal/Goal"; +import { Goals } from "../../api/goal/Goals"; +import { + IndependentOfEnvironment, + ProjectDisposalEnvironment, +} from "../../api/goal/support/environment"; +import { + BuildGoal, + LocalDeploymentGoal, + NoGoal, +} from "../../api/machine/wellKnownGoals"; + +/** + * @ModuleExport + */ +export const VersionGoal = new Goal({ + uniqueName: "Version", + environment: IndependentOfEnvironment, + orderedName: "0.1-version", + workingDescription: "Calculating project version", + completedDescription: "Versioned", +}); + +/** + * @ModuleExport + */ +export const DockerBuildGoal = new GoalWithPrecondition({ + uniqueName: "DockerBuild", + environment: IndependentOfEnvironment, + orderedName: "3-docker", + displayName: "docker build", + workingDescription: "Running docker build", + completedDescription: "Docker build successful", + failedDescription: "Docker build failed", + isolated: true, +}, BuildGoal); + +/** + * @ModuleExport + */ +export const TagGoal = new GoalWithPrecondition({ + uniqueName: "Tag", + environment: IndependentOfEnvironment, + orderedName: "4-tag", + displayName: "tag", + workingDescription: "Tagging", + completedDescription: "Tagged", + failedDescription: "Failed to create Tag", +}, DockerBuildGoal, BuildGoal); + +/** + * @ModuleExport + */ +export const StagingUndeploymentGoal = new Goal({ + uniqueName: "UndeployFromTest", + environment: ProjectDisposalEnvironment, + orderedName: "2-staging-undeploy", + displayName: "undeploy from test", + completedDescription: "not deployed in test", +}); + +/** + * @ModuleExport + */ +export const LocalUndeploymentGoal = new Goal({ + uniqueName: "UndeployHere", + environment: ProjectDisposalEnvironment, + orderedName: "1-undeploy-locally", + failedDescription: "Failed at local undeploy", + completedDescription: "not deployed locally", +}); + +/** + * @ModuleExport + */ +// not an enforced precondition, but it's real enough to graph +export const LocalEndpointGoal = new GoalWithPrecondition({ + uniqueName: "FindLocalEndpoint", + environment: IndependentOfEnvironment, + orderedName: "2-endpoint", + displayName: "locate local service endpoint", + completedDescription: "Here is the local service endpoint", + +}, LocalDeploymentGoal); + +/** + * Special Goals object to be returned if changes are immaterial. + * The identity of this object is important. + * @type {Goals} + * @ModuleExport + */ +export const NoGoals = new Goals( + "No action needed", + NoGoal); diff --git a/src/pack/well-known-goals/httpServiceGoals.ts b/src/pack/well-known-goals/httpServiceGoals.ts new file mode 100644 index 000000000..1d6151d04 --- /dev/null +++ b/src/pack/well-known-goals/httpServiceGoals.ts @@ -0,0 +1,79 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Goals } from "../../api/goal/Goals"; +import { + ArtifactGoal, + AutofixGoal, + BuildGoal, + CodeInspectionGoal, + DeleteAfterUndeploysGoal, + DeleteRepositoryGoal, + FingerprintGoal, + LocalDeploymentGoal, + ProductionDeploymentGoal, + ProductionEndpointGoal, + ProductionUndeploymentGoal, + PushReactionGoal, + StagingDeploymentGoal, + StagingEndpointGoal, + StagingVerifiedGoal, +} from "../../api/machine/wellKnownGoals"; +import { + LocalEndpointGoal, + LocalUndeploymentGoal, + StagingUndeploymentGoal, +} from "./commonGoals"; + +/** + * Goals for an Http service, mirroring a typical flow through + * staging to production. + * Goal sets are normally user defined, so this is largely + * an illustration. + * @type {Goals} + */ +export const HttpServiceGoals = new Goals( + "HTTP Service", + FingerprintGoal, + AutofixGoal, + CodeInspectionGoal, + PushReactionGoal, + BuildGoal, + ArtifactGoal, + StagingDeploymentGoal, + StagingEndpointGoal, + StagingVerifiedGoal, + ProductionDeploymentGoal, + ProductionEndpointGoal); + +export const LocalDeploymentGoals = new Goals( + "Local Deployment", + PushReactionGoal, + LocalDeploymentGoal, + LocalEndpointGoal); + +export const UndeployEverywhereGoals = new Goals( + "Undeploy all environments", + LocalUndeploymentGoal, + StagingUndeploymentGoal, + ProductionUndeploymentGoal, + DeleteAfterUndeploysGoal, +); + +export const RepositoryDeletionGoals = new Goals( + "Offer to delete repository", + DeleteRepositoryGoal, +); diff --git a/src/pack/well-known-goals/libraryGoals.ts b/src/pack/well-known-goals/libraryGoals.ts new file mode 100644 index 000000000..25d2a939d --- /dev/null +++ b/src/pack/well-known-goals/libraryGoals.ts @@ -0,0 +1,31 @@ +/* + * Copyright © 2018 Atomist, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Goals } from "../../api/goal/Goals"; +import { + CodeInspectionGoal, + JustBuildGoal, +} from "../../api/machine/wellKnownGoals"; + +/** + * Flow to build a library + * @type {Goals} + */ +export const LibraryGoals = new Goals( + "Library", + CodeInspectionGoal, + JustBuildGoal, +); diff --git a/test/api-helper/listener/executeAutoInspect.test.ts b/test/api-helper/listener/executeAutoInspect.test.ts index cc2dbf30d..ff9b0cd31 100644 --- a/test/api-helper/listener/executeAutoInspect.test.ts +++ b/test/api-helper/listener/executeAutoInspect.test.ts @@ -86,11 +86,13 @@ describe("executeAutoInspects", () => { const p = InMemoryProject.from(id); const reviewEvents: ReviewListenerInvocation[] = []; const l = loggingReviewListenerWithApproval(reviewEvents); - const ge = executeAutoInspects(new SingleProjectLoader(p), [HatesTheWorld], [{ + const ge = executeAutoInspects([HatesTheWorld], [{ name: "thing", listener: l, }]); - const r = await ge(fakeGoalInvocation(id)); + const r = await ge(fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any)); assert.equal(r.code, 0); assert(!r.requireApproval); assert.equal(reviewEvents.length, 1); @@ -102,11 +104,13 @@ describe("executeAutoInspects", () => { const p = InMemoryProject.from(id, new InMemoryFile("thing", "1")); const reviewEvents: ReviewListenerInvocation[] = []; const l = loggingReviewListenerWithApproval(reviewEvents); - const ge = executeAutoInspects(new SingleProjectLoader(p), [HatesTheWorld], [{ + const ge = executeAutoInspects([HatesTheWorld], [{ name: "thing", listener: l, }]); - const rwlc = fakeGoalInvocation(id); + const rwlc = fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any); const r = await ge(rwlc); assert.equal(reviewEvents.length, 1); assert.equal(reviewEvents[0].review.comments.length, 1); @@ -120,11 +124,13 @@ describe("executeAutoInspects", () => { const p = InMemoryProject.from(id, new InMemoryFile("thing", "1")); const reviewEvents: ReviewListenerInvocation[] = []; const listener = loggingReviewListenerWithoutApproval(reviewEvents); - const ge = executeAutoInspects(new SingleProjectLoader(p), [HatesTheWorld], [{ + const ge = executeAutoInspects([HatesTheWorld], [{ name: "thing", listener, }]); - const rwlc = fakeGoalInvocation(id); + const rwlc = fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any); const r = await ge(rwlc); assert.equal(reviewEvents.length, 1); assert.equal(reviewEvents[0].review.comments.length, 1); @@ -138,12 +144,14 @@ describe("executeAutoInspects", () => { const p = InMemoryProject.from(id, new InMemoryFile("thing", "1")); const reviewEvents: ReviewListenerInvocation[] = []; const listener = loggingReviewListenerWithApproval(reviewEvents); - const ge = executeAutoInspects(new SingleProjectLoader(p), [HatesTheWorld, JustTheOne], + const ge = executeAutoInspects([HatesTheWorld, JustTheOne], [{ name: "thing", listener, }]); - const rwlc = fakeGoalInvocation(id); + const rwlc = fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any); const r = await ge(rwlc); assert.equal(reviewEvents.length, 1); assert.equal(reviewEvents[0].review.comments.length, 2); diff --git a/test/api-helper/listener/executeAutofixes.test.ts b/test/api-helper/listener/executeAutofixes.test.ts index 48876a577..752b99eaa 100644 --- a/test/api-helper/listener/executeAutofixes.test.ts +++ b/test/api-helper/listener/executeAutofixes.test.ts @@ -34,7 +34,11 @@ import { PushListenerInvocation } from "../../../src/api/listener/PushListener"; import { pushTest } from "../../../src/api/mapping/PushTest"; import { AutofixRegistration } from "../../../src/api/registration/AutofixRegistration"; import { RepoRefResolver } from "../../../src/spi/repo-ref/RepoRefResolver"; -import { CoreRepoFieldsAndChannels, OnPushToAnyBranch, ScmProvider } from "../../../src/typings/types"; +import { + CoreRepoFieldsAndChannels, + OnPushToAnyBranch, + ScmProvider, +} from "../../../src/typings/types"; export const AddThingAutofix: AutofixRegistration = { name: "AddThing", @@ -118,9 +122,10 @@ describe("executeAutofixes", () => { it("should execute none", async () => { const id = new GitHubRepoRef("a", "b"); const pl = new SingleProjectLoader({ id } as any); - const r = await executeAutofixes(pl, - [], - FakeRepoRefResolver)(fakeGoalInvocation(id)); + const r = await executeAutofixes([])(fakeGoalInvocation(id, { + projectLoader: pl, + repoRefResolver: FakeRepoRefResolver, + } as any)); assert.equal(r.code, 0); }); @@ -130,9 +135,10 @@ describe("executeAutofixes", () => { const f = new InMemoryFile("src/main/java/Thing.java", initialContent); const p = InMemoryProject.from(id, f); const pl = new SingleProjectLoader(p); - const r = await executeAutofixes(pl, - [AddThingAutofix], - FakeRepoRefResolver)(fakeGoalInvocation(id)); + const r = await executeAutofixes([AddThingAutofix])(fakeGoalInvocation(id, { + projectLoader: pl, + repoRefResolver: FakeRepoRefResolver, + } as any)); assert.equal(r.code, 0); assert.equal(p.findFileSync(f.path).getContentSync(), initialContent); }); @@ -145,9 +151,10 @@ describe("executeAutofixes", () => { (p as any as GitProject).revert = async () => null; (p as any as GitProject).gitStatus = async () => ({ isClean: false } as any); const pl = new SingleProjectLoader(p); - const r = await executeAutofixes(pl, - [AddThingAutofix], - FakeRepoRefResolver)(fakeGoalInvocation(id)); + const r = await executeAutofixes([AddThingAutofix])(fakeGoalInvocation(id, { + projectLoader: pl, + repoRefResolver: FakeRepoRefResolver, + } as any)); assert.equal(r.code, 0); assert(!!p); const foundFile = p.findFileSync("thing"); @@ -163,9 +170,10 @@ describe("executeAutofixes", () => { (p as any as GitProject).revert = async () => null; (p as any as GitProject).gitStatus = async () => ({ isClean: false } as any); const pl = new SingleProjectLoader(p); - const r = await executeAutofixes(pl, - [AddThingWithParamAutofix], - FakeRepoRefResolver)(fakeGoalInvocation(id)); + const r = await executeAutofixes([AddThingWithParamAutofix])(fakeGoalInvocation(id, { + projectLoader: pl, + repoRefResolver: FakeRepoRefResolver, + } as any)); assert.equal(r.code, 0); assert(!!p); const foundFile = p.findFileSync("bird"); @@ -190,9 +198,11 @@ describe("executeAutofixes", () => { }], }; - const filterAutofixes = filterImmediateAutofixes([autofix], { sdmGoal: { + const filterAutofixes = filterImmediateAutofixes([autofix], { + sdmGoal: { push, - } } as any as GoalInvocation); + }, + } as any as GoalInvocation); assert.strictEqual(filterAutofixes.length, 0); }); @@ -216,9 +226,11 @@ describe("executeAutofixes", () => { }], }; - const filterAutofixes = filterImmediateAutofixes([autofix1, autofix2], { sdmGoal: { + const filterAutofixes = filterImmediateAutofixes([autofix1, autofix2], { + sdmGoal: { push, - } } as any as GoalInvocation); + }, + } as any as GoalInvocation); assert.strictEqual(filterAutofixes.length, 1); assert.strictEqual(filterAutofixes[0].name, autofix2.name); diff --git a/test/api-helper/listener/executePushReactions.test.ts b/test/api-helper/listener/executePushReactions.test.ts index 691f1056f..5d8ca7c53 100644 --- a/test/api-helper/listener/executePushReactions.test.ts +++ b/test/api-helper/listener/executePushReactions.test.ts @@ -16,14 +16,13 @@ import { GitHubRepoRef } from "@atomist/automation-client/operations/common/GitHubRepoRef"; import { InMemoryProject } from "@atomist/automation-client/project/mem/InMemoryProject"; -import { TruePushTest } from "../../api/mapping/support/pushTestUtils.test"; - import * as assert from "power-assert"; import { executePushReactions } from "../../../src/api-helper/listener/executePushReactions"; import { fakeGoalInvocation } from "../../../src/api-helper/test/fakeGoalInvocation"; import { SingleProjectLoader } from "../../../src/api-helper/test/SingleProjectLoader"; import { PushListenerInvocation } from "../../../src/api/listener/PushListener"; import { PushImpactListenerRegistration, PushReactionResponse } from "../../../src/api/registration/PushImpactListenerRegistration"; +import { TruePushTest } from "../../api/mapping/support/pushTestUtils.test"; function react(invocations: PushListenerInvocation[], stopTheWorld: boolean): PushImpactListenerRegistration { return { @@ -44,8 +43,10 @@ describe("executePushReactions", () => { const id = new GitHubRepoRef("a", "b"); const p = InMemoryProject.from(id); const invocations: PushListenerInvocation[] = []; - const ge = executePushReactions(new SingleProjectLoader(p), [react(invocations, true)]); - const r = await ge(fakeGoalInvocation(id)); + const ge = executePushReactions([react(invocations, true)]); + const r = await ge(fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any)); assert.equal(invocations.length, 1); assert(!r.requireApproval); assert.equal(r.code, 1); @@ -55,8 +56,10 @@ describe("executePushReactions", () => { const id = new GitHubRepoRef("a", "b"); const p = InMemoryProject.from(id); const invocations: PushListenerInvocation[] = []; - const ge = executePushReactions(new SingleProjectLoader(p), [react(invocations, false)]); - const r = await ge(fakeGoalInvocation(id)); + const ge = executePushReactions([react(invocations, false)]); + const r = await ge(fakeGoalInvocation(id, { + projectLoader: new SingleProjectLoader(p), + } as any)); assert.equal(invocations.length, 1); assert.equal(r.code, 0); assert(!r.requireApproval);