Skip to content

Commit 71b6baa

Browse files
authored
Merge pull request #657 from keymanapp/auto/A19S29-merge-master-into-staging
auto: A19S29 merge master into staging
2 parents e888900 + 5ca171c commit 71b6baa

4 files changed

Lines changed: 138 additions & 95 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ environment variables. These should be added in script `server/localenv.sh`.
2929
export KEYMANSTATUS_TEAMCITY_TOKEN=[your personal auth token here]
3030
export KEYMANSTATUS_GITHUB_TOKEN=[your personal auth token here]
3131
export KEYMANSTATUS_SENTRY_TOKEN=[your personal auth token here]
32+
# export KEYMANSTATUS_GITHUB_WEBHOOK_SECRET=[github webhook secret here] # optional unless testing the github webhook
3233
```
3334

3435
### @keymanapp-test-bot
@@ -78,6 +79,22 @@ The unit tests will currently stop the back end container before running.
7879
./build.sh test --debug
7980
```
8081

82+
## Webhooks
83+
84+
* GitHub: 2 webhooks are configured to the same endpoint /webhook/github to POST
85+
events from keymanapp organization. These are split to make log review
86+
simpler, because check_run and check_suite are very noisy.
87+
* check_run, check_suite
88+
* issues, issue_comment, milestones, pull requests, pull request reviews
89+
90+
* TeamCity: /webhook/teamcity - for build status
91+
92+
* Discourse: /webhook/discourse - for community.software.sil.org topics
93+
94+
* Sentry: /webhook/sentry - when new errors are logged
95+
96+
* GitHub Testbot app: /webhook/keymanapp-test-bot
97+
8198
## Production setup
8299

83100
This site is deployed to a Kubernetes cluster via configuration in a private

server/code.ts

Lines changed: 10 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import { DataChangeTimingManager } from './util/DataChangeTimingManager.js';
2020

2121
import githubContributionsService from "./services/github/github-contributions.js";
2222
import discourseService from "./services/discourse/discourse.js";
23-
import githubTestContributionsService, { USER_TEST_RESULT_REGEX } from "./services/github/github-test-contributions.js";
23+
import githubTestContributionsService from "./services/github/github-test-contributions.js";
2424
import gitHubMilestonesService from './services/github/github-milestones.js';
2525

2626
import { testUserTestComment } from './keymanapp-test-bot/test-user-test-results-comment.js';
2727
import { performanceLog } from './performance-log.js';
2828
import { consoleLog } from './util/console-log.js';
2929
import { buildVersion } from '../shared/version.js';
30+
import { processGithubWebhookEvent } from './webhooks/github-webhook.js';
3031

3132
sms.install();
3233

