xref: /web-master/github-webhook.php (revision 369ff201)
1<?php
2
3const DRY_RUN = false;
4
5if (!DRY_RUN) {
6    require __DIR__ . '/include/mailer.php';
7}
8
9function verify_signature($requestBody) {
10    if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])){
11        $sig = 'sha1=' . hash_hmac('sha1', $requestBody, getenv('GITHUB_SECRET'));
12        return $sig === $_SERVER['HTTP_X_HUB_SIGNATURE'];
13    }
14    return false;
15}
16
17function get_repo_email($repos, $repoName) {
18    // if we somehow end up receiving a PR for a repo not matching anything send it to systems so that we can fix it
19    $to = 'systems@php.net';
20    foreach ($repos as $repoPrefix => $email) {
21        if (strpos($repoName, $repoPrefix) === 0) {
22            $to = $email;
23        }
24    }
25
26    return $to;
27}
28
29function is_pr($issue) {
30    return strpos($issue->html_url, '/pull/') !== false;
31}
32
33function prep_title($issue, $repoName) {
34    $issueNumber = $issue->number;
35    $title = $issue->title;
36    $type = is_pr($issue) ? 'PR' : 'Issue';
37
38    $subject = sprintf('[%s][%s #%s] - %s', $repoName, $type, $issueNumber, $title);
39
40    return $subject;
41}
42
43function send_mail($to, $subject, $message, MailAddress $from, array $replyTos = []) {
44    printf("Sending mail...\nTo: %s\nFrom: %s <%s>\nSubject: %s\nMessage:\n%s",
45        $to, $from->name, $from->email, $subject, $message);
46
47    if (!DRY_RUN) {
48        mailer($to, $subject, $message, $from, $replyTos);
49    }
50}
51
52function get_first_line($message) {
53    $newLinePos = strpos($message, "\n");
54    return $newLinePos !== false ? substr($message, 0, $newLinePos) : $message;
55}
56
57function handle_bug_close($commit) {
58    $message = $commit->message;
59    $author = $commit->author->username;
60    $committer = $commit->committer->username;
61    $url = $commit->url;
62
63    if (!preg_match_all('/^Fix(?:ed)? (?:bug )?\#([0-9]+)/mi', $message, $matches)) {
64        return;
65    }
66    $bugIds = $matches[1];
67
68    if ($author === $committer) {
69        $blame = $author;
70    } else {
71        $blame = "$author (author) and $committer (committer)";
72    }
73
74    $firstLine = get_first_line($message);
75
76    $comment = <<<MSG
77Automatic comment on behalf of $blame
78Revision: $url
79Log: $firstLine
80MSG;
81
82    foreach ($bugIds as $bugId) {
83        $postData = [
84            'user' => 'git',
85            'id' => (int) $bugId,
86            'ncomment' => $comment,
87            'status' => 'Closed',
88            'MAGIC_COOKIE' => getenv('BUGS_MAGIC_COOKIE'),
89        ];
90        $postData = http_build_query($postData, '', '&');
91
92        echo "Closing bug #$bugId\n";
93        if (!DRY_RUN) {
94            $context = stream_context_create(['http' => [
95                'method' => 'POST',
96                'header' => 'Content-Type: application/x-www-form-urlencoded',
97                'content' => $postData,
98                'timeout' => 5,
99            ]]);
100            $result = file_get_contents('https://bugs.php.net/rpc.php', false, $context);
101            echo "Response: $result\n";
102        }
103    }
104}
105
106function get_commit_mailing_list($repoName) {
107    if ($repoName === 'playground') {
108        return 'nikic@php.net';
109    } else if ($repoName === 'php-src' || $repoName === 'karma') {
110        return 'php-cvs@lists.php.net';
111    } else if ($repoName === 'php-langspec') {
112        return 'standards-vcs@lists.php.net';
113    } else if ($repoName === 'phpruntests' || $repoName === 'pftt2' || $repoName === 'web-qa') {
114        return 'php-qa@lists.php.net';
115    } else if ($repoName === 'systems') {
116        return 'systems@php.net';
117    } else if ($repoName === 'php-gtk-src') {
118        return 'php-gtk-cvs@lists.php.net';
119    } else if ($repoName === 'presentations' || $repoName === 'web-pres2') {
120        return 'pres@lists.php.net';
121    } else if ($repoName === 'doc-base' || $repoName === 'doc-en'
122        || $repoName === 'phd' || $repoName === 'web-doc-editor') {
123        return 'doc-cvs@lists.php.net';
124    } else if ($repoName === 'web-doc') {
125        return 'doc-web@lists.php.net';
126    } else if ($repoName === 'web-pecl') {
127        return 'pecl-cvs@lists.php.net';
128    } else if (strpos($repoName, 'web-') === 0) {
129        return 'php-webmaster@lists.php.net';
130    } else if (strpos($repoName, 'pecl-') === 0) {
131        return 'pecl-cvs@lists.php.net';
132    } else if (strpos($repoName, 'doc-') === 0 && $repoName !== 'doc-gtk') {
133        return str_replace('-', '_', $repoName) . '@lists.php.net';
134    } else {
135        return null;
136    }
137}
138
139function parse_ref($ref) {
140    if (!preg_match('(^refs/([^/]+)/(.+)$)', $ref, $matches)) {
141        return null;
142    }
143
144    return [$matches[1], $matches[2]];
145}
146
147function handle_ref_change_mail($mailingList, $payload) {
148    $repoName = $payload->repository->name;
149    $ref = $payload->ref;
150    $before = $payload->before;
151    $after = $payload->after;
152    $compare = $payload->compare;
153    $pusherName = $payload->pusher->name;
154
155    if (!$parsedRef = parse_ref($ref)) {
156        echo "Unexpected ref format: $ref";
157        return;
158    }
159
160    list($refKind, $refName) = $parsedRef;
161    if ($refKind === 'heads') {
162        $what = "branch $refName";
163    } else if ($refKind === 'tags') {
164        $what = "tag $refName";
165    } else {
166        $what = "unknown ref $ref";
167    }
168
169    if ($payload->created) {
170        $action = "created";
171    } else if ($payload->deleted) {
172        $action = "deleted";
173    } else if ($payload->forced) {
174        $action = "force pushed";
175    } else {
176        $action = "performed unknown action on";
177    }
178
179    $subject = "[$repoName] $action $what";
180
181    $message = ucfirst($action) . " $what in repository $repoName.\n\n";
182    $message .= "Pusher: $pusherName\n";
183    if ($action !== 'created') {
184        $message .= "Before: https://github.com/php/$repoName/commit/$before\n";
185    }
186    if ($action !== 'deleted') {
187        $message .= "After: https://github.com/php/$repoName/commit/$after\n";
188    }
189    $message .= "Compare: $compare\n";
190    $message .= "Tree: https://github.com/php/$repoName/tree/$refName\n";
191    if ($refKind === 'tags') {
192        $message .= "Tag: https://github.com/php/$repoName/releases/tag/$refName\n";
193    }
194
195    send_mail($mailingList, $subject, $message, MailAddress::noReply($pusherName));
196}
197
198function handle_commit_mail($mailingList, $repoName, $ref, $pusherUser, $commit) {
199    $authorUser = isset($commit->author->username) ? $commit->author->username : null;
200    $authorName = $commit->author->name;
201    $committerUser = isset($commit->committer->username) ? $commit->committer->username : null;
202    $committerName = $commit->committer->name;
203    $message = $commit->message;
204    $timestamp = $commit->timestamp;
205    $url = $commit->url;
206    $diffUrl = $url . '.diff';
207    $firstLine = get_first_line($message);
208
209    if (!$parsedRef = parse_ref($ref)) {
210        echo "Unexpected ref format: $ref";
211        return;
212    }
213
214    list(, $refName) = $parsedRef;
215
216    $from = $authorName === $committerName ? $authorName : "$authorName via $committerName";
217    $replyTos = [new MailAddress($commit->author->email, $authorName)];
218    if ($commit->committer->email !== 'noreply@github.com') {
219        $replyTos[] = new MailAddress($commit->committer->email, $committerName);
220    }
221
222    $subject = "[$repoName] $refName: $firstLine";
223    $body = "Author: $authorName" . ($authorUser ? " ($authorUser)" : "") . "\n";
224    if ($authorName !== $committerName) {
225        $body .= "Committer: $committerName" . ($committerUser ? " ($committerUser)" : "") . "\n";
226    }
227    if ($committerUser !== $pusherUser) {
228        $body .= "Pusher: $pusherUser\n";
229    }
230    $body .= "Date: $timestamp\n\n";
231
232    $body .= "Commit: $url\n";
233    $body .= "Raw diff: $diffUrl\n\n";
234    $body .= "$message\n\n";
235
236    $body .= "Changed paths:\n";
237    foreach ($commit->added as $file) {
238        $body .= "  A  $file\n";
239    }
240    foreach ($commit->removed as $file) {
241        $body .= "  D  $file\n";
242    }
243    foreach ($commit->modified as $file) {
244        $body .= "  M  $file\n";
245    }
246    $body .= "\n\n";
247
248    /*$diff = file_get_contents($diffUrl);
249    if (strlen($diff) > 128 * 1024) {
250        $body .= "Diff exceeded maximum size.";
251    } else {
252        $body .= "Diff:\n\n$diff";
253    }*/
254
255    send_mail($mailingList, $subject, $body, MailAddress::noReply($from), $replyTos);
256}
257
258function handle_push_mail($payload) {
259    $repoName = $payload->repository->name;
260    $ref = $payload->ref;
261    $mailingList = get_commit_mailing_list($repoName);
262    if ($mailingList === null) {
263        echo "Not sending mail for $repoName (no mailing list)";
264        return;
265    }
266
267    if ($payload->created || $payload->deleted || $payload->forced) {
268        handle_ref_change_mail($mailingList, $payload);
269    }
270
271    // Ignore commits to PHP-x.y branches for now, to avoid duplicate commits on upwards merges.
272    // TODO: Find a better solution to this problem...
273    if ($repoName === 'php-src' && preg_match('(^refs/heads/PHP-\d\.\d$)', $ref)) {
274        echo "Skipping commit mails for push to $payload->ref of $repoName";
275        return;
276    }
277
278    $pusherName = $payload->pusher->name;
279    foreach ($payload->commits as $commit) {
280        handle_commit_mail($mailingList, $repoName, $ref, $pusherName, $commit);
281    }
282}
283
284$CONFIG = [
285    'repos' => [
286        'php-langspec' => 'standards@lists.php.net',
287        'php-src' => 'git-pulls@lists.php.net',
288        'web-' => 'php-webmaster@lists.php.net',
289        'pecl-' => 'pecl-dev@lists.php.net',
290    ],
291];
292
293if (DRY_RUN) {
294    $body = file_get_contents("php://stdin");
295    $event = $argv[1];
296} else {
297    $body = file_get_contents("php://input");
298    $event = $_SERVER['HTTP_X_GITHUB_EVENT'];
299
300    if (!verify_signature($body)) {
301        header('HTTP/1.1 403 Forbidden');
302        exit;
303    }
304}
305
306$payload = json_decode($body);
307$repoName = $payload->repository->name;
308
309switch ($event) {
310    case 'ping':
311        break;
312    case 'pull_request':
313    case 'issues':
314        $action = $payload->action;
315        $issue = $event == 'issues' ? $payload->issue : $payload->pull_request;
316        $htmlUrl = $issue->html_url;
317
318        $description = $issue->body;
319        $username = $issue->user->login;
320
321        $to = get_repo_email($CONFIG["repos"], $repoName);
322        $subject = prep_title($issue, $repoName);
323        $type = is_pr($issue) ? 'Pull Request' : 'Issue';
324
325        $message = sprintf("You can view the %s on github:\r\n%s", $type, $htmlUrl);
326        switch ($action) {
327            case 'opened':
328                $message .= sprintf(
329                    "\r\n\r\nOpened By: %s\r\n%s Description:\r\n%s",
330                    $username, $type, $description);
331                break;
332            case 'closed':
333                $message .= "\r\n\r\nClosed.";
334                break;
335            case 'reopened':
336                $message .= "\r\n\r\nReopened.";
337                break;
338            case 'assigned':
339            case 'unassigned':
340            case 'labeled':
341            case 'unlabeled':
342            case 'edited':
343            case 'synchronize':
344            case 'milestoned':
345            case 'demilestoned':
346                // Ignore these actions
347                break 2;
348        }
349
350        send_mail($to, $subject, $message, MailAddress::noReply());
351        break;
352
353    case 'pull_request_review_comment':
354    case 'issue_comment':
355        $action = $payload->action;
356        $issue = $event == 'issue_comment' ? $payload->issue : $payload->pull_request;
357        $htmlUrl = $issue->html_url;
358
359        $username = $payload->comment->user->login;
360        $comment = $payload->comment->body;
361
362        $to = get_repo_email($CONFIG["repos"], $repoName);
363        $subject = prep_title($issue, $repoName);
364        $type = is_pr($issue) ? 'Pull Request' : 'Issue';
365
366        $message = sprintf("You can view the %s on github:\r\n%s", $type, $htmlUrl);
367        switch ($action) {
368            case 'created':
369                $message .= sprintf("\r\n\r\nComment by %s:\r\n%s", $username, $comment);
370                break;
371            case 'edited':
372            case 'deleted':
373                // Ignore these actions
374                break 2;
375        }
376
377        send_mail($to, $subject, $message, MailAddress::noReply());
378        break;
379
380    case 'push':
381        if ($payload->ref === 'refs/heads/master') {
382            // Only close bugs for pushes to master.
383            foreach ($payload->commits as $commit) {
384                handle_bug_close($commit);
385            }
386        }
387
388        handle_push_mail($payload);
389        break;
390
391    default:
392        header('HTTP/1.1 501 Not Implemented');
393}
394
395