diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/source-action.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/source-action.ts index dd9666582910e..3acbe547109f7 100644 --- a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/source-action.ts +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/source-action.ts @@ -1,9 +1,12 @@ import { Construct } from 'constructs'; +import { Trigger } from './trigger'; import * as codepipeline from '../../../aws-codepipeline'; import * as iam from '../../../aws-iam'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +const ACTION_PROVIDER = 'CodeStarSourceConnection'; + /** * The CodePipeline variables emitted by CodeStar source Action. */ @@ -83,8 +86,23 @@ export interface CodeStarConnectionsSourceActionProps extends codepipeline.Commo * * @default true * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html + * + * @deprecated - use `trigger` instead. */ readonly triggerOnPush?: boolean; + + /** + * The trigger configuration for this action. + * + * In a V1 pipeline, only `Trigger.ALL` or `Trigger.NONE` may be used. + * + * In a V2 pipeline, a trigger may have up to three of each type of filter + * (Pull Request or Push) or may trigger on every change to the default branch + * if no filters are provided. + * + * @default - The pipeline is triggered on all changes to the default branch. + */ + readonly trigger?: Trigger; } /** @@ -107,7 +125,7 @@ export class CodeStarConnectionsSourceAction extends Action { ...props, category: codepipeline.ActionCategory.SOURCE, owner: 'AWS', // because props also has a (different!) owner property! - provider: 'CodeStarSourceConnection', + provider: ACTION_PROVIDER, artifactBounds: sourceArtifactBounds(), outputs: [props.output], }); @@ -158,8 +176,41 @@ export class CodeStarConnectionsSourceAction extends Action { OutputArtifactFormat: this.props.codeBuildCloneOutput === true ? 'CODEBUILD_CLONE_REF' : undefined, - DetectChanges: this.props.triggerOnPush, + // For a v2 pipeline, if `trigger` is set this configuration property is disabled + // and the trigger setting is used. For a v1 pipeline, `trigger` can still be used to + // turn on/off `DetectChanges`. + // + // __attachActionToPipeline() will update this value so that it is handled correctly depending + // on the pipeline version. + DetectChanges: this.props.trigger ? this.renderDetectChanges(this.props.trigger) : this.props.triggerOnPush, }, }; } + + private renderTriggerGitConfiguration(trigger: Trigger): codepipeline.CfnPipeline.PipelineTriggerDeclarationProperty { + const providerType = ACTION_PROVIDER; + const push: codepipeline.CfnPipeline.GitPushFilterProperty[] = trigger._filters.map((_filter) => + _filter._push).filter(item => item) as codepipeline.CfnPipeline.GitPushFilterProperty[]; + + const pullRequest: codepipeline.CfnPipeline.GitPullRequestFilterProperty[] = trigger._filters.map((_filter) => + _filter._pullRequest).filter(item => item) as codepipeline.CfnPipeline.GitPullRequestFilterProperty[]; + + return (push.length === 0 && pullRequest.length === 0) ? { + providerType, + } : { + providerType, + gitConfiguration: { + push: push.length > 0 ? push : undefined, + pullRequest: pullRequest.length > 0 ? pullRequest : undefined, + sourceActionName: this.actionProperties.actionName, + }, + }; + } + + private renderDetectChanges(trigger: Trigger): boolean | codepipeline.CfnPipeline.PipelineTriggerDeclarationProperty { + // No trigger or change detection + if (!trigger._enabled) return false; + + return this.renderTriggerGitConfiguration(trigger); + } } diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/trigger.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/trigger.ts new file mode 100644 index 0000000000000..25c0dfe25870e --- /dev/null +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/codestar-connections/trigger.ts @@ -0,0 +1,247 @@ +import { CfnPipeline } from '../../../aws-codepipeline/lib'; + +/** + * The patterns used for filtering criteria. + */ +export interface FilterPattern { + /** + * The excludes patterns to use. If any pattern are included in both includes and excludes, + * excludes take precedence. + * + * @default - No patterns are excluded in this filter. + */ + readonly excludes?: string[]; + + /** + * The includes patterns to use. If any pattern are included in both includes and excludes, + * excludes take precedence. + * + * @default - No patterns are included in this filter. + */ + readonly includes?: string[]; +} + +/** + * Filtering options for filtering on a branch. + */ +export interface BranchFilterOptions { + /** + * The list of branches to filter on. + */ + readonly branches: FilterPattern; + /** + * The list of filepaths to filter on. + * + * @default - No filtering for filepaths. + */ + readonly filePaths?: FilterPattern; +} + +/** + * Filtering options for pull requests + */ +export interface PullRequestFilterOptions extends BranchFilterOptions { } + +/** + * Filtering options for filtering on a tag, + */ +export interface TagFilterOptions extends FilterPattern { } + +enum Events { + OPENED = 'OPENED', + UPDATED = 'UPDATED', + CLOSED = 'CLOSED', +}; + +/** + * Adds a filter to the trigger. + */ +export class Filter { + /** + * Triggers on all pull request events. These include: OPENED, UPDATED, and CLOSED. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestEvents(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.OPENED, Events.UPDATED, Events.CLOSED], filter); + } + + /** + * Triggers on OPENED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestOpened(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.OPENED], filter); + } + + /** + * Triggers on UPDATED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestUpdated(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.UPDATED], filter); + } + + /** + * Triggers on CLOSED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestClosed(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.CLOSED], filter); + } + + /** + * Triggers on OPENED or UPDATED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestOpenedOrUpdated(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.OPENED, Events.UPDATED], filter); + } + + /** + * Triggers on OPENED or CLOSED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestOpenedOrClosed(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.OPENED, Events.CLOSED], filter); + } + + /** + * Triggers on UPDATED or CLOSED pull request events. + * @param filter The filters to use to limit which pull requests are included in the trigger + */ + public static pullRequestUpdatedOrClosed(filter: PullRequestFilterOptions) { + return Filter._pullRequest([Events.UPDATED, Events.CLOSED], filter); + } + + /** + * Trigger on push events. + * @param filter The filters to use to limit which push events are included in the trigger + */ + public static push(filter: PushFilter) { + return new Filter(undefined, { tags: filter._tags, branches: filter._branches, filePaths: filter._filePaths }); + } + + private static _pullRequest(events: Events[], filter: PullRequestFilterOptions) { + mustContainValue('Functions filtering on a pull request', filter.branches, ' on the \'branches\' field'); + return new Filter({ events, branches: filter.branches, filePaths: filter.filePaths }, undefined); + } + + /** + * @internal + */ + public readonly _pullRequest?: CfnPipeline.GitPullRequestFilterProperty; + + /** + * @internal + */ + public readonly _push?: CfnPipeline.GitPushFilterProperty; + + constructor( + pullRequest?: CfnPipeline.GitPullRequestFilterProperty, + push?: CfnPipeline.GitPushFilterProperty, + ) { + this._pullRequest = pullRequest; + this._push = push; + } +} + +/** + * Represents a CodePipeline V2 Pipeline trigger. Each trigger may include filters to limit the + * circumstances in which the pipeline will trigger. + */ +export class Trigger { + /** + * Trigger on all code pushes to the default branch. + */ + public static readonly ENABLED = new Trigger(true); + + /** + * Disables triggers for the pipeline. + */ + public static readonly DISABLED = new Trigger(false); + + /** + * Enables a trigger for the pipeline, filtering for specific events. + * Requires at least one filter. + * @param filters Additional filters for this trigger + */ + public static withFilters(filter: Filter, ...filters: Filter[]) { + const trigger = new Trigger(true); + trigger._filters.push(filter, ...filters); + return trigger; + } + + /** + * @internal + */ + public _filters: Filter[] = []; + + /** + * @internal + */ + public _enabled: boolean; + + constructor(enabled: boolean) { + this._enabled = enabled; + } +} + +/** + * Filters specific to push triggers. + */ +export class PushFilter { + /** + * Filter on tags + * @param options The filtering options for tags + */ + public static onTags(options: TagFilterOptions) { + mustContainValue('PushFilter.onTags()', options); + return new PushFilter(options); + } + + /** + * Filter on branches + * @param options The filtering options for branches + */ + public static onBranches(options: BranchFilterOptions) { + mustContainValue('PushFilter.onBranches()', options.branches, ' on the \'branches\' field'); + return new PushFilter(undefined, options.branches, options.filePaths); + } + + /** + * @internal + */ + public readonly _tags?: CfnPipeline.GitTagFilterCriteriaProperty; + + /** + * @internal + */ + public readonly _branches?: CfnPipeline.GitBranchFilterCriteriaProperty; + + /** + * @internal + */ + public readonly _filePaths?: CfnPipeline.GitFilePathFilterCriteriaProperty; + + constructor( + tags?: CfnPipeline.GitTagFilterCriteriaProperty, + branches?: CfnPipeline.GitBranchFilterCriteriaProperty, + filePaths?: CfnPipeline.GitFilePathFilterCriteriaProperty, + ) { + this._tags = maybeUndefined(tags); + this._branches = maybeUndefined(branches); + this._filePaths = maybeUndefined(filePaths); + } +} + +function maybeUndefined(input?: FilterPattern) { + return (input?.excludes || input?.includes) ? + (input?.excludes?.length == 0 && input.includes?.length == 0 ? undefined : input) : + undefined; +} + +function mustContainValue(type: string, input?: FilterPattern, additionalDetails?: string) { + if (!maybeUndefined(input)) { + throw new Error(`${type} must contain at least one 'includes' or 'excludes' pattern${additionalDetails ?? ''}.`); + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/index.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/index.ts index e82e34232e931..f79a516236b0c 100644 --- a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/index.ts +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/index.ts @@ -1,6 +1,7 @@ export * from './alexa-ask/deploy-action'; export * from './bitbucket/source-action'; export * from './codestar-connections/source-action'; +export * from './codestar-connections/trigger'; export * from './cloudformation'; export * from './codebuild/build-action'; export * from './codecommit/source-action'; diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts index a985871e289a4..8b31ab7e8f052 100644 --- a/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts @@ -146,6 +146,242 @@ describe('CodeStar Connections source Action', () => { }, ], }); + }); + + test('setting trigger enabled reflected in the configuration and is fine for v1', () => { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.ENABLED, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Triggers': Match.absent(), + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [ + { + 'Name': 'BitBucket', + 'ActionTypeId': { + 'Owner': 'AWS', + 'Provider': 'CodeStarSourceConnection', + }, + 'Configuration': { + 'ConnectionArn': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + 'FullRepositoryId': 'aws/aws-cdk', + 'BranchName': 'master', + 'DetectChanges': true, + }, + }, + ], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + }, + ], + }, + ], + }); + }); + + test('setting trigger enabled reflected in the configuration for v2', () => { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.ENABLED, + pipelineType: codepipeline.PipelineType.V2, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [ + { + 'Name': 'BitBucket', + 'ActionTypeId': { + 'Owner': 'AWS', + 'Provider': 'CodeStarSourceConnection', + }, + 'Configuration': { + 'ConnectionArn': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + 'FullRepositoryId': 'aws/aws-cdk', + 'BranchName': 'master', + 'DetectChanges': true, + }, + }, + ], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + }, + ], + }, + ], + 'Triggers': [ + { + 'ProviderType': 'CodeStarSourceConnection', + 'GitConfiguration': { + 'SourceActionName': 'BitBucket', + }, + }, + ], + }); + }); + + test('setting trigger disabled reflects in the configuration for v1', () => { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.DISABLED, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [ + { + 'Name': 'BitBucket', + 'ActionTypeId': { + 'Owner': 'AWS', + 'Provider': 'CodeStarSourceConnection', + }, + 'Configuration': { + 'ConnectionArn': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + 'FullRepositoryId': 'aws/aws-cdk', + 'BranchName': 'master', + 'DetectChanges': false, + }, + }, + ], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + }, + ], + }, + ], + 'Triggers': Match.absent(), + }); + }); + + test('setting trigger disabled reflects in the configuration for v2', () => { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.DISABLED, + pipelineType: codepipeline.PipelineType.V2, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [ + { + 'Name': 'BitBucket', + 'ActionTypeId': { + 'Owner': 'AWS', + 'Provider': 'CodeStarSourceConnection', + }, + 'Configuration': { + 'ConnectionArn': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + 'FullRepositoryId': 'aws/aws-cdk', + 'BranchName': 'master', + 'DetectChanges': false, + }, + }, + ], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + }, + ], + }, + ], + 'Triggers': Match.absent(), + }); + }); + + test('setting trigger with filters reflects in the configuration', () => { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.withFilters(cpactions.Filter.pullRequestClosed({ branches: { includes: ['main'] } })), + pipelineType: codepipeline.PipelineType.V2, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [ + { + 'Name': 'BitBucket', + 'ActionTypeId': { + 'Owner': 'AWS', + 'Provider': 'CodeStarSourceConnection', + }, + 'Configuration': { + 'ConnectionArn': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + 'FullRepositoryId': 'aws/aws-cdk', + 'BranchName': 'master', + 'DetectChanges': true, + }, + }, + ], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + }, + ], + }, + ], + 'Triggers': [ + { + 'ProviderType': 'CodeStarSourceConnection', + 'GitConfiguration': { + 'SourceActionName': 'BitBucket', + 'PullRequest': [ + { + 'Branches': { + 'Includes': ['main'], + }, + 'Events': ['CLOSED'], + }, + ], + }, + }, + ], + }); + }); + + test('v1 pipeline throws error when triggers are included with filters', () => { + const stack = new Stack(); + + expect(() => + createBitBucketAndCodeBuildPipeline(stack, { + trigger: cpactions.Trigger.withFilters(cpactions.Filter.pullRequestClosed({ branches: { includes: ['main'] } })), + pipelineName: 'FailedV1Pipeline', + }), + ).toThrow('Invalid configuration for FailedV1Pipeline. Filters may only be set if PipelineType is set to V2.'); }); @@ -264,8 +500,13 @@ describe('CodeStar Connections source Action', () => { }); }); +interface TestPipelineProps extends Partial { + pipelineType?: codepipeline.PipelineType; + pipelineName?: string; +} + function createBitBucketAndCodeBuildPipeline( - stack: Stack, props: Partial = {}, + stack: Stack, props: TestPipelineProps = {}, pipelineType?: codepipeline.PipelineType, ): codepipeline.Pipeline { const sourceOutput = new codepipeline.Artifact(); const sourceAction = new cpactions.CodeStarConnectionsSourceAction({ @@ -278,6 +519,8 @@ function createBitBucketAndCodeBuildPipeline( }); return new codepipeline.Pipeline(stack, 'Pipeline', { + pipelineType: props.pipelineType, + pipelineName: props.pipelineName, stages: [ { stageName: 'Source', diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/trigger.test.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/trigger.test.ts new file mode 100644 index 0000000000000..9b0a6d269b3ef --- /dev/null +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/test/codestar-connections/trigger.test.ts @@ -0,0 +1,236 @@ +import { Filter, PushFilter, Trigger } from '../../lib/codestar-connections/trigger'; + +describe('CodeStarSourceConnections Trigger Tests', () => { + describe('Filter tests', () => { + describe('Filter on push', () => { + test('onTags returns expected filters when both includes and excludes are provided', () => { + const filter = Filter.push(PushFilter.onTags({ + includes: ['tag1', 'tag2'], + excludes: ['notThisTag', 'i-dont-know-her'], + })); + + expect(filter._push?.tags).toEqual({ includes: ['tag1', 'tag2'], excludes: ['notThisTag', 'i-dont-know-her'] }); + expect(filter._push?.branches).toBeUndefined(); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onTags return expected filters when only includes is provided', () => { + const filter = Filter.push(PushFilter.onTags({ + includes: ['tag1', 'tag2'], + })); + + expect(filter._push?.tags).toEqual({ includes: ['tag1', 'tag2'] }); + expect(filter._push?.branches).toBeUndefined(); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onTags returns expected filters when only excludes are provided', () => { + const filter = Filter.push(PushFilter.onTags({ + excludes: ['notThisTag', 'i-dont-know-her'], + })); + + expect(filter._push?.tags).toEqual({ excludes: ['notThisTag', 'i-dont-know-her'] }); + expect(filter._push?.branches).toBeUndefined(); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onTags returns no filters when none are provided', () => { + expect(() => + Filter.push(PushFilter.onTags({})), + ).toThrow('PushFilter.onTags() must contain at least one \'includes\' or \'excludes\' pattern.'); + }); + + test('onBranches returns expected filters when both includes and excludes are provided for branch filters', () => { + const filter = Filter.push(PushFilter.onBranches({ branches: { includes: ['main'], excludes: ['release', 'test', 'idk-anything-else'] } })); + + expect(filter._push?.tags).toBeUndefined(); + expect(filter._push?.branches).toEqual({ includes: ['main'], excludes: ['release', 'test', 'idk-anything-else'] }); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onBranches returns expected filters when includes filter is provided for branch filters', () => { + const filter = Filter.push(PushFilter.onBranches({ branches: { includes: ['main'] } })); + + expect(filter._push?.tags).toBeUndefined(); + expect(filter._push?.branches).toEqual({ includes: ['main'] }); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onBranches returns expected filters when excludes filter is provided for branch filters', () => { + const filter = Filter.push(PushFilter.onBranches({ branches: { excludes: ['release', 'test', 'idk-anything-else'] } })); + + expect(filter._push?.tags).toBeUndefined(); + expect(filter._push?.branches).toEqual({ excludes: ['release', 'test', 'idk-anything-else'] }); + expect(filter._push?.filePaths).toBeUndefined(); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onBranches returns expected filters when filePath are included', () => { + const filter = Filter.push(PushFilter.onBranches({ branches: { excludes: ['release', 'test', 'idk-anything-else'] }, filePaths: { includes: ['src/'] } })); + + expect(filter._push?.tags).toBeUndefined(); + expect(filter._push?.branches).toEqual({ excludes: ['release', 'test', 'idk-anything-else'] }); + expect(filter._push?.filePaths).toEqual({ includes: ['src/'] }); + expect(filter._pullRequest).toBeUndefined(); + }); + + test('onBranches throws error when none are provided for branch filters', () => { + + expect(() => + Filter.push(PushFilter.onBranches({ branches: {} })), + ).toThrow('PushFilter.onBranches() must contain at least one \'includes\' or \'excludes\' pattern on the \'branches\' field.'); + }); + }); + + describe('Filter on pull request', () => { + test('pullRequestEvents returns expected output when both includes and excludes are provided for branch filters', () => { + const filter = Filter.pullRequestEvents({ branches: { includes: ['mainV1, mainV2'], excludes: ['random-other-branch'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['mainV1, mainV2'], excludes: ['random-other-branch'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'UPDATED', 'CLOSED']); + }); + + test('pullRequestEvents returns expected output when only includes filter is provided for branch filters', () => { + const filter = Filter.pullRequestEvents({ branches: { includes: ['mainV1, mainV2'], excludes: ['random-other-branch'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['mainV1, mainV2'], excludes: ['random-other-branch'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'UPDATED', 'CLOSED']); + }); + + test('pullRequestEvents returns expected output when only excludes filter is provided for branch filters', () => { + const filter = Filter.pullRequestEvents({ branches: { excludes: ['random-other-branch'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ excludes: ['random-other-branch'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'UPDATED', 'CLOSED']); + }); + + test('pullRequestEvents returns expected output when filepath is included', () => { + const filter = Filter.pullRequestEvents({ branches: { excludes: ['random-other-branch'] }, filePaths: { includes: ['test/'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ excludes: ['random-other-branch'] }); + expect(filter._pullRequest?.filePaths).toEqual({ includes: ['test/'] }); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'UPDATED', 'CLOSED']); + }); + + test('pullRequestEvents throws when none are provided for branch filters', () => { + expect(() => + Filter.pullRequestEvents({ branches: {} }), + ).toThrow('Functions filtering on a pull request must contain at least one \'includes\' or \'excludes\' pattern on the \'branches\' field.'); + }); + + test('pullRequestOpened returns expected events', () => { + const filter = Filter.pullRequestOpened({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED']); + }); + + test('pullRequestUpdated returns correct events', () => { + const filter = Filter.pullRequestUpdated({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['UPDATED']); + }); + + test('pullRequestClosed returns correct events', () => { + const filter = Filter.pullRequestClosed({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['CLOSED']); + }); + + test('pullRequestOpenedOrUpdated returns correct events', () => { + const filter = Filter.pullRequestOpenedOrUpdated({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'UPDATED']); + }); + + test('pullRequestOpenedOrClosed returns correct events', () => { + const filter = Filter.pullRequestOpenedOrClosed({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['OPENED', 'CLOSED']); + }); + + test('pullRequestUpdatedOrClosed returns correct events', () => { + const filter = Filter.pullRequestUpdatedOrClosed({ branches: { includes: ['main'] } }); + + expect(filter._push).toBeUndefined(); + expect(filter._pullRequest?.branches).toEqual({ includes: ['main'] }); + expect(filter._pullRequest?.filePaths).toBeUndefined(); + expect(filter._pullRequest?.events).toEqual(['UPDATED', 'CLOSED']); + }); + }); + }); + + describe('Trigger function tests', () => { + test('Trigger.ENABLED enables triggers with no filters', () => { + expect(Trigger.ENABLED).toEqual({ _enabled: true, _filters: [] }); + }); + + test('Trigger.DISABLED disables triggers and contains no filters', () => { + expect(Trigger.DISABLED).toEqual({ _enabled: false, _filters: [] }); + }); + + test('Trigger.withFilters() enables triggers and adds single branch filter', () => { + expect(Trigger.withFilters(Filter.pullRequestEvents({ branches: { includes: ['main'] } }))).toEqual({ + _enabled: true, + _filters: [{ + _pullRequest: { + branches: { + includes: ['main'], + }, + events: ['OPENED', 'UPDATED', 'CLOSED'], + }, + }], + }); + }); + + test('Trigger.withFilters() enables triggers and adds multiple types of filters', () => { + expect(Trigger.withFilters( + Filter.pullRequestEvents({ branches: { includes: ['main'] } }), + Filter.push(PushFilter.onTags({ includes: ['this-one'] }), + ))).toEqual({ + _enabled: true, + _filters: [{ + _pullRequest: { + branches: { + includes: ['main'], + }, + events: ['OPENED', 'UPDATED', 'CLOSED'], + }, + }, + { + _push: { + tags: { + includes: ['this-one'], + }, + }, + }], + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-codepipeline/README.md b/packages/aws-cdk-lib/aws-codepipeline/README.md index 09a0b90bc1b6a..22bd175ebf392 100644 --- a/packages/aws-cdk-lib/aws-codepipeline/README.md +++ b/packages/aws-cdk-lib/aws-codepipeline/README.md @@ -1,6 +1,5 @@ # AWS CodePipeline Construct Library - ## Pipeline To construct an empty Pipeline: @@ -73,7 +72,8 @@ Or append a Stage to an existing Pipeline: declare const pipeline: codepipeline.Pipeline; const sourceStage = pipeline.addStage({ stageName: 'Source', - actions: [ // optional property + actions: [ + // optional property // see below... ], }); @@ -93,7 +93,7 @@ const someStage = pipeline.addStage({ // note: you can only specify one of the below properties rightBefore: anotherStage, justAfter: yetAnotherStage, - } + }, }); ``` @@ -107,7 +107,7 @@ const someStage = pipeline.addStage({ stageName: 'SomeStage', transitionToEnabled: false, transitionDisabledReason: 'Manual transition only', // optional reason -}) +}); ``` This is useful if you don't want every executions of the pipeline to flow into @@ -134,46 +134,57 @@ To make your own custom CodePipeline Action requires registering the action prov ```ts // Make a custom CodePipeline Action -new codepipeline.CustomActionRegistration(this, 'GenericGitSourceProviderResource', { - category: codepipeline.ActionCategory.SOURCE, - artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 1 }, - provider: 'GenericGitSource', - version: '1', - entityUrl: 'https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-create-custom-action.html', - executionUrl: 'https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-create-custom-action.html', - actionProperties: [ - { - name: 'Branch', - required: true, - key: false, - secret: false, - queryable: false, - description: 'Git branch to pull', - type: 'String', - }, - { - name: 'GitUrl', - required: true, - key: false, - secret: false, - queryable: false, - description: 'SSH git clone URL', - type: 'String', +new codepipeline.CustomActionRegistration( + this, + 'GenericGitSourceProviderResource', + { + category: codepipeline.ActionCategory.SOURCE, + artifactBounds: { + minInputs: 0, + maxInputs: 0, + minOutputs: 1, + maxOutputs: 1, }, - ], -}); + provider: 'GenericGitSource', + version: '1', + entityUrl: + 'https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-create-custom-action.html', + executionUrl: + 'https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-create-custom-action.html', + actionProperties: [ + { + name: 'Branch', + required: true, + key: false, + secret: false, + queryable: false, + description: 'Git branch to pull', + type: 'String', + }, + { + name: 'GitUrl', + required: true, + key: false, + secret: false, + queryable: false, + description: 'SSH git clone URL', + type: 'String', + }, + ], + } +); ``` ## Cross-account CodePipelines -> Cross-account Pipeline actions require that the Pipeline has *not* been +> Cross-account Pipeline actions require that the Pipeline has _not_ been > created with `crossAccountKeys: false`. Most pipeline Actions accept an AWS resource object to operate on. For example: -* `S3DeployAction` accepts an `s3.IBucket`. -* `CodeBuildAction` accepts a `codebuild.IProject`. -* etc. +- `S3DeployAction` accepts an `s3.IBucket`. +- `CodeBuildAction` accepts a `codebuild.IProject`. +- etc. These resources can be either newly defined (`new s3.Bucket(...)`) or imported (`s3.Bucket.fromBucketAttributes(...)`) and identify the resource that should @@ -187,15 +198,17 @@ different account: // Deploy an imported S3 bucket from a different account declare const stage: codepipeline.IStage; declare const input: codepipeline.Artifact; -stage.addAction(new codepipeline_actions.S3DeployAction({ - bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { - account: '123456789012', +stage.addAction( + new codepipeline_actions.S3DeployAction({ + bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { + account: '123456789012', + // ... + }), + input: input, + actionName: 's3-deploy-action', // ... - }), - input: input, - actionName: 's3-deploy-action', - // ... -})); + }) +); ``` Actions that don't accept a resource object accept an explicit `account` parameter: @@ -204,14 +217,16 @@ Actions that don't accept a resource object accept an explicit `account` paramet // Actions that don't accept a resource objet accept an explicit `account` parameter declare const stage: codepipeline.IStage; declare const templatePath: codepipeline.ArtifactPath; -stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - account: '123456789012', - templatePath, - adminPermissions: false, - stackName: Stack.of(this).stackName, - actionName: 'cloudformation-create-update', - // ... -})); +stage.addAction( + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + account: '123456789012', + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', + // ... + }) +); ``` The `Pipeline` construct automatically defines an **IAM Role** for you in the @@ -228,35 +243,39 @@ account the role belongs to: // Explicitly pass in a `role` when creating an action. declare const stage: codepipeline.IStage; declare const templatePath: codepipeline.ArtifactPath; -stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - templatePath, - adminPermissions: false, - stackName: Stack.of(this).stackName, - actionName: 'cloudformation-create-update', - // ... - role: iam.Role.fromRoleArn(this, 'ActionRole', '...'), -})); +stage.addAction( + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', + // ... + role: iam.Role.fromRoleArn(this, 'ActionRole', '...'), + }) +); ``` ## Cross-region CodePipelines Similar to how you set up a cross-account Action, the AWS resource object you -pass to actions can also be in different *Regions*. For example, the +pass to actions can also be in different _Regions_. For example, the following Action deploys to an imported S3 bucket from a different Region: ```ts // Deploy to an imported S3 bucket from a different Region. declare const stage: codepipeline.IStage; declare const input: codepipeline.Artifact; -stage.addAction(new codepipeline_actions.S3DeployAction({ - bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { - region: 'us-west-1', +stage.addAction( + new codepipeline_actions.S3DeployAction({ + bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { + region: 'us-west-1', + // ... + }), + input: input, + actionName: 's3-deploy-action', // ... - }), - input: input, - actionName: 's3-deploy-action', - // ... -})); + }) +); ``` Actions that don't take an AWS resource will accept an explicit `region` @@ -266,14 +285,16 @@ parameter: // Actions that don't take an AWS resource will accept an explicit `region` parameter. declare const stage: codepipeline.IStage; declare const templatePath: codepipeline.ArtifactPath; -stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - templatePath, - adminPermissions: false, - stackName: Stack.of(this).stackName, - actionName: 'cloudformation-create-update', - // ... - region: 'us-west-1', -})); +stage.addAction( + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', + // ... + region: 'us-west-1', + }) +); ``` The `Pipeline` construct automatically defines a **replication bucket** for @@ -293,13 +314,19 @@ const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { crossRegionReplicationBuckets: { // note that a physical name of the replication Bucket must be known at synthesis time - 'us-west-1': s3.Bucket.fromBucketAttributes(this, 'UsWest1ReplicationBucket', { - bucketName: 'my-us-west-1-replication-bucket', - // optional KMS key - encryptionKey: kms.Key.fromKeyArn(this, 'UsWest1ReplicationKey', - 'arn:aws:kms:us-west-1:123456789012:key/1234-5678-9012' - ), - }), + 'us-west-1': s3.Bucket.fromBucketAttributes( + this, + 'UsWest1ReplicationBucket', + { + bucketName: 'my-us-west-1-replication-bucket', + // optional KMS key + encryptionKey: kms.Key.fromKeyArn( + this, + 'UsWest1ReplicationKey', + 'arn:aws:kms:us-west-1:123456789012:key/1234-5678-9012' + ), + } + ), }, }); ``` @@ -337,7 +364,7 @@ new codepipeline.Pipeline(replicationStack, 'Pipeline', { When trying to encrypt it (and note that if any of the cross-region actions happen to be cross-account as well, -the bucket *has to* be encrypted - otherwise the pipeline will fail at runtime), +the bucket _has to_ be encrypted - otherwise the pipeline will fail at runtime), you cannot use a key directly - KMS keys don't have physical names, and so you can't reference them across environments. @@ -554,7 +581,7 @@ declare const myPipeline: codepipeline.Pipeline; declare const myStage: codepipeline.IStage; declare const myAction: codepipeline.Action; declare const target: events.IRuleTarget; -myPipeline.onStateChange('MyPipelineStateChange', { target: target } ); +myPipeline.onStateChange('MyPipelineStateChange', { target: target }); myStage.onStateChange('MyStageStateChange', target); myAction.onStateChange('MyActionStateChange', target); ``` @@ -574,7 +601,10 @@ const target = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { }); declare const pipeline: codepipeline.Pipeline; -const rule = pipeline.notifyOnExecutionStateChange('NotifyOnExecutionStateChange', target); +const rule = pipeline.notifyOnExecutionStateChange( + 'NotifyOnExecutionStateChange', + target +); ``` ## Trigger @@ -582,169 +612,6 @@ const rule = pipeline.notifyOnExecutionStateChange('NotifyOnExecutionStateChange To trigger a pipeline with Git tags or branches, specify the `triggers` property. The triggers can only be used with pipeline type V2. -### Push filter - -Pipelines can be started based on push events. You can specify the `pushFilter` property to -filter the push events. The `pushFilter` can specify Git tags. - -In the case of Git tags, your pipeline starts when a Git tag is pushed. -You can filter with glob patterns. The `tagsExcludes` takes priority over the `tagsIncludes`. - -```ts -declare const sourceAction: codepipeline_actions.CodeStarConnectionsSourceAction; -declare const buildAction: codepipeline_actions.CodeBuildAction; - -new codepipeline.Pipeline(this, 'Pipeline', { - pipelineType: codepipeline.PipelineType.V2, - stages: [ - { - stageName: 'Source', - actions: [sourceAction], - }, - { - stageName: 'Build', - actions: [buildAction], - }, - ], - triggers: [{ - providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION, - gitConfiguration: { - sourceAction, - pushFilter: [{ - tagsExcludes: ['exclude1', 'exclude2'], - tagsIncludes: ['include*'], - }], - }, - }], -}); -``` - -### Pull request filter - -Pipelines can be started based on pull request events. You can specify the `pullRequestFilter` property to -filter the pull request events. The `pullRequestFilter` can specify branches, file paths, and event types. - -In the case of branches, your pipeline starts when a pull request event occurs on the specified branches. -You can filter with glob patterns. The `branchesExcludes` takes priority over the `branchesIncludes`. - -```ts -declare const sourceAction: codepipeline_actions.CodeStarConnectionsSourceAction; -declare const buildAction: codepipeline_actions.CodeBuildAction; - -new codepipeline.Pipeline(this, 'Pipeline', { - pipelineType: codepipeline.PipelineType.V2, - stages: [ - { - stageName: 'Source', - actions: [sourceAction], - }, - { - stageName: 'Build', - actions: [buildAction], - }, - ], - triggers: [{ - providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION, - gitConfiguration: { - sourceAction, - pullRequestFilter: [{ - branchesExcludes: ['exclude1', 'exclude2'], - branchesIncludes: ['include*'], - }], - }, - }], -}); -``` - -File paths can also be specified along with the branches to start the pipeline. -You can filter with glob patterns. The `filePathsExcludes` takes priority over the `filePathsIncludes`. - -```ts -declare const sourceAction: codepipeline_actions.CodeStarConnectionsSourceAction; -declare const buildAction: codepipeline_actions.CodeBuildAction; - -new codepipeline.Pipeline(this, 'Pipeline', { - pipelineType: codepipeline.PipelineType.V2, - stages: [ - { - stageName: 'Source', - actions: [sourceAction], - }, - { - stageName: 'Build', - actions: [buildAction], - }, - ], - triggers: [{ - providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION, - gitConfiguration: { - sourceAction, - pullRequestFilter: [{ - branchesExcludes: ['exclude1', 'exclude2'], - branchesIncludes: ['include1', 'include2'], - filePathsExcludes: ['/path/to/exclude1', '/path/to/exclude2'], - filePathsIncludes: ['/path/to/include1', '/path/to/include1'], - }], - }, - }], -}); -``` - -To filter types of pull request events for triggers, you can specify the `events` property. - -```ts -declare const sourceAction: codepipeline_actions.CodeStarConnectionsSourceAction; -declare const buildAction: codepipeline_actions.CodeBuildAction; - -new codepipeline.Pipeline(this, 'Pipeline', { - pipelineType: codepipeline.PipelineType.V2, - stages: [ - { - stageName: 'Source', - actions: [sourceAction], - }, - { - stageName: 'Build', - actions: [buildAction], - }, - ], - triggers: [{ - providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION, - gitConfiguration: { - sourceAction, - pullRequestFilter: [{ - branchesExcludes: ['exclude1', 'exclude2'], - branchesIncludes: ['include1', 'include2'], - events: [ - codepipeline.GitPullRequestEvent.OPEN, - codepipeline.GitPullRequestEvent.CLOSED, - ], - }], - }, - }], -}); -``` - -### Append a trigger to an existing pipeline - -You can append a trigger to an existing pipeline: - -```ts -declare const pipeline: codepipeline.Pipeline; -declare const sourceAction: codepipeline_actions.CodeStarConnectionsSourceAction; - -pipeline.addTrigger({ - providerType: codepipeline.ProviderType.CODE_STAR_SOURCE_CONNECTION, - gitConfiguration: { - sourceAction, - pushFilter: [{ - tagsExcludes: ['exclude1', 'exclude2'], - tagsIncludes: ['include*'], - }], - }, -}); -``` - ## Execution mode To control the concurrency behavior when multiple executions of a pipeline are started, you can use the `executionMode` property. diff --git a/packages/aws-cdk-lib/aws-codepipeline/lib/pipeline.ts b/packages/aws-cdk-lib/aws-codepipeline/lib/pipeline.ts index 57465e8d5093b..08a319b64a9c5 100644 --- a/packages/aws-cdk-lib/aws-codepipeline/lib/pipeline.ts +++ b/packages/aws-cdk-lib/aws-codepipeline/lib/pipeline.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import { ActionCategory, + ActionConfig, IAction, IPipeline, IStage, @@ -255,6 +256,8 @@ export interface PipelineProps { * You can always add more triggers later by calling `Pipeline#addTrigger`. * * @default - No triggers + * + * @deprecated - Use other instead */ readonly triggers?: TriggerProps[]; @@ -466,7 +469,8 @@ export class Pipeline extends PipelineBase { private readonly codePipeline: CfnPipeline; private readonly pipelineType: PipelineType; private readonly variables = new Array(); - private readonly triggers = new Array(); + private readonly triggers_deprecated = new Array(); + private readonly triggers = new Array(); constructor(scope: Construct, id: string, props: PipelineProps = {}) { super(scope, id, { @@ -642,17 +646,19 @@ export class Pipeline extends PipelineBase { * * @param props Trigger property to add to this Pipeline * @returns the newly created trigger + * + * @deprecated - Use triggers in CodeStarSourceConnections Source Action */ public addTrigger(props: TriggerProps): Trigger { const trigger = new Trigger(props); const actionName = props.gitConfiguration?.sourceAction.actionProperties.actionName; // check for duplicate source actions for triggers - if (actionName !== undefined && this.triggers.find(t => t.sourceAction?.actionProperties.actionName === actionName)) { + if (actionName !== undefined && this.triggers_deprecated.find(t => t.sourceAction?.actionProperties.actionName === actionName)) { throw new Error(`Trigger with duplicate source action '${actionName}' added to the Pipeline`); } - this.triggers.push(trigger); + this.triggers_deprecated.push(trigger); return trigger; } @@ -719,18 +725,58 @@ export class Pipeline extends PipelineBase { bucket: crossRegionInfo.artifactBucket, }); + // Do we have a trigger configuration, boolean, or undefined? + const updatedActionConfig = this.processTriggersInActionConfig(actionConfig, action.actionProperties.actionName); + return new FullActionDescriptor({ // must be 'action', not 'richAction', // as those are returned by the IStage.actions property, // and it's important customers of Pipeline get the same instance // back as they added to the pipeline action, - actionConfig, + actionConfig: updatedActionConfig, actionRole, actionRegion: crossRegionInfo.region, }); } + private processTriggersInActionConfig(actionConfig: ActionConfig, actionName: string): ActionConfig { + // No updates needed if trigger isn't set. This is the boolean or undefined use case for DetectChanges + if (!this.isTriggerConfiguration(actionConfig)) return actionConfig; + + const trigger: CfnPipeline.PipelineTriggerDeclarationProperty = actionConfig.configuration.DetectChanges; + const _actionConfig = actionConfig; + + // The trigger class in CodeStarConnectionsSourceAction can be used without filters for v1. + // If used, `DetectChanges` is set to true but no triggers are actually set. + // If `Trigger.NONE` was used, `DetectChanges` was already set to `false` so we only need to handle + // for `Trigger.ALL` here. + if (this.pipelineType === PipelineType.V1) { + _actionConfig.configuration.DetectChanges = this.setV1DetectChanges(trigger); + return _actionConfig; + } + + // Even though the value of `DetectChanges` is ignored in this case, we'll format it correctly + _actionConfig.configuration.DetectChanges = true; + this.triggers.push({ + providerType: trigger.providerType, + gitConfiguration: trigger.gitConfiguration ?? { sourceActionName: actionName }, + }); + return _actionConfig; + } + + private isTriggerConfiguration(actionConfig: ActionConfig) { + return !!actionConfig.configuration?.DetectChanges?.providerType; + } + + private setV1DetectChanges(trigger: CfnPipeline.PipelineTriggerDeclarationProperty) { + if (trigger.gitConfiguration) { + throw new Error(`Invalid configuration for ${this.physicalName}. Filters may only be set if PipelineType is set to V2.`); + } + + return true; + } + /** * Validate the pipeline structure * @@ -1183,7 +1229,7 @@ export class Pipeline extends PipelineBase { private validateTriggers(): string[] { const errors: string[] = []; - if (this.triggers.length && this.pipelineType !== PipelineType.V2) { + if (this.triggers_deprecated.length && this.pipelineType !== PipelineType.V2) { errors.push('Triggers can only be used with V2 pipelines, `PipelineType.V2` must be specified for `pipelineType`'); } return errors; @@ -1254,7 +1300,9 @@ export class Pipeline extends PipelineBase { } private renderTriggers(): CfnPipeline.PipelineTriggerDeclarationProperty[] { - return this.triggers.map(trigger => trigger._render()); + const triggers_deprecated = this.triggers_deprecated.map(trigger => trigger._render()); + this.triggers.push(...triggers_deprecated); + return this.triggers; } private requireRegion(): string { diff --git a/packages/aws-cdk-lib/aws-codepipeline/lib/trigger.ts b/packages/aws-cdk-lib/aws-codepipeline/lib/trigger.ts index 950eb2b3b04f2..6d1f5b8441019 100644 --- a/packages/aws-cdk-lib/aws-codepipeline/lib/trigger.ts +++ b/packages/aws-cdk-lib/aws-codepipeline/lib/trigger.ts @@ -3,6 +3,8 @@ import { CfnPipeline } from './codepipeline.generated'; /** * Git push filter for trigger. + * + * @deprecated - Use trigger in CodeStarSourceConnection Source Action instead */ export interface GitPushFilter { /** @@ -34,6 +36,8 @@ export interface GitPushFilter { /** * Git pull request filter for trigger. + * + * @deprecated - Use trigger in CodeStarSourceConnection Source Action instead */ export interface GitPullRequestFilter { /** @@ -99,6 +103,8 @@ export interface GitPullRequestFilter { /** * Event for trigger with pull request filter. + * + * @deprecated - Use trigger in CodeStarSourceConnection Source Action instead */ export enum GitPullRequestEvent { /** @@ -119,6 +125,8 @@ export enum GitPullRequestEvent { /** * Git configuration for trigger. + * + * @deprecated - Use trigger in CodeStarSourceConnection Source Action instead */ export interface GitConfiguration { /** @@ -157,6 +165,8 @@ export interface GitConfiguration { /** * Provider type for trigger. + * + * @deprecated - Use trigger in CodeStarSourceConnection Source Action instead */ export enum ProviderType { /** @@ -167,6 +177,7 @@ export enum ProviderType { /** * Properties of trigger. + * @deprecated - Use Triggers in CodeStarSourceConnection Source Action instead */ export interface TriggerProps { /** @@ -186,6 +197,8 @@ export interface TriggerProps { /** * Trigger. + * + * @deprecated - Use Trigger in CodeStarSourceConnection Source Action instead */ export class Trigger { /** diff --git a/packages/aws-cdk-lib/aws-codepipeline/test/triggers.test.ts b/packages/aws-cdk-lib/aws-codepipeline/test/triggers.test.ts index 0e186ea40b41a..c7013bdcf1354 100644 --- a/packages/aws-cdk-lib/aws-codepipeline/test/triggers.test.ts +++ b/packages/aws-cdk-lib/aws-codepipeline/test/triggers.test.ts @@ -1,3 +1,4 @@ +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { IConstruct } from 'constructs'; import { FakeBuildAction } from './fake-build-action'; import { FakeSourceAction } from './fake-source-action'; @@ -28,7 +29,7 @@ describe('triggers', () => { }); }); - test('can specify triggers with tags in pushFilter', () => { + testDeprecated('can specify triggers with tags in pushFilter', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -62,7 +63,7 @@ describe('triggers', () => { }); }); - test('can specify triggers with branches in pullRequestFilter', () => { + testDeprecated('can specify triggers with branches in pullRequestFilter', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -96,7 +97,7 @@ describe('triggers', () => { }); }); - test('can specify triggers with branches and file paths in pullRequestFilter', () => { + testDeprecated('can specify triggers with branches and file paths in pullRequestFilter', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -136,7 +137,7 @@ describe('triggers', () => { }); }); - test('can specify triggers with branches and events in pullRequestFilter', () => { + testDeprecated('can specify triggers with branches and events in pullRequestFilter', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -176,7 +177,7 @@ describe('triggers', () => { }); }); - test('can specify multiple triggers', () => { + testDeprecated('can specify multiple triggers', () => { const sourceArtifact2 = new codepipeline.Artifact(); const sourceAction2 = new CodeStarConnectionsSourceAction({ actionName: 'CodeStarConnectionsSourceAction2', @@ -245,7 +246,7 @@ describe('triggers', () => { }); }); - test('can specify triggers by addTrigger method', () => { + testDeprecated('can specify triggers by addTrigger method', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, }); @@ -278,7 +279,7 @@ describe('triggers', () => { }); }); - test('empty tagsExcludes in pushFilter for trigger is set to undefined', () => { + testDeprecated('empty tagsExcludes in pushFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -312,7 +313,7 @@ describe('triggers', () => { }); }); - test('empty tagsIncludes in pushFilter for trigger is set to undefined', () => { + testDeprecated('empty tagsIncludes in pushFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -346,7 +347,7 @@ describe('triggers', () => { }); }); - test('empty branchesExcludes in pullRequestFilter for trigger is set to undefined', () => { + testDeprecated('empty branchesExcludes in pullRequestFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -380,7 +381,7 @@ describe('triggers', () => { }); }); - test('empty branchesIncludes in pullRequestFilter for trigger is set to undefined', () => { + testDeprecated('empty branchesIncludes in pullRequestFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -414,7 +415,7 @@ describe('triggers', () => { }); }); - test('empty filePathsExcludes in pullRequestFilter for trigger is set to undefined', () => { + testDeprecated('empty filePathsExcludes in pullRequestFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -454,7 +455,7 @@ describe('triggers', () => { }); }); - test('empty filePathsIncludes in pullRequestFilter for trigger is set to undefined', () => { + testDeprecated('empty filePathsIncludes in pullRequestFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -494,7 +495,7 @@ describe('triggers', () => { }); }); - test('undefined events in pullRequestFilter for trigger is set to all events', () => { + testDeprecated('undefined events in pullRequestFilter for trigger is set to all events', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -529,7 +530,7 @@ describe('triggers', () => { }); }); - test('empty events in pullRequestFilter for trigger is set to all events', () => { + testDeprecated('empty events in pullRequestFilter for trigger is set to all events', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -565,7 +566,7 @@ describe('triggers', () => { }); }); - test('throw if length of tagsExcludes in pushFilter is greater than 8', () => { + testDeprecated('throw if length of tagsExcludes in pushFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -583,7 +584,7 @@ describe('triggers', () => { }).toThrow(/maximum length of tagsExcludes in pushFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if length of tagsIncludes in pushFilter is greater than 8', () => { + testDeprecated('throw if length of tagsIncludes in pushFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -601,7 +602,7 @@ describe('triggers', () => { }).toThrow(/maximum length of tagsIncludes in pushFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if length of branchesExcludes in pullRequestFilter is greater than 8', () => { + testDeprecated('throw if length of branchesExcludes in pullRequestFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -619,7 +620,7 @@ describe('triggers', () => { }).toThrow(/maximum length of branchesExcludes in pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if length of branchesIncludes in pullRequestFilter is greater than 8', () => { + testDeprecated('throw if length of branchesIncludes in pullRequestFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -637,7 +638,7 @@ describe('triggers', () => { }).toThrow(/maximum length of branchesIncludes in pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if length of filePathsExcludes in pullRequestFilter is greater than 8', () => { + testDeprecated('throw if length of filePathsExcludes in pullRequestFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -657,7 +658,7 @@ describe('triggers', () => { }).toThrow(/maximum length of filePathsExcludes in pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if length of filePathsIncludes in pullRequestFilter is greater than 8', () => { + testDeprecated('throw if length of filePathsIncludes in pullRequestFilter is greater than 8', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -677,7 +678,7 @@ describe('triggers', () => { }).toThrow(/maximum length of filePathsIncludes in pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction' is 8, got 9/); }); - test('throw if branches is not specified in pullRequestFilter', () => { + testDeprecated('throw if branches is not specified in pullRequestFilter', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -692,7 +693,7 @@ describe('triggers', () => { }).toThrow(/must specify branches in pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction'/); }); - test('can eliminate duplicates events in pullRequestFilter', () => { + testDeprecated('can eliminate duplicates events in pullRequestFilter', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -732,7 +733,7 @@ describe('triggers', () => { }); }); - test('empty pushFilter for trigger is set to undefined', () => { + testDeprecated('empty pushFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -763,7 +764,7 @@ describe('triggers', () => { }); }); - test('empty pullRequestFilter for trigger is set to undefined', () => { + testDeprecated('empty pullRequestFilter for trigger is set to undefined', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -794,7 +795,7 @@ describe('triggers', () => { }); }); - test('throw if length of pushFilter is greater than 3', () => { + testDeprecated('throw if length of pushFilter is greater than 3', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -826,7 +827,7 @@ describe('triggers', () => { }).toThrow(/length of pushFilter for sourceAction with name 'CodeStarConnectionsSourceAction' must be less than or equal to 3, got 4/);; }); - test('throw if length of pullRequestFilter is greater than 3', () => { + testDeprecated('throw if length of pullRequestFilter is greater than 3', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -858,7 +859,7 @@ describe('triggers', () => { }).toThrow(/length of pullRequestFilter for sourceAction with name 'CodeStarConnectionsSourceAction' must be less than or equal to 3, got 4/);; }); - test('throw if both pushFilter and pullRequestFilter are specified', () => { + testDeprecated('throw if both pushFilter and pullRequestFilter are specified', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -884,7 +885,7 @@ describe('triggers', () => { }).toThrow(/cannot specify both pushFilter and pullRequestFilter for the trigger with sourceAction with name 'CodeStarConnectionsSourceAction'/);; }); - test('throw if neither pushFilter nor pullRequestFilter are specified', () => { + testDeprecated('throw if neither pushFilter nor pullRequestFilter are specified', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -898,7 +899,7 @@ describe('triggers', () => { }).toThrow(/must specify either pushFilter or pullRequestFilter for the trigger with sourceAction with name 'CodeStarConnectionsSourceAction'/);; }); - test('throw if both pushFilter and pullRequestFilter are empty arrays', () => { + testDeprecated('throw if both pushFilter and pullRequestFilter are empty arrays', () => { expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, @@ -914,7 +915,7 @@ describe('triggers', () => { }).toThrow(/must specify either pushFilter or pullRequestFilter for the trigger with sourceAction with name 'CodeStarConnectionsSourceAction'/);; }); - test('throw if provider of sourceAction is not \'CodeStarSourceConnection\'', () => { + testDeprecated('throw if provider of sourceAction is not \'CodeStarSourceConnection\'', () => { const fakeAction = new FakeSourceAction({ actionName: 'FakeSource', output: sourceArtifact, @@ -936,7 +937,7 @@ describe('triggers', () => { }).toThrow(/provider for actionProperties in sourceAction with name 'FakeSource' must be 'CodeStarSourceConnection', got 'Fake'/); }); - test('throw if source action with duplicate action name added to the Pipeline', () => { + testDeprecated('throw if source action with duplicate action name added to the Pipeline', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V2, triggers: [{ @@ -964,7 +965,7 @@ describe('triggers', () => { }).toThrow(/Trigger with duplicate source action 'CodeStarConnectionsSourceAction' added to the Pipeline/); }); - test('throw if triggers are specified when pipelineType is not set to V2', () => { + testDeprecated('throw if triggers are specified when pipelineType is not set to V2', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V1, triggers: [{ @@ -988,7 +989,7 @@ describe('triggers', () => { expect(error).toMatch(/Triggers can only be used with V2 pipelines, `PipelineType.V2` must be specified for `pipelineType`/); }); - test('throw if triggers are specified when pipelineType is not set to V2 and addTrigger method is used', () => { + testDeprecated('throw if triggers are specified when pipelineType is not set to V2 and addTrigger method is used', () => { const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineType: codepipeline.PipelineType.V1, });