gcf-utils 13.2.0 → 13.3.2-beta.0

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.
@@ -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