CI/CD - CodeBuild + CodeDeploy + CodePipeline

This commit is contained in:
Zakaria
2026-10-01 09:18:04 -04:00
parent aa02cdb7f6
commit 7f0d707578
41 changed files with 1817 additions and 78 deletions
@@ -0,0 +1,108 @@
import {
CreatePipelineCommand,
DeletePipelineCommand,
GetPipelineCommand,
GetPipelineStateCommand,
ListPipelinesCommand,
ListPipelineExecutionsCommand,
StartPipelineExecutionCommand,
type ActionState,
type CodePipelineClient,
type PipelineDeclaration,
} from '@aws-sdk/client-codepipeline'
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
import {awsCodePipelineSchema} from '../cloud-spi/codepipelineSchema'
import type {PipelineExecutionSummary, PipelineState} from '../cloud-spi/codepipeline'
import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
export class AwsCodePipelineAdapter implements CloudServiceAdapter {
readonly cloud = 'aws' as const
readonly service = 'codepipeline' as const
constructor(private readonly client: CodePipelineClient) {}
schema(): ServiceSchema { return awsCodePipelineSchema() }
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
const pipelines: CloudResource[] = []
let nextToken: string | undefined
do {
const response = await this.client.send(new ListPipelinesCommand(nextToken ? {nextToken} : {}))
pipelines.push(...(response.pipelines ?? []).map((pipeline) => toSummaryResource(pipeline)))
nextToken = response.nextToken
} while (nextToken)
const search = query.search?.trim().toLowerCase()
return pipelines.filter((pipeline) => !search || pipeline.name.toLowerCase().includes(search))
}
async get(id: string): Promise<CloudResource | null> {
const response = await this.client.send(new GetPipelineCommand({name: id}))
return response.pipeline ? toResource(response.pipeline, response.metadata) : null
}
async create(input: CreateResourceInput): Promise<CloudResource> {
const name = required(input.values.name, 'name')
const declaration: PipelineDeclaration = {
name,
roleArn: required(input.values.roleArn, 'roleArn'),
artifactStore: {type: 'S3', location: required(input.values.artifactBucket, 'artifactBucket')},
stages: [
stage('Source', action('Source', 'Source', 'S3', {S3Bucket: required(input.values.sourceBucket, 'sourceBucket'), S3ObjectKey: required(input.values.sourceKey, 'sourceKey'), PollForSourceChanges: 'false'}, [], ['SourceOutput'])),
stage('Build', action('Build', 'Build', 'CodeBuild', {ProjectName: required(input.values.codeBuildProject, 'codeBuildProject')}, ['SourceOutput'], ['BuildOutput'])),
stage('Deploy', action('Deploy', 'Deploy', 'S3', {BucketName: required(input.values.deployBucket, 'deployBucket'), ObjectKey: required(input.values.deployKey, 'deployKey'), Extract: 'true'}, ['BuildOutput'], [])),
],
}
const response = await this.client.send(new CreatePipelineCommand({pipeline: declaration}))
if (!response.pipeline) throw new RuntimeError(`CodePipeline did not return pipeline ${name}`)
return toResource(response.pipeline)
}
async delete(id: string): Promise<void> { await this.client.send(new DeletePipelineCommand({name: id})) }
async startPipeline(id: string): Promise<{id: string}> {
const response = await this.client.send(new StartPipelineExecutionCommand({name: id}))
if (!response.pipelineExecutionId) throw new RuntimeError(`CodePipeline did not return an execution for ${id}`)
return {id: response.pipelineExecutionId}
}
async listPipelineExecutions(id: string): Promise<PipelineExecutionSummary[]> {
const executions: PipelineExecutionSummary[] = []
let nextToken: string | undefined
do {
const response = await this.client.send(new ListPipelineExecutionsCommand(nextToken ? {pipelineName: id, nextToken} : {pipelineName: id}))
for (const execution of response.pipelineExecutionSummaries ?? []) {
if (execution.pipelineExecutionId) executions.push({id: execution.pipelineExecutionId, status: execution.status, statusSummary: execution.statusSummary, startTime: execution.startTime, lastUpdateTime: execution.lastUpdateTime})
}
nextToken = response.nextToken
} while (nextToken)
return executions
}
async getPipelineState(id: string): Promise<PipelineState> {
const [pipeline, state] = await Promise.all([this.client.send(new GetPipelineCommand({name: id})), this.client.send(new GetPipelineStateCommand({name: id}))])
const actions = (pipeline.pipeline?.stages ?? []).flatMap((stage) => stage.actions ?? [])
const codeBuildProjects = new Map(actions.filter((item) => item.actionTypeId?.provider === 'CodeBuild').map((item) => [item.name, item.configuration?.ProjectName] as const))
const codeDeployActions = new Set(actions.filter((item) => item.actionTypeId?.provider === 'CodeDeploy').map((item) => item.name))
return {stages: (state.stageStates ?? []).map((stage) => ({name: stage.stageName ?? 'Unknown', status: stage.latestExecution?.status, actions: (stage.actionStates ?? []).map((actionState) => toActionState(actionState, codeBuildProjects, codeDeployActions))}))}
}
}
function toResource(pipeline: PipelineDeclaration, metadata?: {pipelineArn?: string; created?: Date; updated?: Date}): CloudResource {
const name = pipeline.name ?? ''
return {id: name, name, cloud: 'aws', service: 'codepipeline', type: 'codepipeline-pipeline', region: regionFromArn(metadata?.pipelineArn), createdAt: metadata?.created?.toISOString() ?? null, version: pipeline.version?.toString(), metadata: {arn: metadata?.pipelineArn, roleArn: pipeline.roleArn, executionMode: pipeline.executionMode, pipelineType: pipeline.pipelineType, artifactBucket: pipeline.artifactStore?.location, updatedAt: metadata?.updated?.toISOString()}}
}
function toSummaryResource(pipeline: {name?: string; version?: number; created?: Date; updated?: Date; executionMode?: string; pipelineType?: string}): CloudResource {
const name = pipeline.name ?? ''
return {id: name, name, cloud: 'aws', service: 'codepipeline', type: 'codepipeline-pipeline', region: null, createdAt: pipeline.created?.toISOString() ?? null, version: pipeline.version?.toString(), metadata: {executionMode: pipeline.executionMode, pipelineType: pipeline.pipelineType, updatedAt: pipeline.updated?.toISOString()}}
}
function toActionState(action: ActionState, codeBuildProjects: Map<string | undefined, string | undefined>, codeDeployActions: Set<string | undefined>) {
const execution = action.latestExecution
return {name: action.actionName ?? 'Unknown', status: execution?.status, summary: execution?.summary, lastStatusChange: execution?.lastStatusChange, externalExecutionId: execution?.externalExecutionId, externalExecutionUrl: execution?.externalExecutionUrl, entityUrl: action.entityUrl, revisionUrl: action.revisionUrl, codeBuildProject: codeBuildProjects.get(action.actionName), codeDeployDeploymentId: codeDeployActions.has(action.actionName) ? execution?.externalExecutionId : undefined}
}
function stage(name: string, item: ReturnType<typeof action>) { return {name, actions: [item]} }
function action(name: string, category: 'Source' | 'Build' | 'Deploy', provider: 'S3' | 'CodeBuild', configuration: Record<string, string>, inputArtifacts: string[], outputArtifacts: string[]) { return {name, actionTypeId: {category, owner: 'AWS' as const, provider, version: '1'}, configuration, inputArtifacts: inputArtifacts.map((name) => ({name})), outputArtifacts: outputArtifacts.map((name) => ({name}))} }
function required(value: unknown, field: string): string { if (typeof value !== 'string' || !value.trim()) throw new ValidationError(`${field} is required`); return value.trim() }
function regionFromArn(arn?: string): string | null { return arn?.split(':')[3] || null }