/
source-action.ts
233 lines (204 loc) · 7.76 KB
/
source-action.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as targets from '@aws-cdk/aws-events-targets';
import * as iam from '@aws-cdk/aws-iam';
import { Names, Stack, Token, TokenComparison } from '@aws-cdk/core';
import { Action } from '../action';
import { sourceArtifactBounds } from '../common';
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';
/**
* How should the CodeCommit Action detect changes.
* This is the type of the {@link CodeCommitSourceAction.trigger} property.
*/
export enum CodeCommitTrigger {
/**
* The Action will never detect changes -
* the Pipeline it's part of will only begin a run when explicitly started.
*/
NONE = 'None',
/**
* CodePipeline will poll the repository to detect changes.
*/
POLL = 'Poll',
/**
* CodePipeline will use CloudWatch Events to be notified of changes.
* This is the default method of detecting changes.
*/
EVENTS = 'Events',
}
/**
* The CodePipeline variables emitted by the CodeCommit source Action.
*/
export interface CodeCommitSourceVariables {
/** The name of the repository this action points to. */
readonly repositoryName: string;
/** The name of the branch this action tracks. */
readonly branchName: string;
/** The date the currently last commit on the tracked branch was authored, in ISO-8601 format. */
readonly authorDate: string;
/** The date the currently last commit on the tracked branch was committed, in ISO-8601 format. */
readonly committerDate: string;
/** The SHA1 hash of the currently last commit on the tracked branch. */
readonly commitId: string;
/** The message of the currently last commit on the tracked branch. */
readonly commitMessage: string;
}
/**
* Construction properties of the {@link CodeCommitSourceAction CodeCommit source CodePipeline Action}.
*/
export interface CodeCommitSourceActionProps extends codepipeline.CommonAwsActionProps {
/**
*
*/
readonly output: codepipeline.Artifact;
/**
* @default 'master'
*/
readonly branch?: string;
/**
* How should CodePipeline detect source changes for this Action.
*
* @default CodeCommitTrigger.EVENTS
*/
readonly trigger?: CodeCommitTrigger;
/**
* The CodeCommit repository.
*/
readonly repository: codecommit.IRepository;
/**
* Role to be used by on commit event rule.
* Used only when trigger value is CodeCommitTrigger.EVENTS.
*
* @default a new role will be created.
*/
readonly eventRole?: iam.IRole;
/**
* Whether the output should be the contents of the repository
* (which is the default),
* or a link that allows CodeBuild to clone the repository before building.
*
* **Note**: if this option is true,
* then only CodeBuild actions can use the resulting {@link output}.
*
* @default false
* @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html
*/
readonly codeBuildCloneOutput?: boolean;
}
/**
* CodePipeline Source that is provided by an AWS CodeCommit repository.
*
* If the CodeCommit repository is in a different account, you must use
* `CodeCommitTrigger.EVENTS` to trigger the pipeline.
*
* (That is because the Pipeline structure normally only has a `RepositoryName`
* field, and that is not enough for the pipeline to locate the repository's
* source account. However, if the pipeline is triggered via an EventBridge
* event, the event itself has the full repository ARN in there, allowing the
* pipeline to locate the repository).
*/
export class CodeCommitSourceAction extends Action {
/**
* The name of the property that holds the ARN of the CodeCommit Repository
* inside of the CodePipeline Artifact's metadata.
*
* @internal
*/
public static readonly _FULL_CLONE_ARN_PROPERTY = 'CodeCommitCloneRepositoryArn';
private readonly branch: string;
private readonly props: CodeCommitSourceActionProps;
constructor(props: CodeCommitSourceActionProps) {
const branch = props.branch ?? 'master';
if (!branch) {
throw new Error("'branch' parameter cannot be an empty string");
}
if (props.codeBuildCloneOutput === true) {
props.output.setMetadata(CodeCommitSourceAction._FULL_CLONE_ARN_PROPERTY, props.repository.repositoryArn);
}
super({
...props,
resource: props.repository,
category: codepipeline.ActionCategory.SOURCE,
provider: 'CodeCommit',
artifactBounds: sourceArtifactBounds(),
outputs: [props.output],
});
this.branch = branch;
this.props = props;
}
/** The variables emitted by this action. */
public get variables(): CodeCommitSourceVariables {
return {
repositoryName: this.variableExpression('RepositoryName'),
branchName: this.variableExpression('BranchName'),
authorDate: this.variableExpression('AuthorDate'),
committerDate: this.variableExpression('CommitterDate'),
commitId: this.variableExpression('CommitId'),
commitMessage: this.variableExpression('CommitMessage'),
};
}
protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
const createEvent = this.props.trigger === undefined ||
this.props.trigger === CodeCommitTrigger.EVENTS;
if (createEvent) {
const eventId = this.generateEventId(stage);
this.props.repository.onCommit(eventId, {
target: new targets.CodePipeline(stage.pipeline, {
eventRole: this.props.eventRole,
}),
branches: [this.branch],
});
}
// the Action will write the contents of the Git repository to the Bucket,
// so its Role needs write permissions to the Pipeline Bucket
options.bucket.grantReadWrite(options.role);
// when this action is cross-account,
// the Role needs the s3:PutObjectAcl permission for some not yet fully understood reason
if (Token.compareStrings(this.props.repository.env.account, Stack.of(stage.pipeline).account) === TokenComparison.DIFFERENT) {
options.bucket.grantPutAcl(options.role);
}
// https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control-permissions-reference.html#aa-acp
options.role.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [this.props.repository.repositoryArn],
actions: [
'codecommit:GetBranch',
'codecommit:GetCommit',
'codecommit:UploadArchive',
'codecommit:GetUploadArchiveStatus',
'codecommit:CancelUploadArchive',
...(this.props.codeBuildCloneOutput === true ? ['codecommit:GetRepository'] : []),
],
}));
return {
configuration: {
RepositoryName: this.props.repository.repositoryName,
BranchName: this.branch,
PollForSourceChanges: this.props.trigger === CodeCommitTrigger.POLL,
OutputArtifactFormat: this.props.codeBuildCloneOutput === true
? 'CODEBUILD_CLONE_REF'
: undefined,
},
};
}
private generateEventId(stage: codepipeline.IStage): string {
const baseId = Names.nodeUniqueId(stage.pipeline.node);
if (Token.isUnresolved(this.branch)) {
let candidate = '';
let counter = 0;
do {
candidate = this.eventIdFromPrefix(`${baseId}${counter}`);
counter += 1;
} while (this.props.repository.node.tryFindChild(candidate) !== undefined);
return candidate;
} else {
const branchIdDisambiguator = this.branch === 'master' ? '' : `-${this.branch}-`;
return this.eventIdFromPrefix(`${baseId}${branchIdDisambiguator}`);
}
}
private eventIdFromPrefix(eventIdPrefix: string) {
return `${eventIdPrefix}EventRule`;
}
}