gcf-utils 13.2.2 → 13.3.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/#gcf-utils.js# +779 -0
- package/build/src/bin/genkey.js +2 -2
- package/build/src/bin/genkey.js.map +1 -1
- package/build/src/gcf-utils.d.ts +2 -0
- package/build/src/gcf-utils.js +19 -8
- package/build/src/gcf-utils.js.map +1 -1
- package/build/src/gcf-utils.js~ +778 -0
- package/build/src/logging/gcf-logger.js +1 -1
- package/build/src/logging/gcf-logger.js.map +1 -1
- package/build/src/server/server.js +1 -1
- package/build/src/server/server.js.map +1 -1
- package/package.json +2 -2
@@ -0,0 +1,778 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.GCFBootstrapper = exports.addOrUpdateIssueComment = exports.getCommentMark = exports.TriggerType = exports.logger = void 0;
|
7
|
+
// Copyright 2019 Google LLC
|
8
|
+
//
|
9
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
10
|
+
// you may not use this file except in compliance with the License.
|
11
|
+
// You may obtain a copy of the License at
|
12
|
+
//
|
13
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
14
|
+
//
|
15
|
+
// Unless required by applicable law or agreed to in writing, software
|
16
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
17
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
18
|
+
// See the License for the specific language governing permissions and
|
19
|
+
// limitations under the License.
|
20
|
+
//
|
21
|
+
const probot_1 = require("probot");
|
22
|
+
const octokit_auth_probot_1 = require("octokit-auth-probot");
|
23
|
+
const get_stream_1 = __importDefault(require("get-stream"));
|
24
|
+
const into_stream_1 = __importDefault(require("into-stream"));
|
25
|
+
const secret_manager_1 = require("@google-cloud/secret-manager");
|
26
|
+
const tasks_1 = require("@google-cloud/tasks");
|
27
|
+
const storage_1 = require("@google-cloud/storage");
|
28
|
+
// eslint-disable-next-line node/no-extraneous-import
|
29
|
+
const rest_1 = require("@octokit/rest");
|
30
|
+
const octokit_plugin_config_1 = require("@probot/octokit-plugin-config");
|
31
|
+
const trigger_info_builder_1 = require("./logging/trigger-info-builder");
|
32
|
+
const gcf_logger_1 = require("./logging/gcf-logger");
|
33
|
+
const uuid_1 = require("uuid");
|
34
|
+
const server_1 = require("./server/server");
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
36
|
+
const LoggingOctokitPlugin = require('../src/logging/logging-octokit-plugin.js');
|
37
|
+
const DEFAULT_CRON_TYPE = 'repository';
|
38
|
+
const SCHEDULER_GLOBAL_EVENT_NAME = 'schedule.global';
|
39
|
+
const SCHEDULER_INSTALLATION_EVENT_NAME = 'schedule.installation';
|
40
|
+
const SCHEDULER_REPOSITORY_EVENT_NAME = 'schedule.repository';
|
41
|
+
const SCHEDULER_EVENT_NAMES = [
|
42
|
+
SCHEDULER_GLOBAL_EVENT_NAME,
|
43
|
+
SCHEDULER_INSTALLATION_EVENT_NAME,
|
44
|
+
SCHEDULER_REPOSITORY_EVENT_NAME,
|
45
|
+
];
|
46
|
+
const RUNNING_IN_TEST = process.env.NODE_ENV === 'test';
|
47
|
+
const DEFAULT_WRAP_CONFIG = {
|
48
|
+
logging: false,
|
49
|
+
skipVerification: RUNNING_IN_TEST,
|
50
|
+
maxCronRetries: 0,
|
51
|
+
maxRetries: 10,
|
52
|
+
maxPubSubRetries: 0,
|
53
|
+
};
|
54
|
+
exports.logger = new gcf_logger_1.GCFLogger();
|
55
|
+
/**
|
56
|
+
* Type of function execution trigger
|
57
|
+
*/
|
58
|
+
var TriggerType;
|
59
|
+
(function (TriggerType) {
|
60
|
+
TriggerType["GITHUB"] = "GitHub Webhook";
|
61
|
+
TriggerType["SCHEDULER"] = "Cloud Scheduler";
|
62
|
+
TriggerType["TASK"] = "Cloud Task";
|
63
|
+
TriggerType["PUBSUB"] = "Pub/Sub";
|
64
|
+
TriggerType["UNKNOWN"] = "Unknown";
|
65
|
+
})(TriggerType = exports.TriggerType || (exports.TriggerType = {}));
|
66
|
+
/**
|
67
|
+
* It creates a comment string used for `addOrUpdateissuecomment`.
|
68
|
+
*/
|
69
|
+
const getCommentMark = (installationId) => {
|
70
|
+
return `<!-- probot comment [${installationId}]-->`;
|
71
|
+
};
|
72
|
+
exports.getCommentMark = getCommentMark;
|
73
|
+
/**
|
74
|
+
* It creates a comment, or if the bot already created a comment, it
|
75
|
+
* updates the same comment.
|
76
|
+
*
|
77
|
+
* @param {Octokit} octokit - The Octokit instance.
|
78
|
+
* @param {string} owner - The owner of the issue.
|
79
|
+
* @param {string} repo - The name of the repository.
|
80
|
+
* @param {number} issueNumber - The number of the issue.
|
81
|
+
* @param {number} installationId - A unique number for identifying the issue
|
82
|
+
* comment.
|
83
|
+
* @param {string} commentBody - The body of the comment.
|
84
|
+
* @param {boolean} onlyUpdate - If set to true, it will only update an
|
85
|
+
* existing issue comment.
|
86
|
+
*/
|
87
|
+
const addOrUpdateIssueComment = async (octokit, owner, repo, issueNumber, installationId, commentBody, onlyUpdate = false) => {
|
88
|
+
var _a;
|
89
|
+
const commentMark = exports.getCommentMark(installationId);
|
90
|
+
const listCommentsResponse = await octokit.issues.listComments({
|
91
|
+
owner: owner,
|
92
|
+
repo: repo,
|
93
|
+
per_page: 50,
|
94
|
+
issue_number: issueNumber,
|
95
|
+
});
|
96
|
+
let found = false;
|
97
|
+
for (const comment of listCommentsResponse.data) {
|
98
|
+
if ((_a = comment.body) === null || _a === void 0 ? void 0 : _a.includes(commentMark)) {
|
99
|
+
// We found the existing comment, so updating it
|
100
|
+
await octokit.issues.updateComment({
|
101
|
+
owner: owner,
|
102
|
+
repo: repo,
|
103
|
+
comment_id: comment.id,
|
104
|
+
body: `${commentMark}\n${commentBody}`,
|
105
|
+
});
|
106
|
+
found = true;
|
107
|
+
}
|
108
|
+
}
|
109
|
+
if (!found && !onlyUpdate) {
|
110
|
+
await octokit.issues.createComment({
|
111
|
+
owner: owner,
|
112
|
+
repo: repo,
|
113
|
+
issue_number: issueNumber,
|
114
|
+
body: `${commentMark}\n${commentBody}`,
|
115
|
+
});
|
116
|
+
}
|
117
|
+
};
|
118
|
+
exports.addOrUpdateIssueComment = addOrUpdateIssueComment;
|
119
|
+
class GCFBootstrapper {
|
120
|
+
constructor(secretsClient) {
|
121
|
+
this.secretsClient =
|
122
|
+
secretsClient || new secret_manager_1.v1.SecretManagerServiceClient();
|
123
|
+
this.cloudTasksClient = new tasks_1.v2.CloudTasksClient();
|
124
|
+
this.storage = new storage_1.Storage({ autoRetry: !RUNNING_IN_TEST });
|
125
|
+
}
|
126
|
+
async loadProbot(appFn, logging) {
|
127
|
+
if (!this.probot) {
|
128
|
+
const cfg = await this.getProbotConfig(logging);
|
129
|
+
this.probot = probot_1.createProbot({ overrides: cfg });
|
130
|
+
}
|
131
|
+
await this.probot.load(appFn);
|
132
|
+
return this.probot;
|
133
|
+
}
|
134
|
+
getSecretName() {
|
135
|
+
const projectId = process.env.PROJECT_ID || '';
|
136
|
+
const functionName = process.env.GCF_SHORT_FUNCTION_NAME || '';
|
137
|
+
return `projects/${projectId}/secrets/${functionName}`;
|
138
|
+
}
|
139
|
+
getLatestSecretVersionName() {
|
140
|
+
const secretName = this.getSecretName();
|
141
|
+
return `${secretName}/versions/latest`;
|
142
|
+
}
|
143
|
+
async getProbotConfig(logging) {
|
144
|
+
var _a, _b;
|
145
|
+
const name = this.getLatestSecretVersionName();
|
146
|
+
const [version] = await this.secretsClient.accessSecretVersion({
|
147
|
+
name: name,
|
148
|
+
});
|
149
|
+
// Extract the payload as a string.
|
150
|
+
const payload = ((_b = (_a = version === null || version === void 0 ? void 0 : version.payload) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.toString()) || '';
|
151
|
+
if (payload === '') {
|
152
|
+
throw Error('did not retrieve a payload from SecretManager.');
|
153
|
+
}
|
154
|
+
const config = JSON.parse(payload);
|
155
|
+
if (Object.prototype.hasOwnProperty.call(config, 'cert')) {
|
156
|
+
config.privateKey = config.cert;
|
157
|
+
delete config.cert;
|
158
|
+
}
|
159
|
+
if (Object.prototype.hasOwnProperty.call(config, 'id')) {
|
160
|
+
config.appId = config.id;
|
161
|
+
delete config.id;
|
162
|
+
}
|
163
|
+
if (logging) {
|
164
|
+
exports.logger.info('custom logging instance enabled');
|
165
|
+
const LoggingOctokit = rest_1.Octokit.plugin(LoggingOctokitPlugin)
|
166
|
+
.plugin(octokit_plugin_config_1.config)
|
167
|
+
.defaults({ authStrategy: octokit_auth_probot_1.createProbotAuth });
|
168
|
+
return { ...config, Octokit: LoggingOctokit };
|
169
|
+
}
|
170
|
+
else {
|
171
|
+
exports.logger.info('custom logging instance not enabled');
|
172
|
+
const DefaultOctokit = rest_1.Octokit.plugin(octokit_plugin_config_1.config).defaults({
|
173
|
+
authStrategy: octokit_auth_probot_1.createProbotAuth,
|
174
|
+
});
|
175
|
+
return {
|
176
|
+
...config,
|
177
|
+
Octokit: DefaultOctokit,
|
178
|
+
};
|
179
|
+
}
|
180
|
+
}
|
181
|
+
/**
|
182
|
+
* Parse the signature from the request headers.
|
183
|
+
*
|
184
|
+
* If the expected header is not set, returns `unset` because the verification
|
185
|
+
* function throws an exception on empty string when we would rather
|
186
|
+
* treat the error as an invalid signature.
|
187
|
+
* @param request incoming trigger request
|
188
|
+
*/
|
189
|
+
static parseSignatureHeader(request) {
|
190
|
+
const sha1Signature = request.get('x-hub-signature') || request.get('X-Hub-Signature');
|
191
|
+
if (sha1Signature) {
|
192
|
+
// See https://github.com/googleapis/repo-automation-bots/issues/2092
|
193
|
+
return sha1Signature.startsWith('sha1=')
|
194
|
+
? sha1Signature
|
195
|
+
: `sha1=${sha1Signature}`;
|
196
|
+
}
|
197
|
+
return 'unset';
|
198
|
+
}
|
199
|
+
/**
|
200
|
+
* Parse the event name, delivery id, signature and task id from the request headers
|
201
|
+
* @param request incoming trigger request
|
202
|
+
*/
|
203
|
+
static parseRequestHeaders(request) {
|
204
|
+
const name = request.get('x-github-event') || request.get('X-GitHub-Event') || '';
|
205
|
+
const id = request.get('x-github-delivery') ||
|
206
|
+
request.get('X-GitHub-Delivery') ||
|
207
|
+
'';
|
208
|
+
const signature = this.parseSignatureHeader(request);
|
209
|
+
const taskId = request.get('X-CloudTasks-TaskName') ||
|
210
|
+
request.get('x-cloudtasks-taskname') ||
|
211
|
+
'';
|
212
|
+
const taskRetries = parseInt(request.get('X-CloudTasks-TaskRetryCount') ||
|
213
|
+
request.get('x-cloudtasks-taskretrycount') ||
|
214
|
+
'0');
|
215
|
+
return { name, id, signature, taskId, taskRetries };
|
216
|
+
}
|
217
|
+
/**
|
218
|
+
* Determine the type of trigger that started this execution
|
219
|
+
* @param name event name from header
|
220
|
+
* @param taskId task id from header
|
221
|
+
*/
|
222
|
+
static parseTriggerType(name, taskId) {
|
223
|
+
if (!taskId && SCHEDULER_EVENT_NAMES.includes(name)) {
|
224
|
+
return TriggerType.SCHEDULER;
|
225
|
+
}
|
226
|
+
else if (!taskId && name === 'pubsub.message') {
|
227
|
+
return TriggerType.PUBSUB;
|
228
|
+
}
|
229
|
+
else if (!taskId && name) {
|
230
|
+
return TriggerType.GITHUB;
|
231
|
+
}
|
232
|
+
else if (name) {
|
233
|
+
return TriggerType.TASK;
|
234
|
+
}
|
235
|
+
return TriggerType.UNKNOWN;
|
236
|
+
}
|
237
|
+
parseWrapConfig(wrapOptions) {
|
238
|
+
const wrapConfig = {
|
239
|
+
...DEFAULT_WRAP_CONFIG,
|
240
|
+
...wrapOptions,
|
241
|
+
};
|
242
|
+
if ((wrapOptions === null || wrapOptions === void 0 ? void 0 : wrapOptions.background) !== undefined) {
|
243
|
+
exports.logger.warn('`background` option has been deprecated in favor of `maxRetries` and `maxCronRetries`');
|
244
|
+
if (wrapOptions.background === false) {
|
245
|
+
wrapConfig.maxCronRetries = 0;
|
246
|
+
wrapConfig.maxRetries = 0;
|
247
|
+
wrapConfig.maxPubSubRetries = 0;
|
248
|
+
}
|
249
|
+
}
|
250
|
+
return wrapConfig;
|
251
|
+
}
|
252
|
+
getRetryLimit(wrapConfig, eventName) {
|
253
|
+
if (eventName.startsWith('schedule.')) {
|
254
|
+
return wrapConfig.maxCronRetries;
|
255
|
+
}
|
256
|
+
if (eventName.startsWith('pubsub.')) {
|
257
|
+
return wrapConfig.maxPubSubRetries;
|
258
|
+
}
|
259
|
+
return wrapConfig.maxRetries;
|
260
|
+
}
|
261
|
+
/**
|
262
|
+
* Wrap an ApplicationFunction in a http.Server that can be started
|
263
|
+
* directly.
|
264
|
+
* @param appFn {ApplicationFunction} The probot handler function
|
265
|
+
* @param wrapOptions {WrapOptions} Bot handler options
|
266
|
+
*/
|
267
|
+
server(appFn, wrapOptions) {
|
268
|
+
return server_1.getServer(this.gcf(appFn, wrapOptions));
|
269
|
+
}
|
270
|
+
/**
|
271
|
+
* Wrap an ApplicationFunction in so it can be started in a Google
|
272
|
+
* Cloud Function.
|
273
|
+
* @param appFn {ApplicationFunction} The probot handler function
|
274
|
+
* @param wrapOptions {WrapOptions} Bot handler options
|
275
|
+
*/
|
276
|
+
gcf(appFn, wrapOptions) {
|
277
|
+
return async (request, response) => {
|
278
|
+
const wrapConfig = this.parseWrapConfig(wrapOptions);
|
279
|
+
this.probot =
|
280
|
+
this.probot || (await this.loadProbot(appFn, wrapConfig.logging));
|
281
|
+
const { name, id, signature, taskId, taskRetries } = GCFBootstrapper.parseRequestHeaders(request);
|
282
|
+
const triggerType = GCFBootstrapper.parseTriggerType(name, taskId);
|
283
|
+
// validate the signature
|
284
|
+
if (!wrapConfig.skipVerification &&
|
285
|
+
!this.probot.webhooks.verify(request.body, signature)) {
|
286
|
+
response.send({
|
287
|
+
statusCode: 400,
|
288
|
+
body: JSON.stringify({ message: 'Invalid signature' }),
|
289
|
+
});
|
290
|
+
return;
|
291
|
+
}
|
292
|
+
/**
|
293
|
+
* Note: any logs written before resetting bindings may contain
|
294
|
+
* bindings from previous executions
|
295
|
+
*/
|
296
|
+
exports.logger.resetBindings();
|
297
|
+
exports.logger.addBindings(trigger_info_builder_1.buildTriggerInfo(triggerType, id, name, request.body));
|
298
|
+
try {
|
299
|
+
if (triggerType === TriggerType.UNKNOWN) {
|
300
|
+
response.sendStatus(400);
|
301
|
+
return;
|
302
|
+
}
|
303
|
+
else if (triggerType === TriggerType.SCHEDULER) {
|
304
|
+
// Cloud scheduler tasks (cron)
|
305
|
+
await this.handleScheduled(id, request, wrapConfig);
|
306
|
+
}
|
307
|
+
else if (triggerType === TriggerType.PUBSUB) {
|
308
|
+
const payload = this.parsePubSubPayload(request);
|
309
|
+
await this.enqueueTask({
|
310
|
+
id,
|
311
|
+
name,
|
312
|
+
body: JSON.stringify(payload),
|
313
|
+
});
|
314
|
+
}
|
315
|
+
else if (triggerType === TriggerType.TASK) {
|
316
|
+
const maxRetries = this.getRetryLimit(wrapConfig, name);
|
317
|
+
// Abort task retries if we've hit the max number by
|
318
|
+
// returning "success"
|
319
|
+
if (taskRetries > maxRetries) {
|
320
|
+
exports.logger.metric('too-many-retries');
|
321
|
+
exports.logger.info(`Too many retries: ${taskRetries} > ${maxRetries}`);
|
322
|
+
response.send({
|
323
|
+
statusCode: 200,
|
324
|
+
body: JSON.stringify({ message: 'Too many retries' }),
|
325
|
+
});
|
326
|
+
return;
|
327
|
+
}
|
328
|
+
// If the payload contains `tmpUrl` this indicates that the original
|
329
|
+
// payload has been written to Cloud Storage; download it.
|
330
|
+
const payload = await this.maybeDownloadOriginalBody(request.body);
|
331
|
+
// The payload does not exist, stop retrying on this task by letting
|
332
|
+
// this request "succeed".
|
333
|
+
if (!payload) {
|
334
|
+
exports.logger.metric('payload-expired');
|
335
|
+
response.send({
|
336
|
+
statusCode: 200,
|
337
|
+
body: JSON.stringify({ message: 'Payload expired' }),
|
338
|
+
});
|
339
|
+
return;
|
340
|
+
}
|
341
|
+
// TODO: find out the best way to get this type, and whether we can
|
342
|
+
// keep using a custom event name.
|
343
|
+
await this.probot.receive({
|
344
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
345
|
+
name: name,
|
346
|
+
id,
|
347
|
+
payload,
|
348
|
+
});
|
349
|
+
}
|
350
|
+
else if (triggerType === TriggerType.GITHUB) {
|
351
|
+
await this.enqueueTask({
|
352
|
+
id,
|
353
|
+
name,
|
354
|
+
body: JSON.stringify(request.body),
|
355
|
+
});
|
356
|
+
}
|
357
|
+
response.send({
|
358
|
+
statusCode: 200,
|
359
|
+
body: JSON.stringify({ message: 'Executed' }),
|
360
|
+
});
|
361
|
+
}
|
362
|
+
catch (err) {
|
363
|
+
exports.logger.error(err);
|
364
|
+
response.status(500).send({
|
365
|
+
statusCode: 500,
|
366
|
+
body: JSON.stringify({ message: err.message }),
|
367
|
+
});
|
368
|
+
return;
|
369
|
+
}
|
370
|
+
exports.logger.flushSync();
|
371
|
+
};
|
372
|
+
}
|
373
|
+
/**
|
374
|
+
* Entrypoint for handling all scheduled tasks.
|
375
|
+
*
|
376
|
+
* @param id {string} GitHub delivery GUID
|
377
|
+
* @param body {Scheduled} Scheduler params. May contain additional request
|
378
|
+
* parameters besides the ones defined by the Scheduled type.
|
379
|
+
* @param signature
|
380
|
+
* @param wrapConfig
|
381
|
+
*/
|
382
|
+
async handleScheduled(id, req, wrapConfig) {
|
383
|
+
var _a;
|
384
|
+
const body = this.parseRequestBody(req);
|
385
|
+
const cronType = (_a = body.cron_type) !== null && _a !== void 0 ? _a : DEFAULT_CRON_TYPE;
|
386
|
+
if (cronType === 'global') {
|
387
|
+
await this.handleScheduledGlobal(id, body);
|
388
|
+
}
|
389
|
+
else if (cronType === 'installation') {
|
390
|
+
await this.handleScheduledInstallation(id, body, wrapConfig);
|
391
|
+
}
|
392
|
+
else {
|
393
|
+
await this.handleScheduledRepository(id, body, wrapConfig);
|
394
|
+
}
|
395
|
+
}
|
396
|
+
/**
|
397
|
+
* Handle a scheduled tasks that should run once. Queues up a Cloud Task
|
398
|
+
* for the `schedule.global` event.
|
399
|
+
*
|
400
|
+
* @param id {string} GitHub delivery GUID
|
401
|
+
* @param body {Scheduled} Scheduler params. May contain additional request
|
402
|
+
* parameters besides the ones defined by the Scheduled type.
|
403
|
+
* @param signature
|
404
|
+
*/
|
405
|
+
async handleScheduledGlobal(id, body) {
|
406
|
+
await this.enqueueTask({
|
407
|
+
id,
|
408
|
+
name: SCHEDULER_GLOBAL_EVENT_NAME,
|
409
|
+
body: JSON.stringify(body),
|
410
|
+
});
|
411
|
+
}
|
412
|
+
/**
|
413
|
+
* Async iterator over each installation for an app.
|
414
|
+
*
|
415
|
+
* See https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app
|
416
|
+
* @param wrapConfig {WrapConfig}
|
417
|
+
*/
|
418
|
+
async *eachInstallation(wrapConfig) {
|
419
|
+
const octokit = await this.getAuthenticatedOctokit(undefined, wrapConfig);
|
420
|
+
const installationsPaginated = octokit.paginate.iterator(octokit.apps.listInstallations);
|
421
|
+
for await (const response of installationsPaginated) {
|
422
|
+
for (const installation of response.data) {
|
423
|
+
yield installation;
|
424
|
+
}
|
425
|
+
}
|
426
|
+
}
|
427
|
+
/**
|
428
|
+
* Async iterator over each repository for an app installation.
|
429
|
+
*
|
430
|
+
* See https://docs.github.com/en/rest/reference/apps#list-repositories-accessible-to-the-app-installation
|
431
|
+
* @param wrapConfig {WrapConfig}
|
432
|
+
*/
|
433
|
+
async *eachInstalledRepository(installationId, wrapConfig) {
|
434
|
+
const octokit = await this.getAuthenticatedOctokit(installationId, wrapConfig);
|
435
|
+
const installationRepositoriesPaginated = octokit.paginate.iterator(octokit.apps.listReposAccessibleToInstallation, {
|
436
|
+
mediaType: {
|
437
|
+
previews: ['machine-man'],
|
438
|
+
},
|
439
|
+
});
|
440
|
+
for await (const response of installationRepositoriesPaginated) {
|
441
|
+
for (const repo of response.data) {
|
442
|
+
yield repo;
|
443
|
+
}
|
444
|
+
}
|
445
|
+
}
|
446
|
+
/**
|
447
|
+
* Handle a scheduled tasks that should run per-installation.
|
448
|
+
*
|
449
|
+
* If an installation is specified (via installation.id in the payload),
|
450
|
+
* queue up a Cloud Task (`schedule.installation`) for that installation
|
451
|
+
* only. Otherwise, list all installations of the app and queue up a
|
452
|
+
* Cloud Task for each installation.
|
453
|
+
*
|
454
|
+
* @param id {string} GitHub delivery GUID
|
455
|
+
* @param body {Scheduled} Scheduler params. May contain additional request
|
456
|
+
* parameters besides the ones defined by the Scheduled type.
|
457
|
+
* @param wrapConfig
|
458
|
+
*/
|
459
|
+
async handleScheduledInstallation(id, body, wrapConfig) {
|
460
|
+
var _a;
|
461
|
+
if (body.installation) {
|
462
|
+
await this.enqueueTask({
|
463
|
+
id,
|
464
|
+
name: SCHEDULER_INSTALLATION_EVENT_NAME,
|
465
|
+
body: JSON.stringify(body),
|
466
|
+
});
|
467
|
+
}
|
468
|
+
else {
|
469
|
+
const generator = this.eachInstallation(wrapConfig);
|
470
|
+
for await (const installation of generator) {
|
471
|
+
const extraParams = {
|
472
|
+
installation: {
|
473
|
+
id: installation.id,
|
474
|
+
},
|
475
|
+
};
|
476
|
+
if (installation.target_type === 'Organization' &&
|
477
|
+
((_a = installation === null || installation === void 0 ? void 0 : installation.account) === null || _a === void 0 ? void 0 : _a.login)) {
|
478
|
+
extraParams.cron_org = installation.account.login;
|
479
|
+
}
|
480
|
+
const payload = {
|
481
|
+
...body,
|
482
|
+
...extraParams,
|
483
|
+
};
|
484
|
+
await this.enqueueTask({
|
485
|
+
id,
|
486
|
+
name: SCHEDULER_INSTALLATION_EVENT_NAME,
|
487
|
+
body: JSON.stringify(payload),
|
488
|
+
});
|
489
|
+
}
|
490
|
+
}
|
491
|
+
}
|
492
|
+
/**
|
493
|
+
* Handle a scheduled tasks that should run per-repository.
|
494
|
+
*
|
495
|
+
* If a repository is specified (via repo in the payload), queue up a
|
496
|
+
* Cloud Task for that repository only. If an installation is specified
|
497
|
+
* (via installation.id in the payload), list all repositories associated
|
498
|
+
* with that installation and queue up a Cloud Task for each repository.
|
499
|
+
* If neither is specified, list all installations and all repositories
|
500
|
+
* for each installation, then queue up a Cloud Task for each repository.
|
501
|
+
*
|
502
|
+
* @param id {string} GitHub delivery GUID
|
503
|
+
* @param body {Scheduled} Scheduler params. May contain additional request
|
504
|
+
* parameters besides the ones defined by the Scheduled type.
|
505
|
+
* @param signature
|
506
|
+
* @param wrapConfig
|
507
|
+
*/
|
508
|
+
async handleScheduledRepository(id, body, wrapConfig) {
|
509
|
+
var _a;
|
510
|
+
if (body.repo) {
|
511
|
+
// Job was scheduled for a single repository:
|
512
|
+
await this.scheduledToTask(body.repo, id, body, SCHEDULER_REPOSITORY_EVENT_NAME);
|
513
|
+
}
|
514
|
+
else if (body.installation) {
|
515
|
+
const generator = this.eachInstalledRepository(body.installation.id, wrapConfig);
|
516
|
+
const promises = new Array();
|
517
|
+
const batchSize = 30;
|
518
|
+
for await (const repo of generator) {
|
519
|
+
if (repo.archived === true || repo.disabled === true) {
|
520
|
+
continue;
|
521
|
+
}
|
522
|
+
promises.push(this.scheduledToTask(repo.full_name, id, body, SCHEDULER_REPOSITORY_EVENT_NAME));
|
523
|
+
if (promises.length >= batchSize) {
|
524
|
+
await Promise.all(promises);
|
525
|
+
promises.splice(0, promises.length);
|
526
|
+
}
|
527
|
+
}
|
528
|
+
// Wait for the rest.
|
529
|
+
if (promises.length > 0) {
|
530
|
+
await Promise.all(promises);
|
531
|
+
promises.splice(0, promises.length);
|
532
|
+
}
|
533
|
+
}
|
534
|
+
else {
|
535
|
+
const installationGenerator = this.eachInstallation(wrapConfig);
|
536
|
+
const promises = new Array();
|
537
|
+
const batchSize = 30;
|
538
|
+
for await (const installation of installationGenerator) {
|
539
|
+
const generator = this.eachInstalledRepository(installation.id, wrapConfig);
|
540
|
+
const extraParams = {
|
541
|
+
installation: {
|
542
|
+
id: installation.id,
|
543
|
+
},
|
544
|
+
};
|
545
|
+
if (installation.target_type === 'Organization' &&
|
546
|
+
((_a = installation === null || installation === void 0 ? void 0 : installation.account) === null || _a === void 0 ? void 0 : _a.login)) {
|
547
|
+
extraParams.cron_org = installation.account.login;
|
548
|
+
}
|
549
|
+
const payload = {
|
550
|
+
...body,
|
551
|
+
...extraParams,
|
552
|
+
};
|
553
|
+
for await (const repo of generator) {
|
554
|
+
if (repo.archived === true || repo.disabled === true) {
|
555
|
+
continue;
|
556
|
+
}
|
557
|
+
promises.push(this.scheduledToTask(repo.full_name, id, payload, SCHEDULER_REPOSITORY_EVENT_NAME));
|
558
|
+
if (promises.length >= batchSize) {
|
559
|
+
await Promise.all(promises);
|
560
|
+
promises.splice(0, promises.length);
|
561
|
+
}
|
562
|
+
}
|
563
|
+
// Wait for the rest.
|
564
|
+
if (promises.length > 0) {
|
565
|
+
await Promise.all(promises);
|
566
|
+
promises.splice(0, promises.length);
|
567
|
+
}
|
568
|
+
}
|
569
|
+
}
|
570
|
+
}
|
571
|
+
/**
|
572
|
+
* Build an app-based authenticated Octokit instance.
|
573
|
+
*
|
574
|
+
* @param installationId {number|undefined} The installation id to
|
575
|
+
* authenticate as. Required if you are trying to take action
|
576
|
+
* on an installed repository.
|
577
|
+
* @param wrapConfig
|
578
|
+
*/
|
579
|
+
async getAuthenticatedOctokit(installationId, wrapConfig) {
|
580
|
+
const cfg = await this.getProbotConfig(wrapConfig === null || wrapConfig === void 0 ? void 0 : wrapConfig.logging);
|
581
|
+
let opts = {
|
582
|
+
appId: cfg.appId,
|
583
|
+
privateKey: cfg.privateKey,
|
584
|
+
};
|
585
|
+
if (installationId) {
|
586
|
+
opts = {
|
587
|
+
...opts,
|
588
|
+
...{ installationId },
|
589
|
+
};
|
590
|
+
}
|
591
|
+
if (wrapConfig === null || wrapConfig === void 0 ? void 0 : wrapConfig.logging) {
|
592
|
+
const LoggingOctokit = rest_1.Octokit.plugin(LoggingOctokitPlugin)
|
593
|
+
.plugin(octokit_plugin_config_1.config)
|
594
|
+
.defaults({ authStrategy: octokit_auth_probot_1.createProbotAuth });
|
595
|
+
return new LoggingOctokit({ auth: opts });
|
596
|
+
}
|
597
|
+
else {
|
598
|
+
const DefaultOctokit = rest_1.Octokit.plugin(octokit_plugin_config_1.config).defaults({
|
599
|
+
authStrategy: octokit_auth_probot_1.createProbotAuth,
|
600
|
+
});
|
601
|
+
return new DefaultOctokit({ auth: opts });
|
602
|
+
}
|
603
|
+
}
|
604
|
+
async scheduledToTask(repoFullName, id, body, eventName) {
|
605
|
+
// The payload from the scheduler is updated with additional information
|
606
|
+
// providing context about the organization/repo that the event is
|
607
|
+
// firing for.
|
608
|
+
const payload = {
|
609
|
+
...body,
|
610
|
+
...this.buildRepositoryDetails(repoFullName),
|
611
|
+
};
|
612
|
+
try {
|
613
|
+
await this.enqueueTask({
|
614
|
+
id,
|
615
|
+
name: eventName,
|
616
|
+
body: JSON.stringify(payload),
|
617
|
+
});
|
618
|
+
}
|
619
|
+
catch (err) {
|
620
|
+
exports.logger.error(err);
|
621
|
+
}
|
622
|
+
}
|
623
|
+
parsePubSubPayload(req) {
|
624
|
+
const body = this.parseRequestBody(req);
|
625
|
+
return {
|
626
|
+
...body,
|
627
|
+
...(body.repo ? this.buildRepositoryDetails(body.repo) : {}),
|
628
|
+
};
|
629
|
+
}
|
630
|
+
parseRequestBody(req) {
|
631
|
+
let body = (Buffer.isBuffer(req.body)
|
632
|
+
? JSON.parse(req.body.toString('utf8'))
|
633
|
+
: req.body);
|
634
|
+
// PubSub messages have their payload encoded in body.message.data
|
635
|
+
// as a base64 blob.
|
636
|
+
if (body.message && body.message.data) {
|
637
|
+
body = JSON.parse(Buffer.from(body.message.data, 'base64').toString());
|
638
|
+
}
|
639
|
+
return body;
|
640
|
+
}
|
641
|
+
buildRepositoryDetails(repoFullName) {
|
642
|
+
const [orgName, repoName] = repoFullName.split('/');
|
643
|
+
return {
|
644
|
+
repository: {
|
645
|
+
name: repoName,
|
646
|
+
full_name: repoFullName,
|
647
|
+
owner: {
|
648
|
+
login: orgName,
|
649
|
+
name: orgName,
|
650
|
+
},
|
651
|
+
},
|
652
|
+
organization: {
|
653
|
+
login: orgName,
|
654
|
+
},
|
655
|
+
};
|
656
|
+
}
|
657
|
+
/**
|
658
|
+
* Schedule a event trigger as a Cloud Task.
|
659
|
+
* @param params {EnqueueTaskParams} Task parameters.
|
660
|
+
*/
|
661
|
+
async enqueueTask(params) {
|
662
|
+
var _a, _b;
|
663
|
+
exports.logger.info('scheduling cloud task');
|
664
|
+
// Make a task here and return 200 as this is coming from GitHub
|
665
|
+
const projectId = process.env.PROJECT_ID || '';
|
666
|
+
const location = process.env.GCF_LOCATION || '';
|
667
|
+
// queue name can contain only letters ([A-Za-z]), numbers ([0-9]), or hyphens (-):
|
668
|
+
const queueName = (process.env.GCF_SHORT_FUNCTION_NAME || '').replace(/_/g, '-');
|
669
|
+
const queuePath = this.cloudTasksClient.queuePath(projectId, location, queueName);
|
670
|
+
// https://us-central1-repo-automation-bots.cloudfunctions.net/merge_on_green:
|
671
|
+
const url = `https://${location}-${projectId}.cloudfunctions.net/${process.env.GCF_SHORT_FUNCTION_NAME}`;
|
672
|
+
exports.logger.info(`scheduling task in queue ${queueName}`);
|
673
|
+
if (params.body) {
|
674
|
+
// Payload conists of either the original params.body or, if Cloud
|
675
|
+
// Storage has been configured, a tmp file in a bucket:
|
676
|
+
const payload = await this.maybeWriteBodyToTmp(params.body);
|
677
|
+
const signature = ((_a = this.probot) === null || _a === void 0 ? void 0 : _a.webhooks.sign(payload)) || '';
|
678
|
+
await this.cloudTasksClient.createTask({
|
679
|
+
parent: queuePath,
|
680
|
+
task: {
|
681
|
+
httpRequest: {
|
682
|
+
httpMethod: 'POST',
|
683
|
+
headers: {
|
684
|
+
'X-GitHub-Event': params.name || '',
|
685
|
+
'X-GitHub-Delivery': params.id || '',
|
686
|
+
'X-Hub-Signature': signature,
|
687
|
+
'Content-Type': 'application/json',
|
688
|
+
},
|
689
|
+
url,
|
690
|
+
body: Buffer.from(payload),
|
691
|
+
},
|
692
|
+
},
|
693
|
+
});
|
694
|
+
}
|
695
|
+
else {
|
696
|
+
const signature = ((_b = this.probot) === null || _b === void 0 ? void 0 : _b.webhooks.sign('')) || '';
|
697
|
+
await this.cloudTasksClient.createTask({
|
698
|
+
parent: queuePath,
|
699
|
+
task: {
|
700
|
+
httpRequest: {
|
701
|
+
httpMethod: 'POST',
|
702
|
+
headers: {
|
703
|
+
'X-GitHub-Event': params.name || '',
|
704
|
+
'X-GitHub-Delivery': params.id || '',
|
705
|
+
'X-Hub-Signature': signature,
|
706
|
+
'Content-Type': 'application/json',
|
707
|
+
},
|
708
|
+
url,
|
709
|
+
},
|
710
|
+
},
|
711
|
+
});
|
712
|
+
}
|
713
|
+
}
|
714
|
+
/*
|
715
|
+
* Setting the process.env.WEBHOOK_TMP environment variable indicates
|
716
|
+
* that the webhook payload should be written to a tmp file in Cloud
|
717
|
+
* Storage. This allows us to circumvent the 100kb limit on Cloud Tasks.
|
718
|
+
*
|
719
|
+
* @param body
|
720
|
+
*/
|
721
|
+
async maybeWriteBodyToTmp(body) {
|
722
|
+
if (process.env.WEBHOOK_TMP) {
|
723
|
+
const tmp = `${Date.now()}-${uuid_1.v4()}.txt`;
|
724
|
+
const bucket = this.storage.bucket(process.env.WEBHOOK_TMP);
|
725
|
+
const writeable = bucket.file(tmp).createWriteStream({
|
726
|
+
validation: !RUNNING_IN_TEST,
|
727
|
+
});
|
728
|
+
exports.logger.info(`uploading payload to ${tmp}`);
|
729
|
+
into_stream_1.default(body).pipe(writeable);
|
730
|
+
await new Promise((resolve, reject) => {
|
731
|
+
writeable.on('error', reject);
|
732
|
+
writeable.on('finish', resolve);
|
733
|
+
});
|
734
|
+
return JSON.stringify({
|
735
|
+
tmpUrl: tmp,
|
736
|
+
});
|
737
|
+
}
|
738
|
+
else {
|
739
|
+
return body;
|
740
|
+
}
|
741
|
+
}
|
742
|
+
/*
|
743
|
+
* If body has the key tmpUrl, download the original body from a temporary
|
744
|
+
* folder in Cloud Storage.
|
745
|
+
*
|
746
|
+
* @param body
|
747
|
+
*/
|
748
|
+
async maybeDownloadOriginalBody(payload) {
|
749
|
+
if (payload.tmpUrl) {
|
750
|
+
if (!process.env.WEBHOOK_TMP) {
|
751
|
+
throw Error('no tmp directory configured');
|
752
|
+
}
|
753
|
+
const bucket = this.storage.bucket(process.env.WEBHOOK_TMP);
|
754
|
+
const file = bucket.file(payload.tmpUrl);
|
755
|
+
const readable = file.createReadStream({
|
756
|
+
validation: process.env.NODE_ENV !== 'test',
|
757
|
+
});
|
758
|
+
try {
|
759
|
+
const content = await get_stream_1.default(readable);
|
760
|
+
exports.logger.info(`downloaded payload from ${payload.tmpUrl}`);
|
761
|
+
return JSON.parse(content);
|
762
|
+
}
|
763
|
+
catch (e) {
|
764
|
+
if (e.code === 404) {
|
765
|
+
exports.logger.info(`payload not found ${payload.tmpUrl}`);
|
766
|
+
return null;
|
767
|
+
}
|
768
|
+
exports.logger.error(`failed to download from ${payload.tmpUrl}`, e);
|
769
|
+
throw e;
|
770
|
+
}
|
771
|
+
}
|
772
|
+
else {
|
773
|
+
return payload;
|
774
|
+
}
|
775
|
+
}
|
776
|
+
}
|
777
|
+
exports.GCFBootstrapper = GCFBootstrapper;
|
778
|
+
//# sourceMappingURL=gcf-utils.js.map
|