Repository: cdklabs/cdk-pipelines-github
Pull request #1024 (2024-06-17 20:02:15)

feat: customizable `environment` in `GitHubActionStep`
`GitHubActionStep` may require access to variables/secrets only available within a GitHub [environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment). 
This PR adds `environment` as an optional input prop for `GitHubActionStep`, allowing users to select an environment per `GitHubActionStep` as they see fit.

a-bigelow: > I hate (and refuse to accept :)) an API that has both `env` and `environment` params. I know you are trying to differentiate in the documentation but thats not enough.
> 
> Do you like `github_environment`? or `github_env_name`.

`githubEnvironment` it is.

Patch:
diff --git a/API.md b/API.md
index 62abf89..50ea531 100644
--- a/API.md
+++ b/API.md
@@ -2321,6 +2321,7 @@ const gitHubActionStepProps: GitHubActionStepProps = { ... }
 | --- | --- | --- |
 | <code><a href="#cdk-pipelines-github.GitHubActionStepProps.property.jobSteps">jobSteps</a></code> | <code><a href="#cdk-pipelines-github.JobStep">JobStep</a>[]</code> | The Job steps. |
 | <code><a href="#cdk-pipelines-github.GitHubActionStepProps.property.env">env</a></code> | <code>{[ key: string ]: string}</code> | Environment variables to set. |
+| <code><a href="#cdk-pipelines-github.GitHubActionStepProps.property.githubEnvironment">githubEnvironment</a></code> | <code>string</code> | The GitHub Environment for the GitHub Action step. |
 | <code><a href="#cdk-pipelines-github.GitHubActionStepProps.property.permissions">permissions</a></code> | <code><a href="#cdk-pipelines-github.JobPermissions">JobPermissions</a></code> | Permissions for the GitHub Action step. |
 
 ---
@@ -2349,6 +2350,23 @@ Environment variables to set.
 
 ---
 
+##### `githubEnvironment`<sup>Optional</sup> <a name="githubEnvironment" id="cdk-pipelines-github.GitHubActionStepProps.property.githubEnvironment"></a>
+
+```typescript
+public readonly githubEnvironment: string;
+```
+
+- *Type:* string
+- *Default:* No GitHub Environment is selected.
+
+The GitHub Environment for the GitHub Action step.
+
+To set shell-level environment variables, use `env`.
+
+> [https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)
+
+---
+
 ##### `permissions`<sup>Optional</sup> <a name="permissions" id="cdk-pipelines-github.GitHubActionStepProps.property.permissions"></a>
 
 ```typescript
@@ -5589,6 +5607,7 @@ API. For example, if you want `secondStep` to occur after `firstStep`, call
 | <code><a href="#cdk-pipelines-github.GitHubActionStep.property.primaryOutput">primaryOutput</a></code> | <code>aws-cdk-lib.pipelines.FileSet</code> | The primary FileSet produced by this Step. |
 | <code><a href="#cdk-pipelines-github.GitHubActionStep.property.env">env</a></code> | <code>{[ key: string ]: string}</code> | *No description.* |
 | <code><a href="#cdk-pipelines-github.GitHubActionStep.property.jobSteps">jobSteps</a></code> | <code><a href="#cdk-pipelines-github.JobStep">JobStep</a>[]</code> | *No description.* |
+| <code><a href="#cdk-pipelines-github.GitHubActionStep.property.githubEnvironment">githubEnvironment</a></code> | <code>string</code> | *No description.* |
 | <code><a href="#cdk-pipelines-github.GitHubActionStep.property.permissions">permissions</a></code> | <code><a href="#cdk-pipelines-github.JobPermissions">JobPermissions</a></code> | *No description.* |
 
 ---
@@ -5690,6 +5709,16 @@ public readonly jobSteps: JobStep[];
 
 ---
 
+##### `githubEnvironment`<sup>Optional</sup> <a name="githubEnvironment" id="cdk-pipelines-github.GitHubActionStep.property.githubEnvironment"></a>
+
+```typescript
+public readonly githubEnvironment: string;
+```
+
+- *Type:* string
+
+---
+
 ##### `permissions`<sup>Optional</sup> <a name="permissions" id="cdk-pipelines-github.GitHubActionStep.property.permissions"></a>
 
 ```typescript
