in packages/aws-cdk-lib/pipelines/lib/codepipeline/private/codebuild-factory.ts [197:369]
public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult {
const projectOptions = mergeCodeBuildOptions(options.codeBuildDefaults, this.props.projectOptions);
if ((!projectOptions.buildEnvironment?.privileged || projectOptions.vpc === undefined) &&
(projectOptions.fileSystemLocations !== undefined && projectOptions.fileSystemLocations.length != 0)) {
throw new UnscopedValidationError('Setting fileSystemLocations requires a vpc to be set and privileged to be set to true.');
}
const inputs = this.props.inputs ?? [];
const outputs = this.props.outputs ?? [];
const mainInput = inputs.find(x => x.directory === '.');
const extraInputs = inputs.filter(x => x.directory !== '.');
const inputArtifact = mainInput
? options.artifacts.toCodePipeline(mainInput.fileSet)
: options.fallbackArtifact;
const extraInputArtifacts = extraInputs.map(x => options.artifacts.toCodePipeline(x.fileSet));
const outputArtifacts = outputs.map(x => options.artifacts.toCodePipeline(x.fileSet));
if (!inputArtifact) {
// This should actually never happen because CodeBuild projects shouldn't be added before the
// Source, which always produces at least an artifact.
throw new UnscopedValidationError(`CodeBuild action '${this.stepId}' requires an input (and the pipeline doesn't have a Source to fall back to). Add an input or a pipeline source.`);
}
const installCommands = [
...generateInputArtifactLinkCommands(options.artifacts, extraInputs),
...this.props.installCommands ?? [],
];
const buildSpecHere = codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
install: (installCommands.length ?? 0) > 0 ? { commands: installCommands } : undefined,
build: this.props.commands.length > 0 ? { commands: this.props.commands } : undefined,
},
artifacts: noEmptyObject<any>(renderArtifactsBuildSpec(options.artifacts, this.props.outputs ?? [])),
});
// Partition environment variables into environment variables that can go on the project
// and environment variables that MUST go in the pipeline (those that reference CodePipeline variables)
const env = noUndefined(this.props.env ?? {});
const [actionEnvs, projectEnvs] = partition(Object.entries(env ?? {}), ([, v]) => containsPipelineVariable(v));
const environment = mergeBuildEnvironments(
projectOptions?.buildEnvironment ?? {},
{
environmentVariables: noEmptyObject(mapValues(mkdict(projectEnvs), value => ({ value }))),
});
const fullBuildSpec = projectOptions?.partialBuildSpec
? codebuild.mergeBuildSpecs(projectOptions.partialBuildSpec, buildSpecHere)
: buildSpecHere;
const osFromEnvironment = environment.buildImage && environment.buildImage instanceof codebuild.WindowsBuildImage
? ec2.OperatingSystemType.WINDOWS
: ec2.OperatingSystemType.LINUX;
const actualBuildSpec = filterBuildSpecCommands(fullBuildSpec, osFromEnvironment);
const scope = this.props.scope ?? options.scope;
let projectBuildSpec;
if (this.props.passBuildSpecViaCloudAssembly) {
// Write to disk and replace with a reference
const relativeSpecFile = `buildspec-${Node.of(scope).addr}-${this.constructId}.yaml`;
const absSpecFile = path.join(cloudAssemblyBuildSpecDir(scope), relativeSpecFile);
// This should resolve to a pure JSON string. If it resolves to an object, it's a CFN
// expression, and we can't support that yet. Maybe someday if we think really hard about it.
const fileContents = Stack.of(scope).resolve(actualBuildSpec.toBuildSpec());
if (typeof fileContents !== 'string') {
throw new UnscopedValidationError(`This BuildSpec contains CloudFormation references and is supported by publishInParallel=false: ${JSON.stringify(fileContents, undefined, 2)}`);
}
fs.writeFileSync(absSpecFile, fileContents, { encoding: 'utf-8' });
projectBuildSpec = codebuild.BuildSpec.fromSourceFilename(relativeSpecFile);
} else {
projectBuildSpec = actualBuildSpec;
}
// A hash over the values that make the CodeBuild Project unique (and necessary
// to restart the pipeline if one of them changes). projectName is not necessary to include
// here because the pipeline will definitely restart if projectName changes.
// (Resolve tokens)
const projectConfigHash = hash(Stack.of(scope).resolve({
environment: serializeBuildEnvironment(environment),
buildSpecString: actualBuildSpec.toBuildSpec(),
}));
const actionName = options.actionName;
let projectScope = scope;
if (this.props.additionalConstructLevel ?? true) {
projectScope = obtainScope(scope, actionName);
}
const safePipelineName = Token.isUnresolved(options.pipeline.pipeline.pipelineName)
? `${Stack.of(options.pipeline).stackName}/${Node.of(options.pipeline.pipeline).id}`
: options.pipeline.pipeline.pipelineName;
const project = new codebuild.PipelineProject(projectScope, this.constructId, {
projectName: this.props.projectName,
description: `Pipeline step ${safePipelineName}/${stage.stageName}/${actionName}`.substring(0, 255),
environment,
vpc: projectOptions.vpc,
subnetSelection: projectOptions.subnetSelection,
securityGroups: projectOptions.securityGroups,
cache: projectOptions.cache,
buildSpec: projectBuildSpec,
role: this.props.role,
timeout: projectOptions.timeout,
fileSystemLocations: projectOptions.fileSystemLocations,
logging: projectOptions.logging,
});
if (this.props.additionalDependable) {
project.node.addDependency(this.props.additionalDependable);
}
if (projectOptions.rolePolicy !== undefined) {
projectOptions.rolePolicy.forEach(policyStatement => {
project.addToRolePolicy(policyStatement);
});
}
const stackOutputEnv = mapValues(this.props.envFromCfnOutputs ?? {}, outputRef =>
options.stackOutputsMap.toCodePipeline(outputRef),
);
const configHashEnv = options.beforeSelfMutation
? { _PROJECT_CONFIG_HASH: projectConfigHash }
: {};
// Start all CodeBuild projects from a single (shared) Action Role, so that we don't have to generate an Action Role for each
// individual CodeBuild Project and blow out the pipeline policy size (and potentially # of resources in the stack).
const actionRoleCid = 'CodeBuildActionRole';
const actionRole = this.props.actionRole
?? ( options.pipeline.usePipelineRoleForActions ?
undefined :
(options.pipeline.node.tryFindChild(actionRoleCid) as iam.IRole
?? new iam.Role(options.pipeline, actionRoleCid, {
assumedBy: options.pipeline.pipeline.role,
})));
stage.addAction(new codepipeline_actions.CodeBuildAction({
actionName: actionName,
input: inputArtifact,
extraInputs: extraInputArtifacts,
outputs: outputArtifacts,
project,
runOrder: options.runOrder,
variablesNamespace: options.variablesNamespace,
role: actionRole,
// Inclusion of the hash here will lead to the pipeline structure for any changes
// made the config of the underlying CodeBuild Project.
// Hence, the pipeline will be restarted. This is necessary if the users
// adds (for example) build or test commands to the buildspec.
environmentVariables: noEmptyObject(cbEnv({
...mkdict(actionEnvs),
...configHashEnv,
...stackOutputEnv,
})),
}));
this._project = project;
return { runOrdersConsumed: 1, project };
}