@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
300 lines (278 loc) • 13.6 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 { HandleCommand } from "@atomist/automation-client/lib/HandleCommand";
import { HandlerContext } from "@atomist/automation-client/lib/HandlerContext";
import {
RedirectResult,
Success,
} from "@atomist/automation-client/lib/HandlerResult";
import {
commandHandlerFrom,
OnCommand,
} from "@atomist/automation-client/lib/onCommand";
import { CommandDetails } from "@atomist/automation-client/lib/operations/CommandDetails";
import { ProjectAction } from "@atomist/automation-client/lib/operations/common/projectAction";
import {
isRemoteRepoRef,
ProviderType,
RemoteRepoRef,
RepoRef,
} from "@atomist/automation-client/lib/operations/common/RepoId";
import { RepoLoader } from "@atomist/automation-client/lib/operations/common/repoLoader";
import { AnyProjectEditor } from "@atomist/automation-client/lib/operations/edit/projectEditor";
import {
generate,
ProjectPersister,
} from "@atomist/automation-client/lib/operations/generate/generatorUtils";
import { RepoCreationParameters } from "@atomist/automation-client/lib/operations/generate/RepoCreationParameters";
import { SeedDrivenGeneratorParameters } from "@atomist/automation-client/lib/operations/generate/SeedDrivenGeneratorParameters";
import {
isProject,
Project,
} from "@atomist/automation-client/lib/project/Project";
import { addressEvent } from "@atomist/automation-client/lib/spi/message/MessageClient";
import {
Maker,
toFactory,
} from "@atomist/automation-client/lib/util/constructionUtils";
import {
bold,
codeBlock,
url,
} from "@atomist/slack-messages";
import { SoftwareDeliveryMachineOptions } from "../../../api/machine/SoftwareDeliveryMachineOptions";
import { CommandRegistration } from "../../../api/registration/CommandRegistration";
import {
GeneratorRegistration,
StartingPoint,
} from "../../../api/registration/GeneratorRegistration";
import { constructProvenance } from "../../goal/storeGoals";
import {
CommandListenerExecutionInterruptError,
resolveCredentialsPromise,
toCommandListenerInvocation,
} from "../../machine/handlerRegistrations";
import { projectLoaderRepoLoader } from "../../machine/projectLoaderRepoLoader";
import {
MachineOrMachineOptions,
toMachineOptions,
} from "../../machine/toMachineOptions";
import {
slackErrorMessage,
slackInfoMessage,
slackSuccessMessage,
} from "../../misc/slack/messages";
import { CachingProjectLoader } from "../../project/CachingProjectLoader";
/**
* Create a command handler for project generation
* @param sdm this machine or its options
* @param {EditorFactory<P extends SeedDrivenGeneratorParameters>} editorFactory to create editorCommand to perform transformation
* @param {Maker<P extends SeedDrivenGeneratorParameters>} paramsMaker
* @param {string} name
* @param {Partial<GeneratorCommandDetails<P extends SeedDrivenGeneratorParameters>>} details
* @return {HandleCommand}
*/
export function generatorCommand<P>(sdm: MachineOrMachineOptions,
editorFactory: EditorFactory<P>,
name: string,
paramsMaker: Maker<P>,
fallbackTarget: Maker<RepoCreationParameters>,
startingPoint: StartingPoint<P>,
details: Partial<GeneratorCommandDetails<any>> = {},
cr: GeneratorRegistration<P>): HandleCommand {
const detailsToUse: GeneratorCommandDetails<any> = {
...defaultDetails(toMachineOptions(sdm), name),
...details,
};
return commandHandlerFrom(handleGenerate(editorFactory, detailsToUse, startingPoint, cr, toMachineOptions(sdm)),
toGeneratorParametersMaker<P>(
paramsMaker,
toFactory(fallbackTarget)()),
name,
detailsToUse.description, detailsToUse.intent, detailsToUse.tags);
}
export type EditorFactory<P> = (params: P, ctx: HandlerContext) => AnyProjectEditor<P>;
interface GeneratorCommandDetails<P extends SeedDrivenGeneratorParameters> extends CommandDetails {
redirecter: (r: RepoRef) => string;
projectPersister?: ProjectPersister;
afterAction?: ProjectAction<P>;
}
/**
* Return a parameters maker that is targeting aware
* @param {Maker<PARAMS>} paramsMaker
* @return {Maker<EditorOrReviewerParameters & PARAMS>}
*/
export function toGeneratorParametersMaker<PARAMS>(paramsMaker: Maker<PARAMS>,
target: RepoCreationParameters): Maker<SeedDrivenGeneratorParameters & PARAMS> {
const sampleParams = toFactory(paramsMaker)();
return isSeedDrivenGeneratorParameters(sampleParams) ?
paramsMaker as Maker<SeedDrivenGeneratorParameters & PARAMS> as any :
() => {
// This way we won't bother with source, but rely on startingPoint
const rawParms: PARAMS = toFactory(paramsMaker)();
const allParms = rawParms as SeedDrivenGeneratorParameters & PARAMS;
allParms.target = target;
return allParms;
};
}
export function isSeedDrivenGeneratorParameters(p: any): p is SeedDrivenGeneratorParameters {
const maybe = p as SeedDrivenGeneratorParameters;
return !!maybe && !!maybe.target;
}
function handleGenerate<P extends SeedDrivenGeneratorParameters>(editorFactory: EditorFactory<P>,
details: GeneratorCommandDetails<P>,
startingPoint: StartingPoint<P>,
cr: GeneratorRegistration<P>,
sdmo: SoftwareDeliveryMachineOptions): OnCommand<P> {
return (ctx: HandlerContext, parameters: P) => {
return handle(ctx, editorFactory, parameters, details, startingPoint, cr, sdmo);
};
}
async function handle<P extends SeedDrivenGeneratorParameters>(ctx: HandlerContext,
editorFactory: EditorFactory<P>,
params: P,
details: GeneratorCommandDetails<P>,
startingPoint: StartingPoint<P>,
cr: GeneratorRegistration<P>,
sdmo: SoftwareDeliveryMachineOptions): Promise<RedirectResult> {
try {
const pi = {
...toCommandListenerInvocation(cr, ctx, params, sdmo),
...params,
} as any;
pi.credentials = await resolveCredentialsPromise(pi.credentials);
const r = await generate(
computeStartingPoint(params, ctx, details.repoLoader(params), details, startingPoint, cr, sdmo),
ctx,
pi.credentials,
editorFactory(params, ctx),
details.projectPersister,
params.target.repoRef,
params,
undefined, // set to undefined as we run the afterActions below explicitly
);
if (!!cr.afterAction && r.success === true) {
const afterActions = Array.isArray(cr.afterAction) ? cr.afterAction : [cr.afterAction];
for (const afterAction of afterActions) {
await afterAction(r.target, pi);
}
}
// TODO cd support other providers which needs to start upstream from this
if (params.target.repoRef.providerType === ProviderType.github_com && r.success === true) {
const repoProvenance = {
repo: {
name: params.target.repoRef.repo,
owner: params.target.repoRef.owner,
providerId: "zjlmxjzwhurspem",
},
provenance: constructProvenance(ctx),
};
await ctx.messageClient.send(repoProvenance, addressEvent("SdmRepoProvenance"));
}
await ctx.messageClient.respond(
slackSuccessMessage(
`Create Project`,
`Successfully created new project ${bold(`${params.target.repoRef.owner}/${
params.target.repoRef.repo}`)} at ${url(params.target.repoRef.url)}`));
return {
code: 0,
// Redirect to local project page
redirect: details.redirecter(params.target.repoRef),
// local SDM uses this to print instructions
generatedRepositoryUrl: params.target.repoRef.url,
} as any;
} catch (err) {
if (err instanceof CommandListenerExecutionInterruptError) {
// We're continuing
return Success as any;
}
await ctx.messageClient.respond(
slackErrorMessage(
`Create Project`,
`Project creation for ${bold(`${params.target.repoRef.owner}/${params.target.repoRef.repo}`)} failed:
${codeBlock(err.message)}`,
ctx));
}
return undefined;
}
/**
* Retrieve a seed. Set the seed location on the parameters if possible and necessary.
*/
export async function computeStartingPoint<P extends SeedDrivenGeneratorParameters>(params: P,
ctx: HandlerContext,
repoLoader: RepoLoader,
details: GeneratorCommandDetails<any>,
startingPoint: StartingPoint<P>,
cr: CommandRegistration<P>,
sdmo: SoftwareDeliveryMachineOptions): Promise<Project> {
if (!startingPoint) {
if (!params.source || !params.source.repoRef) {
throw new Error("If startingPoint is not provided in GeneratorRegistration, parameters.source must specify seed project location: " +
`Offending registration had intent ${details.intent}`);
}
await infoMessage(`Cloning seed project from parameters ${url(params.source.repoRef.url)}`, ctx);
return repoLoader(params.source.repoRef);
}
if (isProject(startingPoint)) {
await infoMessage(`Using starting point project specified in registration`, ctx);
return startingPoint;
} else if (isRemoteRepoRef(startingPoint as RepoRef)) {
const source = startingPoint as RemoteRepoRef;
await infoMessage(`Cloning seed project from starting point ${bold(`${source.owner}/${source.repo}`)} at ${url(source.url)}`, ctx);
const repoRef = startingPoint as RemoteRepoRef;
params.source = { repoRef };
return repoLoader(repoRef);
} else {
// Combine this for backward compatibility
const pi = {
...toCommandListenerInvocation(cr, ctx, params, sdmo),
...params,
};
pi.credentials = await resolveCredentialsPromise(pi.credentials);
// It's a function that takes the parameters and returns either a project or a RemoteRepoRef
const rr: RemoteRepoRef | Project | Promise<Project> = (startingPoint as any)(pi);
if (isProjectPromise(rr)) {
const p = await rr;
await infoMessage(`Using dynamically chosen starting point project ${bold(`${p.id.owner}:${p.id.repo}`)}`, ctx);
return p;
}
if (isProject(rr)) {
await infoMessage(`Using dynamically chosen starting point project ${bold(`${rr.id.owner}:${rr.id.repo}`)}`, ctx);
// params.source will remain undefined in this case
return rr;
} else {
await infoMessage(`Cloning dynamically chosen starting point from ${url(rr.url)}`, ctx);
params.source = { repoRef: rr };
return repoLoader(rr);
}
}
}
function isProjectPromise(a: any): a is Promise<Project> {
return !!a.then;
}
function defaultDetails<P extends SeedDrivenGeneratorParameters>(opts: SoftwareDeliveryMachineOptions, name: string): GeneratorCommandDetails<P> {
return {
description: name,
repoFinder: opts.repoFinder,
repoLoader: (p: P) => projectLoaderRepoLoader(opts.projectLoader || new CachingProjectLoader(),
p.target.credentials, true),
projectPersister: opts.projectPersister,
redirecter: () => undefined,
};
}
async function infoMessage(text: string, ctx: HandlerContext): Promise<void> {
return ctx.messageClient.respond(slackInfoMessage("Create Project", text));
}