-
Notifications
You must be signed in to change notification settings - Fork 202
Bug 2043733 - Live Github Status for Pull Requests #2635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dklawren
wants to merge
3
commits into
mozilla-bteam:master
Choose a base branch
from
dklawren:2043733
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # | ||
| # This Source Code Form is "Incompatible With Secondary Licenses", as | ||
| # defined by the Mozilla Public License, v. 2.0. | ||
|
|
||
| package Bugzilla::Extension::GitHubPullRequests; | ||
|
|
||
| use 5.10.1; | ||
| use strict; | ||
| use warnings; | ||
|
|
||
| use constant NAME => 'GitHubPullRequests'; | ||
|
|
||
| __PACKAGE__->NAME; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # | ||
| # This Source Code Form is "Incompatible With Secondary Licenses", as | ||
| # defined by the Mozilla Public License, v. 2.0. | ||
|
|
||
| package Bugzilla::Extension::GitHubPullRequests; | ||
|
|
||
| use 5.10.1; | ||
| use strict; | ||
| use warnings; | ||
|
|
||
| use parent qw(Bugzilla::Extension); | ||
|
|
||
| use Bugzilla; | ||
|
|
||
| use constant GITHUB_CONTENT_TYPE => 'text/x-github-pull-request'; | ||
|
|
||
| our $VERSION = '0.01'; | ||
|
|
||
| sub template_before_process { | ||
| my ($self, $args) = @_; | ||
| my $file = $args->{'file'}; | ||
| my $vars = $args->{'vars'}; | ||
|
|
||
| return unless Bugzilla->user->id; | ||
| return unless $file =~ /bug_modal\/(header|edit)\.html\.tmpl$/; | ||
|
|
||
| my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; | ||
| return unless $bug; | ||
|
|
||
| # Note: this only counts linked (non-obsolete) PR attachments. The actual | ||
| # open/closed/merged state isn't known until the WebService queries GitHub, | ||
| # so we can't report an "open" count at template-processing time. | ||
| my $has_prs = 0; | ||
| my $linked_pr_count = 0; | ||
| foreach my $attachment (@{$bug->attachments}) { | ||
| next if $attachment->contenttype ne GITHUB_CONTENT_TYPE; | ||
| next if $attachment->isobsolete; | ||
|
|
||
| # Don't reveal that a private PR attachment exists to users who aren't | ||
| # permitted to see it; the WebService applies the same check. | ||
| next if $attachment->isprivate && !Bugzilla->user->is_insider; | ||
| $has_prs = 1; | ||
| $linked_pr_count++; | ||
| } | ||
|
|
||
| $vars->{github_pull_requests} = $has_prs; | ||
| $vars->{github_linked_pr_count} = $linked_pr_count; | ||
| } | ||
|
|
||
| sub webservice { | ||
| my ($self, $args) = @_; | ||
| $args->{dispatch}->{GitHubPullRequests} | ||
| = 'Bugzilla::Extension::GitHubPullRequests::WebService'; | ||
| } | ||
|
|
||
| __PACKAGE__->NAME; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # | ||
| # This Source Code Form is "Incompatible With Secondary Licenses", as | ||
| # defined by the Mozilla Public License, v. 2.0. | ||
|
|
||
| package Bugzilla::Extension::GitHubPullRequests::WebService; | ||
|
|
||
| use 5.10.1; | ||
| use strict; | ||
| use warnings; | ||
|
|
||
| use base qw(Bugzilla::WebService); | ||
|
|
||
| use Bugzilla::Bug; | ||
| use Bugzilla::Constants; | ||
| use Bugzilla::Error; | ||
| use Bugzilla::Logging; | ||
| use Bugzilla::WebService::Constants; | ||
| use Types::Standard qw(-types); | ||
| use Type::Params qw(compile); | ||
|
|
||
| use JSON qw(decode_json); | ||
| use LWP::UserAgent; | ||
|
|
||
| use constant GITHUB_CONTENT_TYPE => 'text/x-github-pull-request'; | ||
| use constant GITHUB_PR_REGEX => qr{^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)/?$}; | ||
| use constant GITHUB_API_BASE => 'https://api.github.com'; | ||
| use constant GITHUB_API_TIMEOUT => 10; | ||
|
|
||
| # How long (in seconds) to cache a PR's summary in memcached. GitHub's | ||
| # unauthenticated rate limit is low (60 req/hr per IP) and authenticated is | ||
| # 5000/hr, so caching avoids re-fetching the same PR on every bug view. | ||
| use constant GITHUB_CACHE_SECONDS => 300; | ||
|
|
||
| # Cache inaccessible/failed lookups for a shorter period so that persistent | ||
| # failures (rate limiting, outages, private repos) don't re-hit GitHub on every | ||
| # bug view, while still recovering quickly once the PR becomes reachable again. | ||
| use constant GITHUB_ERROR_CACHE_SECONDS => 60; | ||
|
|
||
| use constant READ_ONLY => qw( | ||
| bug_pull_requests | ||
| ); | ||
|
|
||
| use constant PUBLIC_METHODS => qw( | ||
| bug_pull_requests | ||
| ); | ||
|
|
||
| sub bug_pull_requests { | ||
| state $check = compile(Object, Dict [bug_id => Int]); | ||
| my ($self, $params) = $check->(@_); | ||
|
|
||
| my $user = Bugzilla->login(LOGIN_REQUIRED); | ||
|
|
||
| ThrowUserError('invalid_parameter', {name => 'bug_id', err => 'required'}) | ||
| unless $params->{bug_id}; | ||
|
|
||
| my $bug = Bugzilla::Bug->check({id => $params->{bug_id}, cache => 1}); | ||
|
|
||
| my $ua = LWP::UserAgent->new(timeout => GITHUB_API_TIMEOUT); | ||
| $ua->agent('BMO-Bugzilla/1.0'); | ||
| if (Bugzilla->params->{proxy_url}) { | ||
| $ua->proxy('https', Bugzilla->params->{proxy_url}); | ||
| } | ||
|
|
||
| # Authenticate when a token is configured. This raises GitHub's API rate | ||
| # limit from 60 to 5000 requests/hour, avoiding HTTP 403 failures under load. | ||
| my $token = Bugzilla->params->{github_api_token}; | ||
| if ($token) { | ||
| $ua->default_header('Authorization' => "Bearer $token"); | ||
| } | ||
|
|
||
| my @pull_requests; | ||
| foreach my $attachment (@{$bug->attachments}) { | ||
| next if $attachment->contenttype ne GITHUB_CONTENT_TYPE; | ||
| next if $attachment->isobsolete; | ||
|
|
||
| # Don't expose private attachments (and their PR details) to users who | ||
| # aren't permitted to see them. | ||
| next if $attachment->isprivate && !$user->is_insider; | ||
|
|
||
| my $url = $attachment->data; | ||
| $url =~ s/\s+$//; | ||
|
|
||
| my ($owner, $repo, $pr_number) = ($url =~ GITHUB_PR_REGEX); | ||
| unless ($owner && $repo && $pr_number) { | ||
| WARN("GitHub: could not parse PR URL: $url"); | ||
| next; | ||
| } | ||
|
|
||
| my $pr_data = _fetch_pull_request($ua, $owner, $repo, $pr_number); | ||
| push @pull_requests, $pr_data; | ||
| } | ||
|
dklawren marked this conversation as resolved.
|
||
|
|
||
| return {pull_requests => \@pull_requests}; | ||
| } | ||
|
|
||
| sub _fetch_pull_request { | ||
| my ($ua, $owner, $repo, $pr_number) = @_; | ||
|
|
||
| my $url = "https://github.com/$owner/$repo/pull/$pr_number"; | ||
| my $api_url = GITHUB_API_BASE . "/repos/$owner/$repo/pulls/$pr_number"; | ||
|
|
||
| my $base = { | ||
| url => $url, | ||
| number => int($pr_number), | ||
| repo => "$owner/$repo", | ||
| sortkey => int($pr_number), | ||
| }; | ||
|
|
||
| # Return a cached summary if we have a fresh one. | ||
| my $cache_key = "github_pr." . $url; | ||
| my $cached = Bugzilla->memcached->get_data({key => $cache_key}); | ||
| return $cached if defined $cached; | ||
|
|
||
| my $pr_response = _github_get($ua, $api_url); | ||
| unless ($pr_response->{ok}) { | ||
| WARN("GitHub: failed to fetch PR $url: " . $pr_response->{errmsg}); | ||
| return _cache_inaccessible($cache_key, $base); | ||
| } | ||
|
|
||
| my $pr = $pr_response->{data}; | ||
|
|
||
| # GitHub should return a JSON object; anything else (an error object, a | ||
| # list, etc.) means we can't trust the structure, so fall back gracefully. | ||
| unless (ref($pr) eq 'HASH') { | ||
| WARN("GitHub: unexpected response shape for PR $url"); | ||
| return _cache_inaccessible($cache_key, $base); | ||
| } | ||
|
|
||
| my $state; | ||
| if ($pr->{draft}) { | ||
| $state = 'draft'; | ||
| } | ||
| elsif ($pr->{merged_at}) { | ||
| $state = 'merged'; | ||
| } | ||
| elsif ($pr->{state} eq 'closed') { | ||
| $state = 'closed'; | ||
| } | ||
| else { | ||
| $state = 'open'; | ||
| } | ||
|
dklawren marked this conversation as resolved.
|
||
|
|
||
| my @labels = map { $_->{name} } @{$pr->{labels} // []}; | ||
|
|
||
| my $reviews_response = _github_get($ua, $api_url . '/reviews'); | ||
| my @reviews; | ||
| if ($reviews_response->{ok}) { | ||
| @reviews = _summarize_reviews($reviews_response->{data}); | ||
| } | ||
|
|
||
| my $pr_data = { | ||
| %$base, | ||
| title => $pr->{title}, | ||
| state => $state, | ||
| author => ref($pr->{user}) eq 'HASH' ? $pr->{user}{login} : undef, | ||
| reviews => \@reviews, | ||
| labels => \@labels, | ||
| inaccessible => 0, | ||
| }; | ||
|
|
||
| Bugzilla->memcached->set_data( | ||
| {key => $cache_key, value => $pr_data, expires_in => GITHUB_CACHE_SECONDS}); | ||
|
|
||
| return $pr_data; | ||
| } | ||
|
|
||
| sub _cache_inaccessible { | ||
| my ($cache_key, $base) = @_; | ||
|
|
||
| my $error_data = {%$base, inaccessible => 1}; | ||
| Bugzilla->memcached->set_data({ | ||
| key => $cache_key, | ||
| value => $error_data, | ||
| expires_in => GITHUB_ERROR_CACHE_SECONDS, | ||
| }); | ||
|
|
||
| return $error_data; | ||
| } | ||
|
|
||
| sub _github_get { | ||
| my ($ua, $url) = @_; | ||
|
|
||
| my $response = $ua->get( | ||
| $url, | ||
| 'Accept' => 'application/vnd.github+json', | ||
| 'X-GitHub-Api-Version' => '2022-11-28', | ||
| ); | ||
|
|
||
| unless ($response->is_success) { | ||
| return {ok => 0, errmsg => $response->status_line}; | ||
| } | ||
|
|
||
| my $data = eval { decode_json($response->decoded_content) }; | ||
| if ($@) { | ||
| return {ok => 0, errmsg => "JSON parse error: $@"}; | ||
| } | ||
|
|
||
| return {ok => 1, data => $data}; | ||
| } | ||
|
|
||
| sub _summarize_reviews { | ||
| my ($reviews) = @_; | ||
|
|
||
| return () unless ref($reviews) eq 'ARRAY'; | ||
|
|
||
| # Keep only the latest review state per reviewer. | ||
| # Reviews are returned in chronological order so we can just overwrite. | ||
| my %latest; | ||
| my @order; | ||
| for my $review (@{$reviews}) { | ||
| my $login = $review->{user}{login}; | ||
| my $state = $review->{state}; | ||
|
|
||
| # COMMENTED is not a conclusive review state; skip it | ||
| next if $state eq 'COMMENTED'; | ||
|
|
||
| if (!exists $latest{$login}) { | ||
| push @order, $login; | ||
| } | ||
| $latest{$login} = $state; | ||
| } | ||
|
|
||
| return map { {user => $_, state => $latest{$_}} } @order; | ||
| } | ||
|
|
||
| sub rest_resources { | ||
| return [ | ||
| qr{^/githubpr/bug_pull_requests/(\d+)$}, | ||
| { | ||
| GET => { | ||
| method => 'bug_pull_requests', | ||
| params => sub { return {bug_id => $_[0]} }, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| 1; | ||
10 changes: 10 additions & 0 deletions
10
extensions/GitHubPullRequests/template/en/default/github/header.html.tmpl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [%# This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # | ||
| # This Source Code Form is "Incompatible With Secondary Licenses", as | ||
| # defined by the Mozilla Public License, v. 2.0. | ||
| #%] | ||
|
|
||
| [% style_urls.push('extensions/GitHubPullRequests/web/style/github_pull_requests.css') %] | ||
| [% javascript_urls.push('extensions/GitHubPullRequests/web/js/github_pull_requests.js') %] |
38 changes: 38 additions & 0 deletions
38
extensions/GitHubPullRequests/template/en/default/github/table.html.tmpl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| [%# This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # | ||
| # This Source Code Form is "Incompatible With Secondary Licenses", as | ||
| # defined by the Mozilla Public License, v. 2.0. | ||
| #%] | ||
|
|
||
| <table class="github-pull-requests"> | ||
| <thead> | ||
| <tr> | ||
| <th>PR</th> | ||
| <th>Status</th> | ||
| <th>Author</th> | ||
| <th>Reviewers</th> | ||
| <th>Repository</th> | ||
| <th>Labels</th> | ||
| <th>Title</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody class="github-prs-body"> | ||
| <tr class="github-loading-row"> | ||
| <td colspan="7">Loading...</td> | ||
| </tr> | ||
| <tr class="github-loading-error-row bz_default_hidden"> | ||
| <td colspan="7">Error loading GitHub pull requests: | ||
| <span class="github-load-error-string"></span></td> | ||
| </tr> | ||
| </tbody> | ||
| <tbody class="github-show-closed bz_default_hidden"> | ||
| <tr> | ||
| <td colspan="7"> | ||
| <input id="github-show-closed" type="checkbox"> | ||
| <label for="github-show-closed">Show Closed/Merged Pull Requests</label> | ||
| </td> | ||
| </tr> | ||
| </tbody> | ||
| </table> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.