aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.72k stars 3.94k forks source link

aws-codepipeline-actions: CodeStarConnectionsSourceAction.variables inaccurate #31000

Open dleavitt opened 4 months ago

dleavitt commented 4 months ago

Describe the bug

If you try to use CodeStarConnectionsSourceAction.variables.branchName on an execution triggered by a pull request, your build will fail with:

An action in this pipeline failed because one or more variables could not be resolved: Action name=XYZ. This can result when a variable is referenced that does not exist. Validate the configuration for this action.

Detail

5604 added a .variables getter to a number of codepipeline.Action subclasses including CodeStarConnectionsSourceAction. It's hardcoded to return a particular set of variables, and seems to be the only public way to get at the action's variables.

https://github.com/aws/aws-cdk/blob/9295a85a8fb893d7f5eae06108b68df864096c4c/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/source-action.ts#L119-L128

These variables are correct in some cases, but aren't correct if the execution was triggered by a pull request (see screenshots below.)

With a pullRequestFilter in place, BranchName will be unavailable, but four additional variables will be set: DestinationBranchName, PullRequestId, PullRequestTitle, SourceBranchName. There's no good way to get at these right now.

Expected Behavior

One of the below:

a. variables should return the variables that the source action actually exports (likely impossible to implement at CDK level.) b. variables should return only variables that are always safe to use. I'm not sure if there's a public spec indicating what variables are returned under what circumstances. But BranchName is not always defined by the source action and therefore isn't safe to us.

More importantly, since the CDK doesn't/can't know what the possible variables are, I'd expect the underlying variableExpression(variableName: string) should be publicly accessible, like it is on some of the other actions: https://github.com/aws/aws-cdk/blob/9295a85a8fb893d7f5eae06108b68df864096c4c/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codebuild/build-action.ts#L145-L147

https://github.com/aws/aws-cdk/blob/9295a85a8fb893d7f5eae06108b68df864096c4c/packages/aws-cdk-lib/aws-codepipeline/lib/action.ts#L428-L431

Current Behavior

The cdk-provided variables getter misses some variables and returns others that may not exist. It doesn't provide any way to get at the missing ones.

For missed variables, a workaround is to call the protected variableExpression method via action["variableExpression"]("DestinationBranchName") or similar.

For returned variables that don't exist (BranchName), workaround is not to use it.

Reproduction Steps

Below is a construct that will reproduce the issue. Unfortunately, due to the way CodePipeline works, the minimal reproduction is not especially concise.

To use it, you'll need a CDK stack with a VPC and a CodeConnection to a git repo (I used a Github repo.)

Once you've got the stack deployed, open a pull request in the repo. It should trigger an execution. The UsesBranchName build action will fail, because the source action doesn't export the BranchName variable.

Now try manually triggering a execution with the "Release Change" button in the console. Here, the UsesBranchName build action will succeed, but the UsesDestinationBranchName will fail, because the DestinationBranchName won't have been defined (this will also happen when the pipeline is first created.)

Reproduction: https://gist.github.com/dleavitt/7950f5073bb0ebe2f3fa5049a2f44ab8

Possible Solution

  1. Add a public variable(variableName: string): string method to CodeStarConnectionsSourceAction, with the same implementation like this: https://github.com/aws/aws-cdk/blob/9295a85a8fb893d7f5eae06108b68df864096c4c/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codebuild/build-action.ts#L145-L147

Maybe add it directly to Action (or make variableExpression public) if there are other actions where the list of variables could be dynamic.

  1. (breaking) Consider removing BranchName from CodeStarConnectionsSourceAction.variables(), since it's not always present and if missing attempting to use it will cause the build to fail.

Additional Information/Context

From what I can tell, there's an underlying issue with the implementation of the CodeStarSourceConnection Action provider in CodePipeline. For a given pipeline and action:

Therefore the only variables that can be safely used are ones available in all cases (which BranchName is not.)

I would love to be wrong about this, let me know if there's a workaround!

Variables from a non-pr trigger

Screenshot 2024-07-31 at 17 18 50

Variables from a pull request trigger

