forked from aws-actions/aws-codebuild-run-build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
code-build.js
303 lines (264 loc) · 8.43 KB
/
code-build.js
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const core = require("@actions/core");
const github = require("@actions/github");
const aws = require("aws-sdk");
const assert = require("assert");
module.exports = {
runBuild,
build,
waitForBuildEndTime,
inputs2Parameters,
githubInputs,
buildSdk,
logName,
};
function runBuild() {
// get a codeBuild instance from the SDK
const sdk = buildSdk();
const inputs = githubInputs();
const config = (({ updateInterval, updateBackOff }) => ({
updateInterval,
updateBackOff,
}))(inputs);
// Get input options for startBuild
const params = inputs2Parameters(inputs);
return build(sdk, params, config);
}
async function build(sdk, params, config) {
// Start the build
const start = await sdk.codeBuild.startBuild(params).promise();
// Wait for the build to "complete"
return waitForBuildEndTime(sdk, start.build, config);
}
async function waitForBuildEndTime(
sdk,
{ id, logs },
{ updateInterval, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextToken
) {
const { codeBuild, cloudWatchLogs } = sdk;
totalEvents = totalEvents || 0;
seqEmptyLogs = seqEmptyLogs || 0;
throttleCount = throttleCount || 0;
// Get the CloudWatchLog info
const startFromHead = true;
const { cloudWatchLogsArn } = logs;
const { logGroupName, logStreamName } = logName(cloudWatchLogsArn);
let errObject = false;
// Check the state
const [batch, cloudWatch = {}] = await Promise.all([
codeBuild.batchGetBuilds({ ids: [id] }).promise(),
// The CloudWatchLog _may_ not be set up, only make the call if we have a logGroupName
logGroupName &&
cloudWatchLogs
.getLogEvents({
logGroupName,
logStreamName,
startFromHead,
nextToken,
})
.promise(),
]).catch((err) => {
errObject = err;
/* Returning [] here so that the assignment above
* does not throw `TypeError: undefined is not iterable`.
* The error is handled below,
* since it might be a rate limit.
*/
return [];
});
if (errObject) {
//We caught an error in trying to make the AWS api call, and are now checking to see if it was just a rate limiting error
if (errObject.message && errObject.message.search("Rate exceeded") !== -1) {
// We were rate-limited, so add backoff with Full Jitter, ref: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
let jitteredBackOff = Math.floor(
Math.random() * (updateBackOff * 2 ** throttleCount)
);
let newWait = updateInterval + jitteredBackOff;
throttleCount++;
//Sleep before trying again
await new Promise((resolve) => setTimeout(resolve, newWait));
// Try again from the same token position
return waitForBuildEndTime(
{ ...sdk },
{ id, logs },
{ updateInterval: newWait, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextToken
);
} else {
//The error returned from the API wasn't about rate limiting, so throw it as an actual error and fail the job
throw errObject;
}
}
// Pluck off the relevant state
const [current] = batch.builds;
const { nextForwardToken, events = [] } = cloudWatch;
// GetLogEvents can return partial/empty responses even when there is data.
// We wait for two consecutive empty log responses to minimize false positive on EOF.
// Empty response counter starts after any logs have been received, or when the build completes.
if (events.length == 0 && (totalEvents > 0 || current.endTime)) {
seqEmptyLogs++;
} else {
seqEmptyLogs = 0;
}
totalEvents += events.length;
// stdout the CloudWatchLog (everyone likes progress...)
// CloudWatchLogs have line endings.
// I trim and then log each line
// to ensure that the line ending is OS specific.
events.forEach(({ message }) => console.log(message.trimEnd()));
// Stop after the build is ended and we've received two consecutive empty log responses
if (current.endTime && seqEmptyLogs >= 2) {
return current;
}
// More to do: Sleep for a few seconds to avoid rate limiting
// If never throttled and build is complete, halve CWL polling delay to minimize latency
await new Promise((resolve) =>
setTimeout(
resolve,
current.endTime && throttleCount == 0
? updateInterval / 2
: updateInterval
)
);
// Try again
return waitForBuildEndTime(
sdk,
current,
{ updateInterval, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextForwardToken
);
}
function githubInputs() {
const projectName = core.getInput("project-name", { required: true });
const disableSourceOverride =
core.getInput("disable-source-override", { required: false }) === "true";
const { owner, repo } = github.context.repo;
const { payload } = github.context;
// The github.context.sha is evaluated on import.
// This makes it hard to test.
// So I use the raw ENV.
// There is a complexity here because for pull request
// the GITHUB_SHA value is NOT the correct value.
// See: https://github.com/aws-actions/aws-codebuild-run-build/issues/36
const sourceVersion =
process.env[`GITHUB_EVENT_NAME`] === "pull_request"
? (((payload || {}).pull_request || {}).head || {}).sha
: process.env[`GITHUB_SHA`];
assert(sourceVersion, "No source version could be evaluated.");
const buildspecOverride =
core.getInput("buildspec-override", { required: false }) || undefined;
const computeTypeOverride =
core.getInput("compute-type-override", { required: false }) || undefined;
const environmentTypeOverride =
core.getInput("environment-type-override", { required: false }) ||
undefined;
const imageOverride =
core.getInput("image-override", { required: false }) || undefined;
const envPassthrough = core
.getInput("env-vars-for-codebuild", { required: false })
.split(",")
.map((i) => i.trim())
.filter((i) => i !== "");
const updateInterval =
parseInt(
core.getInput("update-interval", { required: false }) || "30",
10
) * 1000;
const updateBackOff =
parseInt(
core.getInput("update-back-off", { required: false }) || "15",
10
) * 1000;
return {
projectName,
owner,
repo,
sourceVersion,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
envPassthrough,
updateInterval,
updateBackOff,
disableSourceOverride,
};
}
function inputs2Parameters(inputs) {
const {
projectName,
owner,
repo,
sourceVersion,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
envPassthrough = [],
disableSourceOverride,
} = inputs;
const sourceOverride = !disableSourceOverride
? {
sourceVersion: sourceVersion,
sourceTypeOverride: "GITHUB",
sourceLocationOverride: `https://github.com/${owner}/${repo}.git`,
}
: {};
const environmentVariablesOverride = Object.entries(process.env)
.filter(
([key]) => key.startsWith("GITHUB_") || envPassthrough.includes(key)
)
.map(([name, value]) => ({ name, value, type: "PLAINTEXT" }));
// The idempotencyToken is intentionally not set.
// This way the GitHub events can manage the builds.
return {
projectName,
...sourceOverride,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
environmentVariablesOverride,
disableSourceOverride,
};
}
function buildSdk() {
const codeBuild = new aws.CodeBuild({
customUserAgent: "aws-actions/aws-codebuild-run-build",
});
const cloudWatchLogs = new aws.CloudWatchLogs({
customUserAgent: "aws-actions/aws-codebuild-run-build",
});
assert(
codeBuild.config.credentials && cloudWatchLogs.config.credentials,
"No credentials. Try adding @aws-actions/configure-aws-credentials earlier in your job to set up AWS credentials."
);
return { codeBuild, cloudWatchLogs };
}
function logName(Arn) {
const logs = {
logGroupName: undefined,
logStreamName: undefined,
};
if (Arn) {
const [logGroupName, logStreamName] = Arn.split(":log-group:")
.pop()
.split(":log-stream:");
if (logGroupName !== "null" && logStreamName !== "null") {
logs.logGroupName = logGroupName;
logs.logStreamName = logStreamName;
}
}
return logs;
}