UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

345 lines (321 loc) 12.8 kB
/* * Copyright © 2020 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 { AutomationContextAware, HandlerContext, } from "@atomist/automation-client/lib/HandlerContext"; import { RemoteRepoRef } from "@atomist/automation-client/lib/operations/common/RepoId"; import { MutationNoCacheOptions } from "@atomist/automation-client/lib/spi/graph/GraphClient"; import * as _ from "lodash"; import * as omitEmpty from "omit-empty"; import { sprintf } from "sprintf-js"; import { Goal, hasPreconditions, } from "../../api/goal/Goal"; import { Parameterized } from "../../api/goal/GoalWithFulfillment"; import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; import { SdmGoalFulfillment, SdmGoalFulfillmentMethod, SdmGoalKey, SdmGoalMessage, SdmProvenance, } from "../../api/goal/SdmGoalMessage"; import { SdmGoalSetMessage } from "../../api/goal/SdmGoalSetMessage"; import { GoalImplementation } from "../../api/goal/support/GoalImplementationMapper"; import { GoalSetTag } from "../../api/goal/tagGoalSet"; import { OnPushToAnyBranch, PushFields, SdmGoalState, UpdateSdmGoalMutation, UpdateSdmGoalMutationVariables, UpdateSdmGoalSetMutation, UpdateSdmGoalSetMutationVariables, } from "../../typings/types"; export function environmentFromGoal(goal: Goal): string { return goal.definition.environment.replace(/\/$/, ""); // remove trailing slash at least } export interface UpdateSdmGoalParams { state: SdmGoalState; description: string; url?: string; externalUrls?: Array<{ label?: string, url: string }>; approved?: boolean; error?: Error; data?: string; phase?: string; } export async function updateGoal(ctx: HandlerContext, before: SdmGoalEvent, params: UpdateSdmGoalParams): Promise<void> { const description = params.description; const approval = params.approved ? constructProvenance(ctx) : !!before ? before.approval : undefined; const data = params.data ? params.data : !!before ? before.data : undefined; before.version = (before.version || 1) + 1; const sdmGoal = { ...eventToMessage(before), state: params.state === "success" && !!before && before.approvalRequired ? SdmGoalState.waiting_for_approval : params.state, phase: params.phase, description, url: params.url ? params.url : before.url, externalUrls: params.externalUrls ? params.externalUrls : before.externalUrls, approval, ts: Date.now(), provenance: [constructProvenance(ctx)].concat(!!before ? before.provenance : []), error: _.get(params, "error.message"), data, push: cleanPush(before.push), version: before.version, }; await storeGoal(ctx, sdmGoal); } function eventToMessage(event: SdmGoalEvent): SdmGoalMessage { return { ...event, repo: { name: event.repo.name, owner: event.repo.owner, providerId: event.repo.providerId, }, id: undefined, } as any; } export function goalCorrespondsToSdmGoal(goal: Goal, sdmGoal: SdmGoalKey): boolean { return goal.name === sdmGoal.name && environmentFromGoal(goal) === sdmGoal.environment; } export function constructSdmGoalImplementation(gi: GoalImplementation, registration: string): SdmGoalFulfillment { return { method: SdmGoalFulfillmentMethod.Sdm, name: gi.implementationName, registration, }; } export function constructSdmGoal(ctx: HandlerContext, parameters: { goalSet: string, goalSetId: string, goal: Goal, state: SdmGoalState, id: RemoteRepoRef, providerId: string url?: string, fulfillment?: SdmGoalFulfillment, }): SdmGoalMessage { const { goalSet, goal, goalSetId, state, id, providerId, url } = parameters; const fulfillment = parameters.fulfillment || { method: SdmGoalFulfillmentMethod.Other, name: "unknown", registration: "unknown", }; if (!id.branch) { throw new Error(sprintf("Please provide a branch in the RemoteRepoRef %j", parameters)); } if (!id.sha) { throw new Error(sprintf("Please provide a sha in the RemoteRepoRef %j", parameters)); } const preConditions: SdmGoalKey[] = []; const description = descriptionFromState(goal, state); const environment = environmentFromGoal(goal); if (hasPreconditions(goal)) { preConditions.push(...goal.dependsOn.map(d => ({ goalSet, name: d.name, uniqueName: d.uniqueName, environment: environmentFromGoal(d), }))); } const retryFeasible = goal.definition.retryFeasible ? goal.definition.retryFeasible : false; return { goalSet, registration: (ctx as any as AutomationContextAware).context.name, goalSetId, name: goal.name, uniqueName: goal.uniqueName, environment, fulfillment, sha: id.sha, branch: id.branch, repo: { name: id.repo, owner: id.owner, providerId, }, state, description, descriptions: { planned: goal.plannedDescription, requested: goal.requestedDescription, inProcess: goal.inProcessDescription, completed: goal.successDescription, failed: goal.failureDescription, canceled: goal.canceledDescription, stopped: goal.stoppedDescription, skipped: goal.skippedDescription, waitingForApproval: goal.waitingForApprovalDescription, waitingForPreApproval: goal.waitingForPreApprovalDescription, }, url, externalKey: goal.context, ts: Date.now(), approvalRequired: goal.definition.approvalRequired ? goal.definition.approvalRequired : false, preApprovalRequired: goal.definition.preApprovalRequired ? goal.definition.preApprovalRequired : false, retryFeasible, provenance: [constructProvenance(ctx)], preConditions, parameters: !!(goal.definition as Parameterized).parameters ? JSON.stringify((goal.definition as Parameterized).parameters) : undefined, version: 1, }; } export async function storeGoal(ctx: HandlerContext, sdmGoal: SdmGoalMessage): Promise<SdmGoalMessage> { const newGoal = omitEmpty(sdmGoal, { omitZero: false }); delete (newGoal).push; await ctx.graphClient.mutate<UpdateSdmGoalMutation, UpdateSdmGoalMutationVariables>({ name: "UpdateSdmGoal", variables: { goal: newGoal, }, options: MutationNoCacheOptions, }); return sdmGoal; } export function constructProvenance(ctx: HandlerContext): SdmProvenance { return { registration: (ctx as any as AutomationContextAware).context.name, version: (ctx as any as AutomationContextAware).context.version, name: (ctx as any as AutomationContextAware).context.operation, correlationId: ctx.correlationId, ts: Date.now(), }; } export function descriptionFromState(goal: Goal, state: SdmGoalState, goalEvent?: SdmGoalEvent): string { switch (state) { case SdmGoalState.planned: return _.get(goalEvent, "descriptions.planned", goal.plannedDescription); case SdmGoalState.requested: return _.get(goalEvent, "descriptions.requested", goal.requestedDescription); case SdmGoalState.in_process: return _.get(goalEvent, "descriptions.inProcess", goal.inProcessDescription); case SdmGoalState.waiting_for_approval: return _.get(goalEvent, "descriptions.waitingForApproval", goal.waitingForApprovalDescription); case SdmGoalState.waiting_for_pre_approval: return _.get(goalEvent, "descriptions.waitingForPreApproval", goal.waitingForPreApprovalDescription); case SdmGoalState.success: return _.get(goalEvent, "descriptions.completed", goal.successDescription); case SdmGoalState.failure: return _.get(goalEvent, "descriptions.failed", goal.failureDescription); case SdmGoalState.skipped: return _.get(goalEvent, "descriptions.skipped", goal.skippedDescription); case SdmGoalState.canceled: return _.get(goalEvent, "descriptions.canceled", goal.canceledDescription); case SdmGoalState.stopped: return _.get(goalEvent, "descriptions.stopped", goal.stoppedDescription); default: throw new Error("Unknown goal state " + state); } } export function constructGoalSet(ctx: HandlerContext, goalSetId: string, goalSet: string, sdmGoals: SdmGoalMessage[], tags: GoalSetTag[], push: OnPushToAnyBranch.Push): SdmGoalSetMessage { let repo; if (!!push) { repo = { name: push.repo.name, owner: push.repo.owner, providerId: push.repo.org.provider.providerId, }; } else if (!!sdmGoals && sdmGoals.length > 0) { const goal = sdmGoals.find(g => !!g.repo); if (!!goal) { repo = { name: goal.repo.name, owner: goal.repo.owner, providerId: goal.repo.providerId, }; } } const sdmGoalSet: SdmGoalSetMessage = { sha: push.after.sha, branch: push.branch, goalSetId, goalSet, ts: Date.now(), repo, state: goalSetState(sdmGoals), goals: sdmGoals.map(g => ({ name: g.name, uniqueName: g.uniqueName, })), provenance: constructProvenance(ctx), tags, }; return sdmGoalSet; } export async function storeGoalSet(ctx: HandlerContext, goalSet: SdmGoalSetMessage): Promise<void> { await ctx.graphClient.mutate<UpdateSdmGoalSetMutation, UpdateSdmGoalSetMutationVariables>({ name: "UpdateSdmGoalSet", variables: { goalSet: omitEmpty(goalSet, { omitZero: false }), }, options: MutationNoCacheOptions, }); } export function goalSetState(goals: Array<Pick<SdmGoalMessage, "name" | "state">>): SdmGoalState { if (goals.some(g => g.state === SdmGoalState.failure)) { return SdmGoalState.failure; } else if (goals.some(g => g.state === SdmGoalState.canceled)) { return SdmGoalState.canceled; } else if (goals.some(g => g.state === SdmGoalState.stopped)) { return SdmGoalState.stopped; } else if (goals.some(g => g.state === SdmGoalState.in_process)) { return SdmGoalState.in_process; } else if (goals.some(g => g.state === SdmGoalState.waiting_for_pre_approval)) { return SdmGoalState.waiting_for_pre_approval; } else if (goals.some(g => g.state === SdmGoalState.waiting_for_approval)) { return SdmGoalState.waiting_for_approval; } else if (goals.some(g => g.state === SdmGoalState.pre_approved)) { return SdmGoalState.pre_approved; } else if (goals.some(g => g.state === SdmGoalState.approved)) { return SdmGoalState.approved; } else if (goals.some(g => g.state === SdmGoalState.requested)) { return SdmGoalState.requested; } else if (goals.some(g => g.state === SdmGoalState.planned)) { return SdmGoalState.planned; } else if (goals.some(g => g.state === SdmGoalState.skipped)) { return SdmGoalState.skipped; } else if (goals.every(g => g.state === SdmGoalState.success)) { return SdmGoalState.success; } else { const unknowns = goals.filter(g => g.state !== SdmGoalState.success).map(g => `${g.name}:${g.state}`); throw new Error("Unknown goal state(s): " + JSON.stringify(unknowns)); } } function cleanPush(push: PushFields.Fragment): PushFields.Fragment { const newPush = _.cloneDeep(push); if (!!newPush && !!(newPush as any).goals) { delete (newPush as any).goals; } return newPush; }