Screenshot 2024-07-31 at 17 19 09

CDK CLI Version

2.150.0 (build 3f93027)

Framework Version

No response

Node.js Version

v20.11.1

OS

MacOS 14.3 (23D56)

Language

TypeScript

Language Version

Typescript (5.4.5)

Other information

No response

ashishdhingra commented 4 months ago

Reproducible using following steps:

export class Issue31000Stack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);

const vpc = ec2.Vpc.fromLookup(this, 'MyVpc', {isDefault: true});
new PipelineRepro(this, 'PipelineDemo',{
  owner: '<<repository-owner>>',
  repo: '<<repository-name>>',
  connectionArn: 'arn:aws:codestar-connections:<<region>>:<<account-id>>:connection/<<connection-arn-guid>>',
  vpc: vpc
});

} }

export interface PipelineReproProps { owner: string; repo: string; connectionArn: string; vpc: ec2.IVpc; }

export class PipelineRepro extends Construct { pipeline: codepipeline.Pipeline;

constructor( scope: Construct, id: string, { owner, repo, connectionArn, vpc }: PipelineReproProps, ) { super(scope, id);

const sourceOutput = new codepipeline.Artifact();

this.pipeline = new codepipeline.Pipeline(this, "Pipeline");

// Source Stage
//
const pullSourceAction =
  new codepipeline_actions.CodeStarConnectionsSourceAction({
    actionName: "PullSource",
    connectionArn,
    output: sourceOutput,
    owner,
    repo,
    codeBuildCloneOutput: true,
  });

this.pipeline.addTrigger({
  providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION,
  gitConfiguration: {
    sourceAction: pullSourceAction,
    pullRequestFilter: [
      {
        events: [
          codepipeline.GitPullRequestEvent.OPEN,
          codepipeline.GitPullRequestEvent.UPDATED,
        ],
        branchesIncludes: ["*"],
      },
    ],
  },
});

this.pipeline.addStage({
  stageName: "Source",
  actions: [pullSourceAction],
});

// Build Stage
//
const projectLogGroup = new logs.LogGroup(this, "ProjectLogGroup");
const project = new codebuild.PipelineProject(this, "Project", {
  vpc,
  buildSpec: codebuild.BuildSpec.fromObject({
    version: 0.2,
    env: { "exported-variables": ["GIT_BRANCH_NAME"] },
    phases: {
      build: { commands: ["env"] },
    },
  }),
  logging: { 
    cloudWatch: {
      logGroup: projectLogGroup
    }
  },
});

// will only succeed as part of the release created when the pipeline is
// first created, will fail when triggered through a pull request
const commitIdAction = new codepipeline_actions.CodeBuildAction({
  actionName: "UsesBranchName",
  input: sourceOutput,
  project,
  environmentVariables: {
    GIT_BRANCH_NAME: { value: pullSourceAction.variables.branchName },
  },
});

// will fail when release created and if doing a manual release. will
// succeed when triggered through a pull request
const sourceCommitIdAction = new codepipeline_actions.CodeBuildAction({
  actionName: "UsesDestinationBranchName",
  input: sourceOutput,
  project,
  environmentVariables: {
    GIT_BRANCH_NAME: {
      // workaround for accessing variable
      value: pullSourceAction["variableExpression"](
        "DestinationBranchName",
      ),
    },
  },
});

this.pipeline.addStage({
  stageName: "Build",
  actions: [commitIdAction, sourceCommitIdAction],
});

} }

... ... const app = new cdk.App();

new Issue31000Stack(app, 'Issue31000Stack', { env: { account: '<>', region: '<>'} // Required if referencing a VPC. });


- Create a pull request in the source repository. CodePipeline will fail with the error `An action in this pipeline failed because one or more variables could not be resolved: Action name=UsesBranchName. This can result when a variable is referenced that does not exist. Validate the configuration for this action.`
  <img width="1648" alt="Screenshot 2024-08-01 at 3 57 59 PM" src="https://github.com/user-attachments/assets/bc49f912-055e-463d-952c-b04d81ddc1a0">