forked from aws-actions/aws-codebuild-run-build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
code-build.js
246 lines (212 loc) · 6.81 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
// 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();
// Get input options for startBuild
const params = inputs2Parameters(githubInputs());
return build(sdk, params);
}
async function build(sdk, params) {
// Start the build
const start = await sdk.codeBuild.startBuild(params).promise();
// Wait for the build to "complete"
return waitForBuildEndTime(sdk, start.build);
}
async function waitForBuildEndTime(
sdk,
{ id, logs },
seqEmptyLogs,
totalEvents,
throttleCount,
nextToken
) {
const {
codeBuild,
cloudWatchLogs,
wait = 1000 * 30,
backOff = 1000 * 15,
} = 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` seconds to the wait time
let newWait = wait + backOff;
throttleCount++;
//Sleep before trying again
await new Promise((resolve) => setTimeout(resolve, newWait));
// Try again from the same token position
return waitForBuildEndTime(
{ ...sdk, wait: newWait },
{ id, logs },
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 ? wait / 2 : wait)
);
// Try again
return waitForBuildEndTime(
sdk,
current,
seqEmptyLogs,
totalEvents,
throttleCount,
nextForwardToken
);
}
function githubInputs() {
const projectName = core.getInput("project-name", { required: 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 envPassthrough = core
.getInput("env-vars-for-codebuild", { required: false })
.split(",")
.map((i) => i.trim())
.filter((i) => i !== "");
return {
projectName,
owner,
repo,
sourceVersion,
buildspecOverride,
envPassthrough,
};
}
function inputs2Parameters(inputs) {
const {
projectName,
owner,
repo,
sourceVersion,
buildspecOverride,
envPassthrough = [],
} = inputs;
const sourceTypeOverride = "GITHUB";
const 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,
sourceVersion,
sourceTypeOverride,
sourceLocationOverride,
buildspecOverride,
environmentVariablesOverride,
};
}
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 [logGroupName, logStreamName] = Arn.split(":log-group:")
.pop()
.split(":log-stream:");
if (logGroupName === "null" || logStreamName === "null")
return {
logGroupName: undefined,
logStreamName: undefined,
};
return { logGroupName, logStreamName };
}