diff --git a/src/pipeline.ts b/src/pipeline.ts
index fc32fb5..1bee61b 100644
--- a/src/pipeline.ts
+++ b/src/pipeline.ts
@@ -838,6 +838,7 @@ export class GitHubWorkflow extends PipelineBase {
         runsOn: this.runner.runsOn,
         needs: this.renderDependencies(node),
         env: step.env,
+        environment: step.githubEnvironment,
         steps: step.jobSteps,
       },
     };
diff --git a/src/steps/github-action-step.ts b/src/steps/github-action-step.ts
index 7033ec4..d0f7fa6 100644
--- a/src/steps/github-action-step.ts
+++ b/src/steps/github-action-step.ts
@@ -17,6 +17,14 @@ export interface GitHubActionStepProps {
    * @default The job receives 'contents: write' permissions. If you set additional permissions and require 'contents: write', it must be provided in your configuration.
    */
   readonly permissions?: JobPermissions;
+
+  /**
+   * The GitHub Environment for the GitHub Action step.
+   * To set shell-level environment variables, use `env`.
+   * @see https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
+   * @default No GitHub Environment is selected.
+   */
+  readonly githubEnvironment?: string;
 }
 
 /**
@@ -25,12 +33,14 @@ export interface GitHubActionStepProps {
 export class GitHubActionStep extends Step {
   public readonly env: Record<string, string>;
   public readonly jobSteps: JobStep[];
+  public readonly githubEnvironment?: string;
   public readonly permissions?: JobPermissions;
 
   constructor(id: string, props: GitHubActionStepProps) {
     super(id);
     this.jobSteps = props.jobSteps;
     this.env = props.env ?? {};
+    this.githubEnvironment = props.githubEnvironment;
     this.permissions = props.permissions;
   }
 }


Test patch:
diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap
index d4589a7..31614ad 100644
--- a/test/__snapshots__/github.test.ts.snap
+++ b/test/__snapshots__/github.test.ts.snap
@@ -527,6 +527,138 @@ jobs:
 "
 `;
 
+exports[`pipeline with GitHubSteps customizing environment 1`] = `
+"# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY.
+# Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github)
+
+name: deploy
+on:
+  push:
+    branches:
+      - main
+  workflow_dispatch: {}
+jobs:
+  Build-Build:
+    name: Synthesize
+    permissions:
+      contents: read
+      id-token: none
+    runs-on: ubuntu-latest
+    needs: []
+    env: {}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build
+        run: \\"\\"
+      - name: Upload cdk.out
+        uses: actions/upload-artifact@v4
+        with:
+          name: cdk.out
+          path: cdk.out
+  Assets-FileAsset1:
+    name: Publish Assets Assets-FileAsset1
+    needs:
+      - Build-Build
+    permissions:
+      contents: read
+      id-token: none
+    runs-on: ubuntu-latest
+    outputs:
+      asset-hash: \${{ steps.Publish.outputs.asset-hash }}
+    steps:
+      - name: Download cdk.out
+        uses: actions/download-artifact@v4
+        with:
+          name: cdk.out
+          path: github.out
+      - name: Install
+        run: npm install --no-save cdk-assets
+      - name: Authenticate Via GitHub Secrets
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          aws-region: us-west-2
+          role-duration-seconds: 1800
+          role-skip-session-tagging: true
+          aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
+      - id: Publish
+        name: Publish Assets-FileAsset1
+        run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset1-step.sh
+  Assets-FileAsset2:
+    name: Publish Assets Assets-FileAsset2
+    needs:
+      - Build-Build
+    permissions:
+      contents: read
+      id-token: none
+    runs-on: ubuntu-latest
+    outputs:
+      asset-hash: \${{ steps.Publish.outputs.asset-hash }}
+    steps:
+      - name: Download cdk.out
+        uses: actions/download-artifact@v4
+        with:
+          name: cdk.out
+          path: github.out
+      - name: Install
+        run: npm install --no-save cdk-assets
+      - name: Authenticate Via GitHub Secrets
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          aws-region: us-west-2
+          role-duration-seconds: 1800
+          role-skip-session-tagging: true
+          aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
+      - id: Publish
+        name: Publish Assets-FileAsset2
+        run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset2-step.sh
+  MyStage-Test-Step:
+    name: Test-Step
+    permissions:
+      contents: write
+    runs-on: ubuntu-latest
+    needs:
+      - Build-Build
+    env: {}
+    environment: production
+    steps:
+      - name: Hello World
+        run: echo hello
+  MyStage-MyStack-Deploy:
+    name: Deploy MyStageMyStackD5720EA1
+    permissions:
+      contents: read
+      id-token: none
+    needs:
+      - Build-Build
+      - Assets-FileAsset1
+      - Assets-FileAsset2
+      - MyStage-Test-Step
+    runs-on: ubuntu-latest
+    steps:
+      - name: Authenticate Via GitHub Secrets
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          aws-region: us-east-1
+          role-duration-seconds: 1800
+          role-skip-session-tagging: true
+          aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          role-to-assume: arn:aws:iam::111111111111:role/cdk-hnb659fds-deploy-role-111111111111-us-east-1
+          role-external-id: Pipeline
+      - id: Deploy
+        uses: aws-actions/aws-cloudformation-github-deploy@v1
+        with:
+          name: MyStage-MyStack
+          template: https://cdk-hnb659fds-assets-111111111111-us-east-1.s3.us-east-1.amazonaws.com/\${{
+            needs.Assets-FileAsset1.outputs.asset-hash }}.json
+          no-fail-on-empty-changeset: \\"1\\"
+          role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1
+"
+`;
+
 exports[`pipeline with GitHubSteps customizing permissions 1`] = `
 "# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY.
 # Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github)
diff --git a/test/github.test.ts b/test/github.test.ts
index 65c6256..c3a2735 100644
--- a/test/github.test.ts
+++ b/test/github.test.ts
@@ -335,6 +335,55 @@ test('pipeline with GitHubSteps customizing permissions', () => {
   });
 });
 
+test('pipeline with GitHubSteps customizing environment', () => {
+  withTemporaryDirectory((dir) => {
+    const pipeline = new GitHubWorkflow(app, 'Pipeline', {
+      workflowPath: `${dir}/.github/workflows/deploy.yml`,
+      synth: new ShellStep('Build', {
+        commands: [],
+      }),
+    });
+
+    const wave = pipeline.addGitHubWave('Test-Wave');
+    const stage = new GitHubStage(app, 'MyStage', {
+      env: { account: '111111111111', region: 'us-east-1' },
+    });
+
+    const stack = new Stack(stage, 'MyStack');
+
+    new lambda.Function(stack, 'Function', {
+      code: lambda.Code.fromAsset(fixtures),
+      handler: 'index.handler',
+      runtime: lambda.Runtime.NODEJS_14_X,
+    });
+
+    wave.addStageWithGitHubOptions(stage, {
+      pre: [new GitHubActionStep('Test-Step', {
+        githubEnvironment: 'production',
+        jobSteps: [{
+          name: 'Hello World',
+          run: 'echo hello',
+        }],
+      })],
+    });
+
+    app.synth();
+
+    const file = readFileSync(pipeline.workflowPath, 'utf-8');
+
+    expect(file).toMatchSnapshot();
+    expect(file).toContain(`MyStage-Test-Step:
+    name: Test-Step
+    permissions:
+      contents: write
+    runs-on: ubuntu-latest
+    needs:
+      - Build-Build
+    env: {}
+    environment: production`);
+  });
+});
+
 test('pipeline with GitHubSteps default permissions', () => {
   withTemporaryDirectory((dir) => {
     const pipeline = new GitHubWorkflow(app, 'Pipeline', {