@@ -95,7 +96,7 @@ if(debugTestBot) {
9596
const port = 80;
9697
const REFRESH_INTERVAL = environment == Environment.Development ? 180000 : 60000;
9798

98-
const timingManager = new DataChangeTimingManager();
99+
export const timingManager = new DataChangeTimingManager();
99100

100101
/* Logging all requests and responses */
101102

@@ -216,7 +217,7 @@ wsServer.on('connection', socket => {
216217
sendInitialRefreshMessages(socket);
217218
});
218219

219-
function reportError(error) {
220+
export function reportError(error) {
220221
console.error(error);
221222
Sentry.captureMessage(error);
222223
}
@@ -227,84 +228,6 @@ function respondKeymanDataChange() {
227228
.catch(error => reportError(error));
228229
}
229230

230-
async function respondGitHubDataChange(request: express.Request) {
231-
// For now, removing this so that we get all refreshes -- check_run and
232-
// check_suite are busy but only do anything significant if they reference a
233-
// specific PR, so majority of those events should have no real impact. The
234-
// problem here is that we lose the individual refreshes as they are coalesced
235-
// by timingManager, but we need them to keep issues and PRs in sync, so a
236-
// redesign is needed.
237-
//
238-
// if(timingManager.isTooSoon('github', GitHubRefreshRate, () => respondGitHubDataChange(request))) {
239-
// return;
240-
// }
241-
242-
timingManager.start('github');
243-
try {
244-
const event = request?.headers?.['x-github-event'];
245-
const issueNumber = request?.body?.issue?.number;
246-
const pullNumber = request?.body?.pull_request?.number;
247-
const repo = request?.body?.repository?.name;
248-
const action = request?.body?.action;
249-
250-
if((event == 'issues' || event == 'issue_comment') && !request?.body?.issue?.pull_request) {
251-
consoleLog('main', 'github', `POST webhook ${event}.${action} : keymanapp/${repo}#${issueNumber}`);
252-
try {
253-
if(await statusData.refreshGitHubIssueData(repo, issueNumber)) {
254-
sendWsAlert(true, 'github-issues');
255-
256-
await respondGitHubContributionsDataChange(
257-
{issue:true, post:false, pull:false, review:false, test:false}
258-
);
259-
}
260-
} catch(error) {
261-
reportError(error);
262-
}
263-
/* } else if(event == 'check_run') {
264-
// TODO: only refresh data related to check runs?
265-
const prNumbers = request.body?.check_suite?.pull_requests?.map(pr => pr.number) ?? [];
266-
} else if(event == 'check_suite') {
267-
// TODO: only refresh data related to check suites?
268-
const prNumbers = request.body?.check_suite?.pull_requests?.map(pr => pr.number) ?? [];
269-
} else if(event == 'pull_request') {
270-
// TODO: only refresh data related to pull requests
271-
const prNumber = request.body?.pull_request?.number;
272-
*/
273-
} else {
274-
try {
275-
const prNumbers: {hasBeenClosed: boolean, repo: string, pullNumber: number}[] = [];
276-
if(issueNumber && repo && request?.body?.issue?.pull_request) {
277-
prNumbers.push({hasBeenClosed: false, repo, pullNumber: issueNumber});
278-
} else if(pullNumber && repo) {
279-
prNumbers.push({hasBeenClosed: request.body.action == 'closed', repo, pullNumber});
280-
} else if(event == 'check_suite') {
281-
prNumbers.push(...request.body?.check_suite?.pull_requests?.map(pr => ({ hasBeenClosed: false, repo: pr.base.repo.name, pullNumber: pr.number })) ?? []);
282-
} else if(event == 'check_run') {
283-
prNumbers.push(...request.body?.check_run?.check_suite?.pull_requests?.map(pr => ({ hasBeenClosed: false, repo: pr.base.repo.name, pullNumber: pr.number })) ?? []);
284-
}
285-
286-
consoleLog('main', 'github', `POST webhook ${event}.${action} : ${prNumbers.map(p => `keymanapp/${p.repo}#${p.pullNumber}`).join(',')}`);
287-
288-
if(await statusData.refreshGitHubPullRequestsData(prNumbers)) {
289-
sendWsAlert(true, 'github'); // TODO: later just refresh prs
290-
291-
if(event != 'check_run' && event != 'check_suite') {
292-
// We'll only refresh contribution data if the event type merits it -- checks do not impact contributions
293-
const isUserTestComment =
294-
event == 'issue_comment' &&
295-
(request?.body?.comment?.body ?? '').match(USER_TEST_RESULT_REGEX);
296-
await respondGitHubContributionsDataChange({issue:event=='issues', post:false, pull:event=='pull_request', review:event=='pull_request' || event=='pull_request_review', test:isUserTestComment});
297-
}
298-
}
299-
} catch(error) {
300-
reportError(error);
301-
}
302-
}
303-
} finally {
304-
timingManager.finish('github');
305-
}
306-
}
307-
308231
function respondCodeOwnersDataChange() {
309232
return statusData.refreshCodeOwnersData()
310233
.then(hasChanged => sendWsAlert(hasChanged, 'code-owners'))
@@ -317,7 +240,7 @@ function respondSiteLivelinessDataChange() {
317240
.catch(error => reportError(error));
318241
}
319242

320-
function respondGitHubContributionsDataChange(contributionChanges: ContributionChanges) {
243+
export function respondGitHubContributionsDataChange(contributionChanges: ContributionChanges) {
321244
return statusData.refreshGitHubContributionsData('current', contributionChanges)
322245
.then(hasChanged => sendWsAlert(hasChanged, 'github-contributions'))
323246
.catch(error => reportError(error));
@@ -395,10 +318,11 @@ app.use('/', express.static((environment == Environment.Development ? '' : '../'
395318

396319
/* Web hooks */
397320

398-
app.post('/webhook/github', (request: express.Request, response) => {
321+
app.post('/webhook/github', (request: express.Request, response: express.Response) => {
399322
(async () => {
400-
respondGitHubDataChange(request);
401-
slackLGTM(request.body);
323+
if(await processGithubWebhookEvent(request, response)) {
324+
slackLGTM(request.body);
325+
}
402326
})();
403327
response.send('ok');
404328
});
@@ -435,7 +359,7 @@ app.post('/webhook/discourse', (request, response) => {
435359

436360
app.use('/webhook/keymanapp-test-bot', keymanAppTestBotMiddleware);
437361

438-
function sendWsAlert(hasChanged: boolean, message: string): boolean {
362+
export function sendWsAlert(hasChanged: boolean, message: string): boolean {
439363
if(hasChanged) {
440364
wsServer.clients.forEach((client) => {
441365
if(client.readyState === ws.OPEN) {

server/services/github/github-status.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -293,15 +293,20 @@ const repoQuery = (name, after = 'null') => `
293293
async function httppostgh(query, key) {
294294
consoleLog('services', `github-status-${key}`, ' starting refresh');
295295
try {
296-
const response = await fetch('https://api.github.com/graphql', {
297-
method: "POST",
298-
headers: {
299-
Authorization: `Bearer ${github_token}`,
300-
Accept: 'application/vnd.github.antiope-preview+json, application/vnd.github.shadow-cat-preview+json'
301-
},
302-
body: JSON.stringify({query: '{' + query + '}'})
303-
});
304-
296+
let response;
297+
try {
298+
response = await fetch('https://api.github.com/graphql', {
299+
method: "POST",
300+
headers: {
301+
Authorization: `Bearer ${github_token}`,
302+
Accept: 'application/vnd.github.antiope-preview+json, application/vnd.github.shadow-cat-preview+json'
303+
},
304+
body: JSON.stringify({query: '{' + query + '}'})
305+
});
306+
} catch(e) {
307+
console.dir(e);
308+
throw e;
309+
}
305310
if(!response.ok) {
306311
throw new Error(`Failed to query github graphql ${query} for ${key}: ${response.status} ${response.statusText}`);
307312
}

server/webhooks/github-webhook.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import express from 'express';
2+
import { timingManager, sendWsAlert, respondGitHubContributionsDataChange, reportError } from '../code.js';
3+
import { statusData } from '../data/status-data.js';
4+
import { USER_TEST_RESULT_REGEX } from '../services/github/github-test-contributions.js';
5+
import { consoleLog } from '../util/console-log.js';
6+
import { Webhooks } from "@octokit/webhooks";
7+
8+
const webhooks = new Webhooks({
9+
secret: process.env.KEYMANSTATUS_GITHUB_WEBHOOK_SECRET ?? 'unset',
10+
});
11+
12+
export async function processGithubWebhookEvent(request: express.Request, response: express.Response): Promise<boolean> {
13+
// For now, removing this so that we get all refreshes -- check_run and
14+
// check_suite are busy but only do anything significant if they reference a
15+
// specific PR, so majority of those events should have no real impact. The
16+
// problem here is that we lose the individual refreshes as they are coalesced
17+
// by timingManager, but we need them to keep issues and PRs in sync, so a
18+
// redesign is needed.
19+
//
20+
// if(timingManager.isTooSoon('github', GitHubRefreshRate, () => respondGitHubDataChange(request))) {
21+
// return;
22+
// }
23+
timingManager.start('github');
24+
try {
25+
26+
const signature = request.headers["x-hub-signature-256"] as string ?? '';
27+
const body = JSON.stringify(request.body);
28+
29+
if (!(await webhooks.verify(body, signature))) {
30+
response.status(401).send("Unauthorized");
31+
return false;
32+
}
33+
34+
const event = request?.headers?.['x-github-event'];
35+
const issueNumber = request?.body?.issue?.number;
36+
const pullNumber = request?.body?.pull_request?.number;
37+
const repo = request?.body?.repository?.name;
38+
const action = request?.body?.action;
39+
40+
if ((event == 'issues' || event == 'issue_comment') && !request?.body?.issue?.pull_request) {
41+
consoleLog('main', 'github', `POST webhook ${event}.${action} : keymanapp/${repo}#${issueNumber}`);
42+
try {
43+
if (await statusData.refreshGitHubIssueData(repo, issueNumber)) {
44+
sendWsAlert(true, 'github-issues');
45+
46+
await respondGitHubContributionsDataChange(
47+
{ issue: true, post: false, pull: false, review: false, test: false }
48+
);
49+
}
50+
} catch (error) {
51+
reportError(error);
52+
}
53+
/* } else if(event == 'check_run') {
54+
// TODO: only refresh data related to check runs?
55+
const prNumbers = request.body?.check_suite?.pull_requests?.map(pr => pr.number) ?? [];
56+
} else if(event == 'check_suite') {
57+
// TODO: only refresh data related to check suites?
58+
const prNumbers = request.body?.check_suite?.pull_requests?.map(pr => pr.number) ?? [];
59+
} else if(event == 'pull_request') {
60+
// TODO: only refresh data related to pull requests
61+
const prNumber = request.body?.pull_request?.number;
62+
*/
63+
} else {
64+
try {
65+
const prNumbers: { hasBeenClosed: boolean; repo: string; pullNumber: number; }[] = [];
66+
if (issueNumber && repo && request?.body?.issue?.pull_request) {
67+
prNumbers.push({ hasBeenClosed: request.body.issue.state == 'closed', repo, pullNumber: issueNumber });
68+
} else if (pullNumber && repo) {
69+
prNumbers.push({ hasBeenClosed: request.body.action == 'closed', repo, pullNumber });
70+
} else if (event == 'check_suite') {
71+
prNumbers.push(...request.body?.check_suite?.pull_requests?.map(pr => ({ hasBeenClosed: false, repo: pr.base.repo.name, pullNumber: pr.number })) ?? []);
72+
} else if (event == 'check_run') {
73+
prNumbers.push(...request.body?.check_run?.check_suite?.pull_requests?.map(pr => ({ hasBeenClosed: false, repo: pr.base.repo.name, pullNumber: pr.number })) ?? []);
74+
}
75+
76+
consoleLog('main', 'github', `POST webhook ${event}.${action} : ${prNumbers.map(p => `keymanapp/${p.repo}#${p.pullNumber}`).join(',')}`);
77+
78+
if (await statusData.refreshGitHubPullRequestsData(prNumbers)) {
79+
sendWsAlert(true, 'github'); // TODO: later just refresh prs
80+
81+
if (event != 'check_run' && event != 'check_suite') {
82+
// We'll only refresh contribution data if the event type merits it -- checks do not impact contributions
83+
const isUserTestComment = event == 'issue_comment' &&
84+
(request?.body?.comment?.body ?? '').match(USER_TEST_RESULT_REGEX);
85+
await respondGitHubContributionsDataChange({ issue: event == 'issues', post: false, pull: event == 'pull_request', review: event == 'pull_request' || event == 'pull_request_review', test: isUserTestComment });
86+
}
87+
}
88+
} catch (error) {
89+
reportError(error);
90+
}
91+
}
92+
} finally {
93+
timingManager.finish('github');
94+
}
95+
return true;
96+
}
97+

0 commit comments

Comments
 (0)