diff --git a/API.md b/API.md index 7fb8d3e6..691f0dd2 100644 --- a/API.md +++ b/API.md @@ -9,6 +9,7 @@ Name|Description [DockerCredential](#cdk-pipelines-github-dockercredential)|Represents a credential used to authenticate to a docker registry. [GitHubActionRole](#cdk-pipelines-github-githubactionrole)|Creates or references a GitHub OIDC provider and accompanying role that trusts the provider. [GitHubActionStep](#cdk-pipelines-github-githubactionstep)|Specifies a GitHub Action as a step in the pipeline. +[GitHubWave](#cdk-pipelines-github-githubwave)|Multiple stages that are deployed in parallel. [GitHubWorkflow](#cdk-pipelines-github-githubworkflow)|CDK Pipelines for GitHub workflows. [JsonPatch](#cdk-pipelines-github-jsonpatch)|Utility for applying RFC-6902 JSON-Patch to a document. [Runner](#cdk-pipelines-github-runner)|The type of runner to run the job on. @@ -367,6 +368,7 @@ new GitHubActionStep(id: string, props: GitHubActionStepProps) * **props** ([GitHubActionStepProps](#cdk-pipelines-github-githubactionstepprops)) *No description* * **jobSteps** (Array<[JobStep](#cdk-pipelines-github-jobstep)>) The Job steps. * **env** (Map) Environment variables to set. __*Optional*__ + * **if** (string) Add an addition `if` clause on the `job.*` step for this `GitHubActionStep`. __*Optional*__ @@ -377,6 +379,88 @@ Name | Type | Description -----|------|------------- **env** | Map | **jobSteps** | Array<[JobStep](#cdk-pipelines-github-jobstep)> | +**if**? | string | __*Optional*__ + + + +## class GitHubWave + +Multiple stages that are deployed in parallel. + +A `Wave`, but with addition GitHub options - created by `GitHubWorkflow.addWave()` or +`GitHubWorkflow.addGitHubWave()` - DO NOT CREATE DIRECTLY + +__Extends__: [pipelines.Wave](#aws-cdk-lib-pipelines-wave) + +### Initializer + + + + +```ts +new GitHubWave(id: string, pipeline: GitHubWorkflow, props?: WaveProps) +``` + +* **id** (string) Identifier for this Wave. +* **pipeline** ([GitHubWorkflow](#cdk-pipelines-github-githubworkflow)) GitHubWorkflow that this wave is part of. +* **props** ([pipelines.WaveProps](#aws-cdk-lib-pipelines-waveprops)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stages in the wave. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stages in the wave. __*Default*__: No additional steps + + + +### Properties + + +Name | Type | Description +-----|------|------------- +**id** | string | Identifier for this Wave. + +### Methods + + +#### addStage(stage, options?) + +Add a Stage to this wave. + +It will be deployed in parallel with all other stages in this +wave. + +```ts +addStage(stage: Stage, options?: AddStageOpts): StageDeployment +``` + +* **stage** ([Stage](#aws-cdk-lib-stage)) *No description* +* **options** ([pipelines.AddStageOpts](#aws-cdk-lib-pipelines-addstageopts)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stacks in the stage. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stacks in the stage. __*Default*__: No additional steps + * **stackSteps** (Array<[pipelines.StackSteps](#aws-cdk-lib-pipelines-stacksteps)>) Instructions for stack level steps. __*Default*__: No additional instructions + +__Returns__: +* [pipelines.StageDeployment](#aws-cdk-lib-pipelines-stagedeployment) + +#### addStageWithGitHubOptions(stage, options?) + +Add a Stage to this wave. + +It will be deployed in parallel with all other stages in this +wave. + +```ts +addStageWithGitHubOptions(stage: Stage, options?: AddGitHubStageOptions): StageDeployment +``` + +* **stage** ([Stage](#aws-cdk-lib-stage)) *No description* +* **options** ([AddGitHubStageOptions](#cdk-pipelines-github-addgithubstageoptions)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stacks in the stage. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stacks in the stage. __*Default*__: No additional steps + * **stackSteps** (Array<[pipelines.StackSteps](#aws-cdk-lib-pipelines-stacksteps)>) Instructions for stack level steps. __*Default*__: No additional instructions + * **gitHubEnvironment** (string) Run the stage in a specific GitHub Environment. __*Default*__: no GitHub environment + * **jobSettings** ([JobSettings](#cdk-pipelines-github-jobsettings)) Job level settings that will be applied to all jobs in the stage. __*Optional*__ + * **stackCapabilities** (Array<[StackCapabilities](#cdk-pipelines-github-stackcapabilities)>) In some cases, you must explicitly acknowledge that your CloudFormation stack template contains certain capabilities in order for CloudFormation to create the stack. __*Default*__: ['CAPABILITY_IAM'] + +__Returns__: +* [pipelines.StageDeployment](#aws-cdk-lib-pipelines-stagedeployment) @@ -430,6 +514,22 @@ Name | Type | Description ### Methods +#### addGitHubWave(id, options?) + + + +```ts +addGitHubWave(id: string, options?: WaveOptions): GitHubWave +``` + +* **id** (string) *No description* +* **options** ([pipelines.WaveOptions](#aws-cdk-lib-pipelines-waveoptions)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stages in the wave. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stages in the wave. __*Default*__: No additional steps + +__Returns__: +* [GitHubWave](#cdk-pipelines-github-githubwave) + #### addStageWithGitHubOptions(stage, options?) Deploy a single Stage by itself with options for further GitHub configuration. @@ -453,6 +553,59 @@ addStageWithGitHubOptions(stage: Stage, options?: AddGitHubStageOptions): StageD __Returns__: * [pipelines.StageDeployment](#aws-cdk-lib-pipelines-stagedeployment) +#### addWave(id, options?) + +Add a Wave to the pipeline, for deploying multiple Stages in parallel. + +Use the return object of this method to deploy multiple stages in parallel. + +Example: + +```ts +declare const pipeline: pipelines.CodePipeline; + +const wave = pipeline.addWave('MyWave'); +wave.addStage(new MyApplicationStage(this, 'Stage1')); +wave.addStage(new MyApplicationStage(this, 'Stage2')); +``` + +```ts +addWave(id: string, options?: WaveOptions): Wave +``` + +* **id** (string) *No description* +* **options** ([pipelines.WaveOptions](#aws-cdk-lib-pipelines-waveoptions)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stages in the wave. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stages in the wave. __*Default*__: No additional steps + +__Returns__: +* [pipelines.Wave](#aws-cdk-lib-pipelines-wave) + +#### addingStageFromWave(stage, stageDeployment, options?) + +Support adding stages with GitHub options to waves - should ONLY be called internally. + +Use `pipeline.addWave()` and it'll call this when `wave.addStage()` is called. + +`pipeline.addStage()` will also call this, since it calls `pipeline.addWave().addStage()`. + +```ts +addingStageFromWave(stage: Stage, stageDeployment: StageDeployment, options?: AddGitHubStageOptions): void +``` + +* **stage** ([Stage](#aws-cdk-lib-stage)) *No description* +* **stageDeployment** ([pipelines.StageDeployment](#aws-cdk-lib-pipelines-stagedeployment)) *No description* +* **options** ([AddGitHubStageOptions](#cdk-pipelines-github-addgithubstageoptions)) *No description* + * **post** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run after all of the stacks in the stage. __*Default*__: No additional steps + * **pre** (Array<[pipelines.Step](#aws-cdk-lib-pipelines-step)>) Additional steps to run before any of the stacks in the stage. __*Default*__: No additional steps + * **stackSteps** (Array<[pipelines.StackSteps](#aws-cdk-lib-pipelines-stacksteps)>) Instructions for stack level steps. __*Default*__: No additional instructions + * **gitHubEnvironment** (string) Run the stage in a specific GitHub Environment. __*Default*__: no GitHub environment + * **jobSettings** ([JobSettings](#cdk-pipelines-github-jobsettings)) Job level settings that will be applied to all jobs in the stage. __*Optional*__ + * **stackCapabilities** (Array<[StackCapabilities](#cdk-pipelines-github-stackcapabilities)>) In some cases, you must explicitly acknowledge that your CloudFormation stack template contains certain capabilities in order for CloudFormation to create the stack. __*Default*__: ['CAPABILITY_IAM'] + + + + #### protected doBuildPipeline() Implemented by subclasses to do the actual pipeline construction. @@ -933,6 +1086,7 @@ Name | Type | Description -----|------|------------- **jobSteps** | Array<[JobStep](#cdk-pipelines-github-jobstep)> | The Job steps. **env**? | Map | Environment variables to set.
__*Optional*__ +**if**? | string | Add an addition `if` clause on the `job.*` step for this `GitHubActionStep`.
__*Optional*__ diff --git a/src/pipeline.ts b/src/pipeline.ts index c4976746..aa3bb73b 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -2,12 +2,13 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs'; import * as path from 'path'; import { Stage } from 'aws-cdk-lib'; import { EnvironmentPlaceholders } from 'aws-cdk-lib/cx-api'; -import { PipelineBase, PipelineBaseProps, ShellStep, StackAsset, StackDeployment, StackOutputReference, StageDeployment, Step } from 'aws-cdk-lib/pipelines'; +import { AddStageOpts, PipelineBase, PipelineBaseProps, ShellStep, StackAsset, StackDeployment, StackOutputReference, StageDeployment, Step, Wave, WaveOptions, WaveProps } from 'aws-cdk-lib/pipelines'; import { AGraphNode, PipelineGraph, Graph, isGraph } from 'aws-cdk-lib/pipelines/lib/helpers-internal'; import { Construct } from 'constructs'; import * as decamelize from 'decamelize'; import { AwsCredentials, AwsCredentialsProvider } from './aws-credentials'; import { DockerCredential } from './docker-credentials'; +import { GitHubStage } from './stage'; import { AddGitHubStageOptions } from './stage-options'; import { GitHubActionStep } from './steps/github-action-step'; import * as github from './workflows-model'; @@ -175,8 +176,13 @@ export class GitHubWorkflow extends PipelineBase { private readonly assetHashMap: Record = {}; private readonly runner: github.Runner; private readonly publishAssetsAuthRegion: string; - private readonly stackProperties: Record> = {}; + private readonly stackProperties: Record = {}; private readonly jobSettings?: JobSettings; + private builtGH = false; // cannot be `built` since that's defined in the parent as private constructor(scope: Construct, id: string, props: GitHubWorkflowProps) { super(scope, id, props); @@ -256,6 +262,57 @@ export class GitHubWorkflow extends PipelineBase { return stageDeployment; } + + /** + * Add a Wave to the pipeline, for deploying multiple Stages in parallel + * + * Use the return object of this method to deploy multiple stages in parallel. + * + * Example: + * + * ```ts + * declare const pipeline: pipelines.CodePipeline; + * + * const wave = pipeline.addWave('MyWave'); + * wave.addStage(new MyApplicationStage(this, 'Stage1')); + * wave.addStage(new MyApplicationStage(this, 'Stage2')); + * ``` + */ + public addWave(id: string, options?: WaveOptions): Wave { + return this.addGitHubWave(id, options); + } + + public addGitHubWave(id: string, options?: WaveOptions): GitHubWave { + if (this.builtGH) { + throw new Error('addWave: can\'t add Waves anymore after buildPipeline() has been called'); + } + + const wave = new GitHubWave(id, this, options); + this.waves.push(wave); + return wave; + } + + /** + * Support adding stages with GitHub options to waves - should ONLY be called internally. + * + * Use `pipeline.addWave()` and it'll call this when `wave.addStage()` is called. + * + * `pipeline.addStage()` will also call this, since it calls `pipeline.addWave().addStage()`. + */ + public addingStageFromWave(stage: Stage, stageDeployment: StageDeployment, options?: AddGitHubStageOptions) { + if (!(stage instanceof GitHubStage) && options === undefined) { + return; + } + + const ghStage = stage instanceof GitHubStage ? stage : undefined; + + // keep track of GitHub specific options + const stacks = stageDeployment.stacks; + this.addStackProps(stacks, 'environment', ghStage?.props?.gitHubEnvironment ?? options?.gitHubEnvironment); + this.addStackProps(stacks, 'capabilities', ghStage?.props?.stackCapabilities ?? options?.stackCapabilities); + this.addStackProps(stacks, 'settings', ghStage?.props?.jobSettings ?? options?.jobSettings); + } + private addStackProps(stacks: StackDeployment[], key: string, value: any) { if (value === undefined) { return; } for (const stack of stacks) { @@ -267,6 +324,7 @@ export class GitHubWorkflow extends PipelineBase { } protected doBuildPipeline() { + this.builtGH = true; const app = Stage.of(this); if (!app) { throw new Error('The GitHub Workflow must be defined in the scope of an App'); @@ -442,7 +500,7 @@ export class GitHubWorkflow extends PipelineBase { id: jobId, definition: { name: `Publish Assets ${jobId}`, - ...this.jobSettings, + ...this.renderJobSettingParameters(), needs: this.renderDependencies(node), permissions: { contents: github.JobPermission.READ, @@ -513,7 +571,7 @@ export class GitHubWorkflow extends PipelineBase { id: node.uniqueId, definition: { name: `Deploy ${stack.stackArtifactId}`, - ...this.jobSettings, + ...this.renderJobSettingParameters(), ...this.stackProperties[stack.stackArtifactId]?.settings, permissions: { contents: github.JobPermission.READ, @@ -564,7 +622,7 @@ export class GitHubWorkflow extends PipelineBase { id: node.uniqueId, definition: { name: 'Synthesize', - ...this.jobSettings, + ...this.renderJobSettingParameters(true), permissions: { contents: github.JobPermission.READ, // The Synthesize job does not use the GitHub Action Role on its own, but it's possible @@ -658,7 +716,7 @@ export class GitHubWorkflow extends PipelineBase { id: node.uniqueId, definition: { name: step.id, - ...this.jobSettings, + ...this.renderJobSettingParameters(), permissions: { contents: github.JobPermission.READ, }, @@ -683,7 +741,8 @@ export class GitHubWorkflow extends PipelineBase { id: node.uniqueId, definition: { name: step.id, - ...this.jobSettings, + ...this.renderJobSettingParameters(), + if: step.if, permissions: { contents: github.JobPermission.WRITE, }, @@ -777,6 +836,16 @@ export class GitHubWorkflow extends PipelineBase { return deps.map(x => x.uniqueId); } + + private renderJobSettingParameters(isBuildStep = false) { + if (isBuildStep) { + return { + if: this.jobSettings?.if, + }; + } + // in the future, additional job settings may be rendered here + return {}; + } } interface Context { @@ -816,6 +885,63 @@ function snakeCaseKeys(obj: T, sep = '-'): T { return result as any; } +/** + * Multiple stages that are deployed in parallel + * + * A `Wave`, but with addition GitHub options - created by `GitHubWorkflow.addWave()` or + * `GitHubWorkflow.addGitHubWave()` - DO NOT CREATE DIRECTLY + */ +export class GitHubWave extends Wave { + constructor( + /** Identifier for this Wave */ + public readonly id: string, + /** GitHubWorkflow that this wave is part of */ + private pipeline: GitHubWorkflow, + props: WaveProps = {}, + ) { + super(id, props); + } + + /** + * Add a Stage to this wave + * + * It will be deployed in parallel with all other stages in this + * wave. + */ + public addStage(stage: Stage, options: AddStageOpts = {}) { + const stageDeployment = super.addStage(stage, options); + this.pipeline.addingStageFromWave(stage, stageDeployment); + return stageDeployment; + } + + /** + * Add a Stage to this wave + * + * It will be deployed in parallel with all other stages in this + * wave. + */ + public addStageWithGitHubOptions(stage: Stage, options?: AddGitHubStageOptions): StageDeployment { + const stageDeployment = super.addStage(stage, options); + this.pipeline.addingStageFromWave(stage, stageDeployment, options); + return stageDeployment; + } + + // /** + // * Add an additional step to run before any of the stages in this wave + // */ + // public addPre(...steps: Step[]) { + // this.pre.push(...steps); + // } + + // /** + // * Add an additional step to run after all of the stages in this wave + // */ + // public addPost(...steps: Step[]) { + // this.post.push(...steps); + // } +} + + /** * Names of secrets for AWS credentials. */ diff --git a/src/stage.ts b/src/stage.ts new file mode 100644 index 00000000..afeb3910 --- /dev/null +++ b/src/stage.ts @@ -0,0 +1,46 @@ +import { Stage, StageProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { JobSettings } from './pipeline'; +import { StackCapabilities } from './stage-options'; + + +export interface GitHubStageProps extends StageProps { + /** + * Run the stage in a specific GitHub Environment. If specified, + * any protection rules configured for the environment must pass + * before the job is set to a runner. For example, if the environment + * has a manual approval rule configured, then the workflow will + * wait for the approval before sending the job to the runner. + * + * Running a workflow that references an environment that does not + * exist will create an environment with the referenced name. + * @see https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment + * + * @default - no GitHub environment + */ + readonly gitHubEnvironment?: string; + + /** + * In some cases, you must explicitly acknowledge that your CloudFormation + * stack template contains certain capabilities in order for CloudFormation + * to create the stack. + * + * If insufficiently specified, CloudFormation returns an `InsufficientCapabilities` + * error. + * + * @default ['CAPABILITY_IAM'] + */ + readonly stackCapabilities?: StackCapabilities[]; + + /** + * Job level settings that will be applied to all jobs in the stage. + * Currently the only valid setting is 'if'. + */ + readonly jobSettings?: JobSettings; +} + +export class GitHubStage extends Stage { + constructor(scope: Construct, id: string, public readonly props?: GitHubStageProps) { + super(scope, id, props); + } +} \ No newline at end of file diff --git a/src/steps/github-action-step.ts b/src/steps/github-action-step.ts index e5bfa414..acd526c4 100644 --- a/src/steps/github-action-step.ts +++ b/src/steps/github-action-step.ts @@ -11,7 +11,28 @@ export interface GitHubActionStepProps { * Environment variables to set. */ readonly env?: Record; -} + + /** + * Add an addition `if` clause on the `job.*` step for this `GitHubActionStep` + * + * Note that setting this may allow the job to run even if any of the jobs it depends on fails. + * + * In cases where it's only desired to run when previous jobs succeed, then use `success()`, such as: + * + * ```ts + * const postStep = new GitHubActionStep('PostDeployAction', { + * jobSteps: [ + * // ... + * ], + * if: "success() && contains(github.event.issue.labels.*.name, 'cleanup')", + * }); + * ``` + * + * @see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idif + * @see https://docs.github.com/en/actions/learn-github-actions/expressions#success + */ + readonly if?: string; +}; /** * Specifies a GitHub Action as a step in the pipeline. @@ -19,10 +40,12 @@ export interface GitHubActionStepProps { export class GitHubActionStep extends Step { public readonly env: Record; public readonly jobSteps: JobStep[]; + public readonly if?: string; constructor(id: string, props: GitHubActionStepProps) { super(id); this.jobSteps = props.jobSteps; this.env = props.env ?? {}; + this.if = props.if; } } diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index aa795e17..d965e85c 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -440,7 +440,6 @@ jobs: path: cdk.out Assets-FileAsset1: name: Publish Assets Assets-FileAsset1 - if: github.repository == 'account/repo' needs: - Build-Build permissions: @@ -470,7 +469,6 @@ jobs: run: /bin/bash ./cdk.out/publish-Assets-FileAsset1-step.sh Assets-FileAsset2: name: Publish Assets Assets-FileAsset2 - if: github.repository == 'account/repo' needs: - Build-Build permissions: @@ -500,7 +498,6 @@ jobs: run: /bin/bash ./cdk.out/publish-Assets-FileAsset2-step.sh MyStack-MyStack-Deploy: name: Deploy MyStack098574E7 - if: github.repository == 'account/repo' permissions: contents: read id-token: none diff --git a/test/__snapshots__/stage-options.test.ts.snap b/test/__snapshots__/stage-options.test.ts.snap index 7a4f702c..e1892f16 100644 --- a/test/__snapshots__/stage-options.test.ts.snap +++ b/test/__snapshots__/stage-options.test.ts.snap @@ -11,8 +11,9 @@ on: - main workflow_dispatch: {} jobs: - Build-Build: + Build-Synth: name: Synthesize + if: contains(fromJson('[\\"push\\", \\"pull_request\\"]'), github.event_name) permissions: contents: read id-token: none @@ -34,7 +35,7 @@ jobs: Assets-FileAsset1: name: Publish Assets Assets-FileAsset1 needs: - - Build-Build + - Build-Synth permissions: contents: read id-token: none @@ -60,13 +61,13 @@ jobs: - id: Publish name: Publish Assets-FileAsset1 run: /bin/bash ./cdk.out/publish-Assets-FileAsset1-step.sh - MyStack-PreDeployAction: + MyPrePostStack-PreDeployAction: name: PreDeployAction permissions: contents: write runs-on: ubuntu-latest needs: - - Build-Build + - Build-Synth env: {} steps: - name: pre deploy action @@ -74,15 +75,16 @@ jobs: with: app-id: 1234 secrets: my-secrets - MyStack-MyStack-Deploy: - name: Deploy MyStack098574E7 + MyPrePostStack-MyStack-Deploy: + name: Deploy MyPrePostStackMyStack8AD5AF9E + if: success() && contains(github.event.issue.labels.*.name, 'deploy') permissions: contents: read id-token: none needs: - - Build-Build + - Build-Synth - Assets-FileAsset1 - - MyStack-PreDeployAction + - MyPrePostStack-PreDeployAction runs-on: ubuntu-latest steps: - name: Authenticate Via GitHub Secrets @@ -98,19 +100,20 @@ jobs: - id: Deploy uses: aws-actions/aws-cloudformation-github-deploy@v1 with: - name: MyStack-MyStack + name: MyPrePostStack-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 - MyStack-PostDeployAction: + MyPrePostStack-PostDeployAction: name: PostDeployAction + if: failure() && contains(github.event.issue.labels.*.name, 'cleanupFailure') permissions: contents: write runs-on: ubuntu-latest needs: - - MyStack-MyStack-Deploy - - Build-Build + - MyPrePostStack-MyStack-Deploy + - Build-Synth env: {} steps: - name: Checkout @@ -429,6 +432,164 @@ jobs: " `; +exports[`github stages in waves works 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 + if: contains(github.event.issue.labels.*.name, 'deployToA') || + contains(github.event.issue.labels.*.name, 'deployToB') + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + needs: [] + env: {} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install + run: yarn + - name: Build + run: yarn build + - name: Upload cdk.out + uses: actions/upload-artifact@v2.1.1 + 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@v2 + with: + name: cdk.out + path: stage.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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/publish-Assets-FileAsset1-step.sh + MyWave-PreWaveAction: + name: PreWaveAction + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - Build-Build + env: {} + steps: + - name: pre wave action + uses: my-pre-wave-action@1.0.0 + with: + app-id: 1234 + secrets: my-secrets + MyWave-MyStageA-MyStackA-Deploy: + name: Deploy MyStageAMyStackA0F0BE321 + if: success() && contains(github.event.issue.labels.*.name, 'deployToA') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + - MyWave-PreWaveAction + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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: MyStageA-MyStackA + 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 + MyWave-MyStageB-MyStackB-Deploy: + name: Deploy MyStageBMyStackBFE4B1ADE + if: success() && contains(github.event.issue.labels.*.name, 'deployToB') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + - MyWave-PreWaveAction + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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::12345678901:role/cdk-hnb659fds-deploy-role-12345678901-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStageB-MyStackB + template: https://cdk-hnb659fds-assets-12345678901-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::12345678901:role/cdk-hnb659fds-cfn-exec-role-12345678901-us-east-1 + MyWave-PostWaveAction: + name: PostWaveAction + if: failure() && contains(github.event.issue.labels.*.name, 'cleanupFailure') + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - MyWave-MyStageA-MyStackA-Deploy + - MyWave-MyStageB-MyStackB-Deploy + - Build-Build + env: {} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: post wave action + uses: my-post-wave-action@1.0.0 + with: + app-id: 4321 + secrets: secrets +" +`; + exports[`job settings can specify job settings at stage level 1`] = ` "# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY. # Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github) @@ -520,3 +681,246 @@ jobs: role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1 " `; + +exports[`stages in github waves works 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@v2 + - name: Install + run: yarn + - name: Build + run: yarn build + - name: Upload cdk.out + uses: actions/upload-artifact@v2.1.1 + 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@v2 + with: + name: cdk.out + path: stage.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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/publish-Assets-FileAsset1-step.sh + MyWave-MyStageA-MyStackA-Deploy: + name: Deploy MyStageAMyStackA0F0BE321 + if: success() && contains(github.event.issue.labels.*.name, 'deployToA') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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: MyStageA-MyStackA + 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 + MyWave-MyStageB-MyStackB-Deploy: + name: Deploy MyStageBMyStackBFE4B1ADE + if: success() && contains(github.event.issue.labels.*.name, 'deployToB') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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::12345678901:role/cdk-hnb659fds-deploy-role-12345678901-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStageB-MyStackB + template: https://cdk-hnb659fds-assets-12345678901-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::12345678901:role/cdk-hnb659fds-cfn-exec-role-12345678901-us-east-1 +" +`; + +exports[`stages in pipeline works with \`if\` 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@v2 + - name: Install + run: yarn + - name: Build + run: yarn build + - name: Upload cdk.out + uses: actions/upload-artifact@v2.1.1 + 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@v2 + with: + name: cdk.out + path: stage.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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/publish-Assets-FileAsset1-step.sh + MyStageA-MyStackA-Deploy: + name: Deploy MyStageAMyStackA0F0BE321 + if: success() && contains(github.event.issue.labels.*.name, 'deployToA') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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: MyStageA-MyStackA + 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 + MyStageB-MyStackB-Deploy: + name: Deploy MyStageBMyStackBFE4B1ADE + if: success() && contains(github.event.issue.labels.*.name, 'deployToB') + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + - MyStageA-MyStackA-Deploy + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v1 + 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::12345678901:role/cdk-hnb659fds-deploy-role-12345678901-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStageB-MyStackB + template: https://cdk-hnb659fds-assets-12345678901-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::12345678901:role/cdk-hnb659fds-cfn-exec-role-12345678901-us-east-1 +" +`; diff --git a/test/stage-options.test.ts b/test/stage-options.test.ts index 7aedd4ab..52964211 100644 --- a/test/stage-options.test.ts +++ b/test/stage-options.test.ts @@ -1,7 +1,9 @@ import { readFileSync } from 'fs'; import { Stack, Stage } from 'aws-cdk-lib'; import { ShellStep } from 'aws-cdk-lib/pipelines'; -import { GitHubWorkflow, StackCapabilities, GitHubActionStep } from '../src'; +import * as YAML from 'yaml'; +import { GitHubWorkflow, StackCapabilities, GitHubActionStep, AddGitHubStageOptions } from '../src'; +import { GitHubStage, GitHubStageProps } from '../src/stage'; import { withTemporaryDirectory, TestApp } from './testutil'; let app: TestApp; @@ -57,8 +59,9 @@ describe('github environment', () => { const testStage = new Stage(app, 'MyStage1', { env: { account: '111111111111', region: 'us-east-1' }, }); - const prodStage = new Stage(app, 'MyStage2', { + const prodStage = new GitHubStage(app, 'MyStage2', { env: { account: '222222222222', region: 'us-west-2' }, + gitHubEnvironment: 'prod', }); // Two stacks @@ -68,9 +71,7 @@ describe('github environment', () => { pipeline.addStageWithGitHubOptions(testStage, { gitHubEnvironment: 'test', }); - pipeline.addStageWithGitHubOptions(prodStage, { - gitHubEnvironment: 'prod', - }); + pipeline.addStage(prodStage); app.synth(); @@ -175,7 +176,7 @@ describe('job settings', () => { commands: ['yarn build'], }), jobSettings: { - if: 'github.repository == \'another/repo\'', + if: 'github.repository == \'another/repoA\'', }, }); @@ -187,13 +188,15 @@ describe('job settings', () => { pipeline.addStageWithGitHubOptions(stage, { jobSettings: { - if: 'github.repository == \'github/repo\'', + if: 'github.repository == \'github/repoB\'', }, }); app.synth(); - expect(readFileSync(pipeline.workflowPath, 'utf-8')).toContain('if: github.repository == \'github/repo\'\n'); + const workflowFileContents = readFileSync(pipeline.workflowPath, 'utf-8'); + expect(workflowFileContents).toContain('if: github.repository == \'another/repoA\'\n'); + expect(workflowFileContents).toContain('if: github.repository == \'github/repoB\'\n'); }); }); }); @@ -202,48 +205,56 @@ test('can set pre/post github action job step', () => { withTemporaryDirectory((dir) => { const pipeline = new GitHubWorkflow(app, 'Pipeline', { workflowPath: `${dir}/.github/workflows/deploy.yml`, - synth: new ShellStep('Build', { + synth: new ShellStep('Synth', { installCommands: ['yarn'], commands: ['yarn build'], }), + jobSettings: { if: 'contains(fromJson(\'["push", "pull_request"]\'), github.event_name)' }, }); - const stage = new Stage(app, 'MyStack', { + const stage = new GitHubStage(app, 'MyPrePostStack', { env: { account: '111111111111', region: 'us-east-1' }, + jobSettings: { if: "success() && contains(github.event.issue.labels.*.name, 'deploy')" }, }); new Stack(stage, 'MyStack'); - pipeline.addStageWithGitHubOptions(stage, { - pre: [new GitHubActionStep('PreDeployAction', { - jobSteps: [ - { - name: 'pre deploy action', - uses: 'my-pre-deploy-action@1.0.0', - with: { - 'app-id': 1234, - 'secrets': 'my-secrets', + pipeline.addStage(stage, { + pre: [ + new GitHubActionStep('PreDeployAction', { + jobSteps: [ + { + name: 'pre deploy action', + uses: 'my-pre-deploy-action@1.0.0', + with: { + 'app-id': 1234, + 'secrets': 'my-secrets', + }, }, - }, - ], - })], - - post: [new GitHubActionStep('PostDeployAction', { - jobSteps: [ - { - name: 'Checkout', - uses: 'actions/checkout@v2', - }, - { - name: 'post deploy action', - uses: 'my-post-deploy-action@1.0.0', - with: { - 'app-id': 4321, - 'secrets': 'secrets', + ], + }), + ], + + + post: [ + new GitHubActionStep('PostDeployAction', { + jobSteps: [ + { + name: 'Checkout', + uses: 'actions/checkout@v2', }, - }, - ], - })], + { + name: 'post deploy action', + uses: 'my-post-deploy-action@1.0.0', + with: { + 'app-id': 4321, + 'secrets': 'secrets', + }, + }, + ], + if: "failure() && contains(github.event.issue.labels.*.name, 'cleanupFailure')", + }), + ], }); app.synth(); @@ -253,5 +264,221 @@ test('can set pre/post github action job step', () => { expect(workflowFileContents).toContain('my-pre-deploy-action\@1\.0\.0'); expect(workflowFileContents).toContain('my-post-deploy-action\@1\.0\.0'); expect(workflowFileContents).toContain('actions/checkout@v2'); + expect(workflowFileContents).toContain('contains(fromJson(\'["push", "pull_request"]\'), github.event_name)'); + expect(workflowFileContents).toContain("success() && contains(github.event.issue.labels.*.name, 'deploy')"); + expect(workflowFileContents).toContain("failure() && contains(github.event.issue.labels.*.name, 'cleanupFailure')"); + }); +}); + +test('stages in github waves works', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + installCommands: ['yarn'], + commands: ['yarn build'], + }), + }); + + const stageA = new Stage(app, 'MyStageA', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + new Stack(stageA, 'MyStackA'); + + const wave = pipeline.addGitHubWave('MyWave'); + + const stageAOptions: AddGitHubStageOptions = { + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToA')", + }, + }; + wave.addStageWithGitHubOptions(stageA, stageAOptions); + + const stageBOptions: GitHubStageProps = { + env: { account: '12345678901', region: 'us-east-1' }, + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToB')", + }, + }; + const stageB = new GitHubStage(app, 'MyStageB', stageBOptions); + + new Stack(stageB, 'MyStackB'); + + wave.addStage(stageB); + + app.synth(); + + const workflowFileContents = readFileSync(pipeline.workflowPath, 'utf-8'); + expect(workflowFileContents).toMatchSnapshot(); + + const yaml = YAML.parse(workflowFileContents); + expect(yaml).toMatchObject({ + jobs: { + 'MyWave-MyStageA-MyStackA-Deploy': { + if: stageAOptions.jobSettings?.if, + }, + 'MyWave-MyStageB-MyStackB-Deploy': { + if: stageBOptions.jobSettings?.if, + }, + }, + }); + }); +}); + + +test('github stages in waves works', () => { + withTemporaryDirectory((dir) => { + const buildIfStatement = "contains(github.event.issue.labels.*.name, 'deployToA') || contains(github.event.issue.labels.*.name, 'deployToB')"; + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + installCommands: ['yarn'], + commands: ['yarn build'], + }), + jobSettings: { + if: buildIfStatement, + }, + }); + + const stageAOptions: GitHubStageProps = { + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToA')", + }, + }; + const stageA = new GitHubStage(app, 'MyStageA', { + env: { account: '111111111111', region: 'us-east-1' }, + ...stageAOptions, + }); + + new Stack(stageA, 'MyStackA'); + + const stageBOptions: GitHubStageProps = { + env: { account: '12345678901', region: 'us-east-1' }, + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToB')", + }, + }; + const stageB = new GitHubStage(app, 'MyStageB', stageBOptions); + + new Stack(stageB, 'MyStackB'); + + // Make a wave to have the stages be parallel (not depend on each other) + const wave = pipeline.addWave('MyWave', + { + pre: [ + new GitHubActionStep('PreWaveAction', { + jobSteps: [ + { + name: 'pre wave action', + uses: 'my-pre-wave-action@1.0.0', + with: { + 'app-id': 1234, + 'secrets': 'my-secrets', + }, + }, + ], + }), + ], + + + post: [ + new GitHubActionStep('PostWaveAction', { + jobSteps: [ + { + name: 'Checkout', + uses: 'actions/checkout@v2', + }, + { + name: 'post wave action', + uses: 'my-post-wave-action@1.0.0', + with: { + 'app-id': 4321, + 'secrets': 'secrets', + }, + }, + ], + if: "failure() && contains(github.event.issue.labels.*.name, 'cleanupFailure')", + }), + ], + }, + ); + wave.addStage(stageA); + wave.addStage(stageB); + + app.synth(); + + const workflowFileContents = readFileSync(pipeline.workflowPath, 'utf-8'); + expect(workflowFileContents).toMatchSnapshot(); + + const yaml = YAML.parse(workflowFileContents); + expect(yaml).toMatchObject({ + jobs: { + 'Build-Build': { + if: buildIfStatement, + }, + 'MyWave-MyStageA-MyStackA-Deploy': { + if: stageAOptions.jobSettings?.if, + }, + 'MyWave-MyStageB-MyStackB-Deploy': { + if: stageBOptions.jobSettings?.if, + }, + }, + }); + }); +}); + + +test('stages in pipeline works with `if`', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + installCommands: ['yarn'], + commands: ['yarn build'], + }), + }); + + const stageA = new Stage(app, 'MyStageA', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + new Stack(stageA, 'MyStackA'); + + const stageAOptions: AddGitHubStageOptions = { + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToA')", + }, + }; + pipeline.addStageWithGitHubOptions(stageA, stageAOptions); + + const stageBOptions: GitHubStageProps = { + env: { account: '12345678901', region: 'us-east-1' }, + jobSettings: { + if: "success() && contains(github.event.issue.labels.*.name, 'deployToB')", + }, + }; + const stageB = new GitHubStage(app, 'MyStageB', stageBOptions); + + new Stack(stageB, 'MyStackB'); + + pipeline.addStage(stageB); + + app.synth(); + + const workflowFileContents = readFileSync(pipeline.workflowPath, 'utf-8'); + expect(workflowFileContents).toMatchSnapshot(); + + const yaml = YAML.parse(workflowFileContents); + expect(yaml).toMatchObject({ + jobs: { + 'MyStageA-MyStackA-Deploy': { + if: stageAOptions.jobSettings?.if, + }, + 'MyStageB-MyStackB-Deploy': { + if: stageBOptions.jobSettings?.if, + }, + }, + }); }); });