-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
317 lines (288 loc) · 10.5 KB
/
index.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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
const flatten = require('flat');
const got = require('got');
const {
GITHUB_URL = 'https://github.com',
GITHUB_API_URL = 'https://api.github.com',
LOGZIO_TOKEN = ''
} = process.env;
var logger = require('logzio-nodejs').createLogger({
token: LOGZIO_TOKEN,
type: 'pr'
});
const newJsonWebToken = require('./newJsonWebToken.js');
const accessTokens = {};
async function updateShaStatus(body) {
const accessToken = accessTokens[`${body.installation.id}`].token;
const pullRequestFlattened = flatten(body.pull_request);
try {
// Initialize variables
let prlintDotJson;
const failureMessages = [];
const failureURLs = [];
const headRepoFullName = body.pull_request.head.repo.full_name;
const defaultFailureURL = `${GITHUB_URL}/${headRepoFullName}/blob/${
body.pull_request.head.sha
}/.github/prlint.json`;
// Get the user's prlint.json settings (returned as base64 and decoded later)
let prlintDotJsonUrl = `${GITHUB_API_URL}/repos/${headRepoFullName}/contents/.github/prlint.json?ref=${body
.pull_request.merge_commit_sha || body.pull_request.head.ref}`;
if (body.pull_request.head.repo.fork) {
prlintDotJsonUrl = `${GITHUB_API_URL}/repos/${
body.pull_request.base.repo.full_name
}/contents/.github/prlint.json?ref=${body.pull_request.head.sha}`;
}
const prlintDotJsonMeta = await got(prlintDotJsonUrl, {
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `token ${accessToken}`,
},
});
// Convert the base64 contents to an actual JSON object
try {
prlintDotJson = JSON.parse(
Buffer.from(JSON.parse(prlintDotJsonMeta.body).content, 'base64'),
);
} catch (e) {
failureMessages.push(e);
}
// Run each of the validations (regex's)
if (prlintDotJson) {
Object.keys(prlintDotJson).forEach((element) => {
if (prlintDotJson[element]) {
prlintDotJson[element].forEach((item, index) => {
const { pattern } = item;
try {
const regex = new RegExp(pattern, item.flags || '');
const pass = regex.test(pullRequestFlattened[element]);
if (!pass || !pullRequestFlattened[element]) {
let message = `Rule \`${element}[${index}]\` failed`;
message = item.message || message;
failureMessages.push(message);
const URL = item.detailsURL || defaultFailureURL;
failureURLs.push(URL);
}
} catch (e) {
failureMessages.push(e);
failureURLs.push(defaultFailureURL);
}
});
}
});
}
// Build up a status for sending to the pull request
let bodyPayload = {};
if (!failureMessages.length) {
bodyPayload = {
state: 'success',
description: 'Your validation rules passed',
context: 'PRLintReloaded',
};
} else {
let description = failureMessages[0];
let URL = failureURLs[0];
if (failureMessages.length > 1) {
description = `1/${failureMessages.length - 1}: ${description}`;
URL = defaultFailureURL;
}
if (description && typeof description.slice === 'function') {
bodyPayload = {
state: 'failure',
description: description.slice(0, 140), // 140 characters is a GitHub limit
target_url: URL,
context: 'PRLintReloaded',
};
} else {
bodyPayload = {
state: 'failure',
description:
'Something went wrong with PRLintReloaded - You can help by opening an issue (click details)',
target_url: 'https://github.com/maor-rozenfeld/prlint-reloaded/issues/new',
context: 'PRLintReloaded',
};
}
}
// POST the status to the pull request
try {
const statusUrl = body.pull_request.statuses_url;
await got.post(statusUrl, {
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `token ${accessToken}`,
},
body: bodyPayload,
json: true,
});
return { statusCode: 200, body: bodyPayload};
} catch (exception) {
return {statusCode: 500, body: {
exception,
request_body: bodyPayload,
response: exception.response.body,
}};
}
} catch (exception) {
// If anyone of the "happy path" logic above failed
// then we post an update to the pull request that our
// application (PRLint) had issues, or that they're missing
// a configuration file (./.github/prlint.json)
let statusCode = 200;
const statusUrl = `${GITHUB_API_URL}/repos/${
body.repository.full_name
}/statuses/${body.pull_request.head.sha}`;
if (exception.response && exception.response.statusCode === 404) {
await got.post(statusUrl, {
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `token ${accessToken}`,
},
body: {
state: 'success',
description: 'No rules are setup for PRLintReloaded',
context: 'PRLintReloaded',
target_url: `${GITHUB_URL}/apps/PRLint-Reloaded`,
},
json: true,
});
} else {
statusCode = 500;
await got.post(statusUrl, {
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `token ${accessToken}`,
},
body: {
state: 'error',
description:
'An error occurred with PRLintReloaded. Click details to open an issue',
context: 'PRLintReloaded',
target_url: `https://github.com/maor-rozenfeld/prlint-reloaded/issues/new?title=Exception Report&body=${encodeURIComponent(
exception.toString(),
)}`,
},
json: true,
});
}
return { statusCode, body: exception.toString()};
}
}
// Get a JWT on server start
let JWT = newJsonWebToken();
// Refresh the JSON Web Token every X milliseconds
// This saves us from persisting and managing tokens
// elsewhere (like redis or postgresql)
setInterval(() => {
JWT = newJsonWebToken();
}, 300000 /* 5 minutes */);
// This is the main entry point, our dependency 'micro' expects a function
// that accepts standard http.IncomingMessage and http.ServerResponse objects
// https://github.com/zeit/micro#usage
exports.handler = async (event) => {
if (event.headers['x-prlint-debug'] === 'true') {
info("request: " + JSON.stringify(event));
}
const http = event.requestContext.http;
if (http.path === '/favicon.ico') {
logger.sendAndClose()
return { statusCode: 200 , headers: {'Content-Type': 'image/x-icon'}};
}
// Used by https://stats.uptimerobot.com/ZzYnEf2BW
if (http.path === '/status' && http.method === 'GET') {
logger.sendAndClose()
return { statusCode: 200, body: { 'message': 'still alive' } };
}
const body = JSON.parse(event.body);
const metadata = { repoOwner: body?.repository?.owner?.login, repoName: body?.repository?.name }
// Used by GitHub
if (http.path === '/webhook' && http.method === 'POST') {
if (body && !body.pull_request) {
// We just return the data that was sent to the webhook
// since there's not really anything for us to do in this situation
info('Not a pull request', metadata)
logger.sendAndClose()
return { statusCode: 200, body };
}
if (body && body.action && body.action === 'closed') {
info('Pull request is closed', metadata)
logger.sendAndClose()
return { statusCode: 200, body };
}
info(`Handling PR ${body.pull_request.number} in ${body.repository.full_name}`, {
...body.installation,
prNumber: body.pull_request.number,
prTitle: body.pull_request.title,
prStatus: body.pull_request.state,
repo: body.repository.full_name,
repoOwner: body.repository.owner.login,
repoName: body.repository.name,
private: body.repository.private
});
if (
body
&& body.pull_request
&& body.installation
&& body.installation.id
&& accessTokens[`${body.installation.id}`]
&& new Date(accessTokens[`${body.installation.id}`].expires_at) > new Date() // make sure token expires in the future
) {
debug('Updating PR status', metadata)
// This is our main "happy path"
let lambdaResponse = await updateShaStatus(body);
logger.sendAndClose()
return lambdaResponse;
}
if (
body
&& body.pull_request
&& body.installation
&& body.installation.id
) {
// This is our secondary "happy path"
// But we need to fetch an access token first
// so we can read ./.github/prlint.json from their repo
try {
debug('Fetching access token', { jwt: JWT.substring(0,20) + '...', ...metadata })
const response = await got.post(`${GITHUB_API_URL}/app/installations/${body.installation.id}/access_tokens`, {
json: {},
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `Bearer ${JWT}`,
},
responseType: 'json',
});
accessTokens[`${body.installation.id}`] = response.body;
info('Updating PR status with new token', metadata)
let lambdaResponse = await updateShaStatus(body);
logger.sendAndClose()
return lambdaResponse;
} catch (exception) {
error('Failed to fetch access token', exception, metadata);
logger.sendAndClose()
return {statusCode: 500, body: {
token: accessTokens[`${body.installation.id}`],
exception
}};
}
}
// Doubtful GitHub will ever end up at this block
// but it was useful while I was developing
error('Invalid payload', null, metadata)
logger.sendAndClose()
return { statusCode: 400, body: { error: 'invalid request payload'}};
}
else {
// Redirect since we don't need anyone visiting our service
// if they happen to stumble upon our URL
info('Redirecting to GitHub repo')
logger.sendAndClose()
return { statusCode: 301, headers: { Location: 'https://github.com/maor-rozenfeld/prlint-reloaded' } };
}
};
function info(message, data) {
logger.log({ message, event: data, level: 'info' });
}
function error(message, error, data) {
logger.log({ message, event: data, error, level: 'error' });
}
function debug(message, data) {
logger.log({ message, event: data , level: 'debug'});
}