@flit/cdk-pipeline
Version:
A highly customizable and extensible CI/CD pipeline intended as alternative to CDK's native CodePipeline
148 lines (133 loc) • 3.69 kB
text/typescript
import { App, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import {
Pipeline as AwsPipeline,
IAction,
PipelineType,
} from "aws-cdk-lib/aws-codepipeline";
import * as path from "path";
import { isSegment, Segment, SegmentConstructed } from "./segment";
import { isSource } from "./source-segment";
import { isPipeline } from "./pipeline-segment";
export interface SegmentGroup {
readonly stageName?: string;
readonly segments: Segment[];
}
export interface PipelineProps extends StackProps {
/**
* The name of the generated pipeline.
* @default Stack ID
*/
readonly pipelineName?: string;
/**
* The path to the cdk projects root directory containing the cdk.json file
* relative to the asset root
* @default "."
*/
readonly rootDir?: string;
/**
* The segments to populating the pipeline.
*/
readonly segments: (Segment | Segment[] | SegmentGroup)[];
}
/**
* @category Constructs
*/
export class Pipeline extends Stack {
readonly pipelineName: string;
readonly rootDir: string;
readonly buildDir: string;
constructor(
scope: Construct,
id: string,
readonly props: PipelineProps,
) {
super(scope, id, props);
this.pipelineName = props.pipelineName || id;
this.rootDir = props.rootDir || ".";
this.buildDir = path.join(this.rootDir, (this.node.root as App).outdir);
if (!this.bundlingRequired) return;
const stages = props.segments.map((segment) =>
Array.isArray(segment)
? { stageName: undefined, segments: segment }
: isSegment(segment)
? { stageName: undefined, segments: [segment] }
: segment,
);
stages.forEach((stage) => {
stage.segments.forEach((segment) => {
segment.inputs.forEach((artifact) => {
if (!artifact.producer) {
throw new Error("Artifact consumed but never produced.");
}
});
});
});
if (
stages[0].segments.filter(isSource).length !== stages[0].segments.length
) {
throw new Error("First segment must contain only source segments");
}
if (
stages.slice(1).find((stage) => stage.segments.filter(isSource).length)
) {
throw new Error("Only the first segment can contain source segments");
}
if (
stages[1].segments.length !== 1 ||
stages[1].segments.filter(isPipeline).length !== stages[1].segments.length
) {
throw new Error("Second segment must be the pipeline segment");
}
if (
stages.slice(2).find((stage) => stage.segments.filter(isPipeline).length)
) {
throw new Error("Only the second segment can be the pipeline segment");
}
new AwsPipeline(this, "Pipeline", {
pipelineName: props.pipelineName,
restartExecutionOnUpdate: true,
pipelineType: PipelineType.V2,
stages: [
{
stageName: stages[0].stageName || "Source",
actions: [
...stages[0].segments.reduce(
(actions, segment) => [
...actions,
...segment.construct(this).actions,
],
[] as IAction[],
),
],
},
{
stageName: stages[1].stageName || "Pipeline",
actions: [
...stages[1].segments.reduce(
(actions, segment) => [
...actions,
...segment.construct(this).actions,
],
[] as IAction[],
),
],
},
...stages.slice(2).map((stage) => {
const builds = stage.segments.reduce(
(segments, segment) => [...segments, segment.construct(this)],
[] as SegmentConstructed[],
);
return {
stageName:
stage.stageName || builds.map((build) => build.name).join("-"),
actions: builds.reduce(
(actions, build) => [...actions, ...build.actions],
[] as IAction[],
),
};
}),
],
});
}
}