public produceAction()

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 };
  }