@atomist/sdm-core
Version:
Atomist Software Delivery Machine - Implementation
172 lines (159 loc) • 7.41 kB
text/typescript
/*
* Copyright © 2019 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 {
automationClientInstance,
EventFired,
GraphQL,
HandlerContext,
HandlerResult,
logger,
Success,
Value,
} from "@atomist/automation-client";
import { EventHandler } from "@atomist/automation-client/lib/decorators";
import { HandleEvent } from "@atomist/automation-client/lib/HandleEvent";
import {
addressChannelsFor,
CredentialsResolver,
GoalApprovalRequestVote,
GoalApprovalRequestVoteDecisionManager,
GoalApprovalRequestVoter,
GoalApprovalRequestVoterInvocation,
GoalImplementationMapper,
PreferenceStoreFactory,
RepoRefResolver,
resolveCredentialsPromise,
SdmGoalEvent,
SdmGoalState,
SoftwareDeliveryMachineConfiguration,
UnanimousGoalApprovalRequestVoteDecisionManager,
updateGoal,
} from "@atomist/sdm";
import { shouldHandle } from "../../../../internal/delivery/goals/support/validateGoal";
import { verifyGoal } from "../../../../internal/signing/goalSigning";
import { OnAnyApprovedSdmGoal } from "../../../../typings/types";
/**
* Vote on approved goals.
*
* This allows GoalApprovalVoter instances to vote on the approved goal to decide
* if this approval request can be granted or not.
*
* The final decision if the request should be granted based on all votes is delegated to the
* configured instance of GoalApprovalRequestVoteDecisionManager.
*/
("Vote on started or approved goals",
() => GraphQL.subscription({
name: "OnAnyApprovedSdmGoal",
variables: { registration: () => [automationClientInstance()?.configuration?.name] },
}))
export class VoteOnGoalApprovalRequest implements HandleEvent<OnAnyApprovedSdmGoal.Subscription> {
("")
public configuration: SoftwareDeliveryMachineConfiguration;
constructor(private readonly repoRefResolver: RepoRefResolver,
private readonly credentialsFactory: CredentialsResolver,
private readonly voters: GoalApprovalRequestVoter[],
private readonly decisionManager: GoalApprovalRequestVoteDecisionManager,
private readonly implementationMapper: GoalImplementationMapper,
private readonly preferenceStoreFactory: PreferenceStoreFactory) {
if (this.voters.length === 0) {
this.voters.push(async () => ({ vote: GoalApprovalRequestVote.Granted }));
}
if (!this.decisionManager) {
this.decisionManager = UnanimousGoalApprovalRequestVoteDecisionManager;
}
}
public async handle(event: EventFired<OnAnyApprovedSdmGoal.Subscription>,
context: HandlerContext): Promise<HandlerResult> {
const sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;
if (!shouldHandle(sdmGoal)) {
logger.debug(`Goal ${sdmGoal.name} skipped because not managed by this SDM`);
return Success;
}
await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);
const id = this.repoRefResolver.repoRefFromPush(sdmGoal.push);
const credentials = await resolveCredentialsPromise(this.credentialsFactory.eventHandlerCredentials(context, id));
const preferences = this.preferenceStoreFactory(context);
const garvi: GoalApprovalRequestVoterInvocation = {
id,
context,
credentials,
addressChannels: addressChannelsFor(sdmGoal.push.repo, context),
configuration: this.configuration,
preferences,
goal: sdmGoal,
};
const votes = await Promise.all(this.voters.map(v => v(garvi)));
const decision = this.decisionManager(...votes);
const goal = this.implementationMapper.findGoalBySdmGoal(sdmGoal);
switch (decision) {
case GoalApprovalRequestVote.Granted:
if (sdmGoal.state === SdmGoalState.pre_approved) {
let g = sdmGoal;
const cbs = this.implementationMapper.findFulfillmentCallbackForGoal(sdmGoal);
for (const cb of cbs) {
g = await cb.callback(g, {
id,
addressChannels: undefined,
preferences,
credentials,
configuration: this.configuration,
context,
});
}
await updateGoal(context, sdmGoal, {
state: SdmGoalState.requested,
description: !!sdmGoal.descriptions && !!sdmGoal.descriptions.requested
? sdmGoal.descriptions.requested : goal.requestedDescription,
data: g.data,
});
} else if (sdmGoal.state === SdmGoalState.approved) {
await updateGoal(context, sdmGoal, {
state: SdmGoalState.success,
description: !!sdmGoal.descriptions && !!sdmGoal.descriptions.completed
? sdmGoal.descriptions.completed : goal.successDescription,
});
}
break;
case GoalApprovalRequestVote.Denied:
if (sdmGoal.state === SdmGoalState.pre_approved) {
const g: SdmGoalEvent = {
...sdmGoal,
preApproval: undefined,
};
await updateGoal(context, g, {
state: SdmGoalState.waiting_for_pre_approval,
description: `${!!sdmGoal.descriptions && !!sdmGoal.descriptions.waitingForPreApproval ?
sdmGoal.descriptions.waitingForPreApproval : goal.waitingForPreApprovalDescription} \u00B7 start by @${sdmGoal.preApproval.userId} denied`,
});
} else if (sdmGoal.state === SdmGoalState.approved) {
const g: SdmGoalEvent = {
...sdmGoal,
approval: undefined,
};
await updateGoal(context, g, {
state: SdmGoalState.waiting_for_approval,
description: `${!!sdmGoal.descriptions && !!sdmGoal.descriptions.waitingForApproval ?
sdmGoal.descriptions.waitingForApproval : goal.waitingForApprovalDescription} \u00B7 approval by @${sdmGoal.approval.userId} denied`,
});
}
break;
case GoalApprovalRequestVote.Abstain:
// We don't do anything if vote isn't either granted or denied
break;
}
return Success;
}
}