Add GitHub App connectivity.
Review Request #15144 — Created June 30, 2026 and discarded
This change adds a GitHub App as an alternative to Personal Access
Tokens. This involves a bunch of moving parts:
The actual GitHub App backend, which includes a redirect view that
will trigger the GitHub App Manifest flow, and callback views for
when the app has been created and when the app has been installed
into a user or organization. This ends up creating a hidden
HostingServiceAccountwhich holds the app registration keys, and
then each time the app gets installed (for a user or organization)
will create a newHostingServiceAccountfor that entry.Modifications to the GitHub client to use the app for authentication.
This involves minting short-lived RS256 JWT tokens for the actual API
requests to GitHub.Customized UI parts for the connected services list. This involves:
-
Special templates and flows for the "Connect a service" wizard which
lets users choose how they want to connect, and triggers for the new
app creation flow. -
An override for the entry template which adds an extra controls
panel that either encourages users to create the app (if they have
existing connections using PATs only), or provides a link into
GitHub's settings UI (allowing changes to URLs or transferring the
app registration to another owner).
This also currently includes a stub webhook handler (since GitHub Apps
have a single webhook endpoint for all authorized repositories), as well
as some commented-out bits for future enhancement.
- Connected a GitHub App and installed it into multiple organizations.
- Tested all aspects of the wizard flow, including creation of the app
and reassignment of any PAT-based repositories after app install. - Verified that API access works correctly when authenticating using the
app tokens. - Ran unit tests.
| Summary | ID |
|---|---|
| qmxulzkloxrsnquyqwrxwlrrquzplqpr |
| Description | From | Last Updated |
|---|---|---|
|
Can we make a pydantic model for the GitHub app data (the github_app data stored on the installation account)? |
|
|
|
Ok so thinking about local sites again (this is on my mind from neglecting to really consider local sites when … |
|
|
|
To check, is there a situation where we'd want to establish multiple GitHub hidden app accounts? What's the story on … |
|
|
|
Do we nicely handle things if there's a lot of repositories here? Like having a vertical scrollbar if there's a … |
|
|
|
continuation line under-indented for visual indent Column: 34 Error code: E128 |
|
|
|
'reviewboard.site.models.LocalSite' imported but unused Column: 5 Error code: F401 |
|
|
|
Can we rename to AppManifestResponse. |
|
|
|
Can we rename to InstallationAccountResponse. |
|
|
|
Can you add a Version Added to this. |
|
|
|
In this change the App views were put into reviewboard.hostingsvcs.github.views and I don't see any other changes where you split … |
|
|
|
This needs a Version Added. |
|
|
|
Should we pass local_site=request.get('local_site') here? |
|
|
|
I think there will always be a request.local_site attribute because of our LocalSiteMiddleware. |
|
|
|
Same note about request.local_site always existing. |
|
|
|
line too long (87 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (85 > 79 characters) Column: 80 Error code: E501 |
|
|
|
Missing a docstring. |
|
|
|
Could this be Literal['user', 'organization', '']? |
|
|
|
Same comment as above. |
|
|
|
Seems like this could be Literal['all', 'selected']. |
|
|
|
Can you add a docstring for this. |
|
|
|
This can be removed, we do this below in the if condition. |
|
|
|
We do these isinstance checks on the app data a lot more than we do is_installation_account() or is_app_record_account(). Might be … |
|
|
|
This can be an elif or add back in the blank line before this. |
|
|
|
We can remove local_site here because we do a local_site = app_account.local_site in the view instead of using this value … |
|
|
|
Same comment as above. |
|
|
|
I'm getting a bit mixed up with the namings. We have "app-record accounts" and "installation accounts". A hosting service account … |
|
|
|
Can we pull the URL out before this? Helps keep this readable. |
|
|
|
We access self.client 4 times in this test. Can we pull it out? Same with other tests. |
|
|
|
Can we move the encryption method to the description? We'll ultimately want to move away from encrypt_password() entirely and may … |
|
|
|
Same here. |
|
|
|
Should this be a TypeAlias? |
|
|
|
These are missing a "Version Added". |
|
|
|
Final[str]? |
|
|
|
We have a standard symbol for this. |
|
|
|
What happens if we need to change this data in the future? Will we hit validation problems? We probably should … |
|
|
|
This is missing a description. |
|
|
|
This is missing "Version Added". |
|
|
|
All others have a "False if..." Can we add one for consistency? |
|
|
|
This is missing a "Version Added". |
|
|
|
This is missing a "Version Added". |
|
|
|
This is missing a "Version Added". |
|
|
|
This is missing a "Version Added". |
|
|
|
Can we pull this out? It's easier to read function calls when they're not nested in f-strings. Also would help … |
|
|
|
APIError sorts first. |
|
|
|
This doesn't provide a lot of guidance as to what this means. Can we elaborate? |
|
|
|
Can you move the )) to its own line, so the string is isolated? |
|
|
|
Instead of cast, can we assert isinstance? |
|
|
|
Is this something we want to encrypt? |
|
|
|
These shouldn't be doc comments. Unless these should be on the class. |
|
|
|
Should we log this? |
|
|
|
Can you use the standard Tuple: form here? |
|
|
|
Should this (and the one below) be Sequence? |
|
|
|
We access self.account twice. Let's pull it out. |
|
|
|
Can you use the standard Tuple: form? |
|
|
|
We're mixing up locations for classmethods. These should be higher up. But I have a question about the design here. … |
|
|
|
This is missing the full class path. |
|
|
|
This is hard to read. Can you pull this out into a variable and then iterate over that? |
|
|
|
This is missing a blank line. But this block would all be better as: if is_app_record(...): app_account = account else: … |
|
|
|
This should probably be Mapping. |
|
|
|
We should be escaping variables going into this. |
|
|
|
These are missing Version Added, and should be Final. |
|
|
|
Can you sort these alphabetically? |
|
|
|
Can you document what this is protecting against (the TypeError and ValueError)? It's not clear. |
|
|
|
Can you document this fallback? It's not clear why we'd need it or why this value would work. |
|
|
|
Can you sort all these alphabetically? |
|
|
|
From the above, this could end up being "Review Board (Review Board)". |
|
|
|
Blank line before the comment. |
|
|
|
Alphabetical here too. |
|
|
|
Alphabetical here too. |
|
|
|
And here. |
|
|
|
And here. |
|
|
|
Should we be doing this above if we return an error and this state is set? |
|
|
|
Blank line before the comment. |
|
|
|
Can you pull these out into separate statements? Upcoming work on stronger encryption's going to require more complex statements here. |
|
|
|
Can you alphabetize this? |
|
|
|
We should escape values going into the URL. |
|
|
|
This can be one statement. |
|
|
|
Or validation failed, I'm guessing? |
|
|
|
We should escape the code going into this. |
|
|
|
This should be in alphabetical order. |
|
|
|
We should escape values going into the URL. |
|
|
|
This can be one statement. |
|
|
|
Same question about re: stale state if we hit an error above. |
|
|
|
We should catch an error here. |
|
|
|
Do we need the import to be inline here? If inline, it should be at the top of the function. |
|
|
|
Can you pull this out into a variable so this is more readable/maintainable? |
|
|
|
This needs to be the full class path. |
|
|
|
This needs to be the full class path. |
|
|
|
If this has to be an inline import, can we move it to the top of the function? |
|
|
|
This needs to be the full class path. |
|
|
|
No need for parens. |
|
|
|
Can we move to the top of the function if needed here? |
|
|
|
No need for parens. |
|
|
|
Missing a Version Added. |
|
|
|
Missing a Version Added. |
|
|
|
Missing a Version Added. |
|
|
|
Let's pull this out, because the newer cryptography stuff will involve some multi-line statements here eventually. Same below. |
|
|
|
Let's pull out the encrypt_password calls, because the newer cryptography stuff will involve some multi-line statements here eventually and it … |
|
|
|
Missing a Version Added. |
|
|
|
Missing a Version Added. |
|
|
|
Missing a Version Added. |
|
|
|
We never type this. That should belong to the parent anyway, since we'd only retype in this case to aid … |
|
|
|
Missing a Version Added. |
|
|
|
This is missing docs. |
|
|
|
Can we move the second selector to the next line? |
|
|
|
The type should have parens for the | null = null. |
|
|
|
Optional, but this can just use ?. |
|
|
|
override is a keyword in TypeScript. Can we give this another name? |
|
|
|
This is really dense. Can you break it up? |
|
|
|
Missing indentation here. |
|
|
|
Maybe "via a Personal Access Token"? |
|
|
|
No blank line needed here. |
|
|
|
Everywhere else, this is capitalized "Personal Access Tokens". Some pages also bold this and "GitHub App", and some don't. Not … |
|
|
|
Can you switch this to use an Ink icon, and leave it out of the translation string? |
|
|
|
'gettext.install' imported but unused Column: 1 Error code: F401 |
|
|
|
'collections.abc.Sequence' imported but unused Column: 5 Error code: F401 |
|
|
|
Use the function form of "use strict". Column: 1 Error code: W097 |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 18 Error code: … |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 26 Error code: … |
|
|
|
Expected '{' and instead saw 'if'. Column: 48 Error code: W116 |
|
|
|
Expected '{' and instead saw 'd'. Column: 96 Error code: W116 |
|
|
|
Expected '{' and instead saw 'throw'. Column: 13 Error code: W116 |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 25 Error code: … |
|
|
|
A constructor name should start with an uppercase letter. Column: 88 Error code: W055 |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 29 Error code: … |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 37 Error code: … |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 21 Error code: … |
|
|
|
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 29 Error code: … |
|
|
|
Expected '{' and instead saw 'throw'. Column: 71 Error code: W116 |
|
|
|
Expected '{' and instead saw 'context'. Column: 34 Error code: W116 |
|
|
|
'p' is already defined. Column: 18 Error code: W004 |
|
|
|
Expected '{' and instead saw 'context'. Column: 41 Error code: W116 |
|
|
|
Expected '{' and instead saw 'throw'. Column: 59 Error code: W116 |
|
|
|
Functions declared within loops referencing an outer scoped variable may lead to confusing semantics. (done, extraInitializers, accept) Column: 34 Error … |
|
|
|
Unorthodox function invocation. Column: 40 Error code: W067 |
|
|
|
Expected '{' and instead saw 'continue'. Column: 36 Error code: W116 |
|
|
|
Expected '{' and instead saw 'throw'. Column: 64 Error code: W116 |
|
|
|
Expected a conditional expression and instead saw an assignment. Column: 19 Error code: W084 |
|
|
|
Expected '{' and instead saw 'descriptor'. Column: 41 Error code: W116 |
|
|
|
Expected a conditional expression and instead saw an assignment. Column: 19 Error code: W084 |
|
|
|
Expected '{' and instead saw 'descriptor'. Column: 41 Error code: W116 |
|
|
|
Expected a conditional expression and instead saw an assignment. Column: 19 Error code: W084 |
|
|
|
Expected '{' and instead saw 'initializers'. Column: 42 Error code: W116 |
|
|
|
Expected a conditional expression and instead saw an assignment. Column: 20 Error code: W084 |
|
|
|
Expected '{' and instead saw 'initializers'. Column: 35 Error code: W116 |
|
|
|
Expected '{' and instead saw 'descriptor'. Column: 18 Error code: W116 |
|
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 2 (+7694 -50)
Checks run (2 succeeded)
- Change Summary:
-
- Match accounts based on numeric ID, which stays constant across account renames.
- Make
is_authorized()returnTruefor GitHub AppHostingServiceAccounts.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 3 (+7994 -50)
Checks run (2 succeeded)
- Change Summary:
-
- Rebase on top of /r/15169/
- Redirect correctly back to connect wizard after app install.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 4 (+8108 -44)
Checks run (2 succeeded)
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 5 (+8106 -44)
Checks run (2 succeeded)
- Change Summary:
-
Update for renamed and changed APIs
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 6 (+8094 -44)
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 7 (+8092 -44)
Checks run (2 succeeded)
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 8 (+8088 -44)
Checks run (2 succeeded)
- Change Summary:
-
Do better account lookups for reassigning repos.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 9 (+8250 -44)
Checks run (2 succeeded)
-
-
Can we make a pydantic model for the GitHub app data (the
github_appdata stored on the installation account)? -
Do we nicely handle things if there's a lot of repositories here? Like having a vertical scrollbar if there's a bunch of repositories listed.
-
-
-
-
In this change the App views were put into
reviewboard.hostingsvcs.github.viewsand I don't see any other changes where you split them out into anapp_viewsmodule. -
-
-
-
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 10 (+8278 -44)
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 11 (+8280 -44)
Checks run (2 succeeded)
- Change Summary:
-
Make use of the newly moved
TestCase.login_userinstead of reinventing that wheel. - Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 12 (+8272 -44)
Checks run (2 succeeded)
- Change Summary:
-
Use Pydantic for the app/install records. I think the change was worth it but golly gee.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 13 (+8974 -68)
Checks run (2 succeeded)
-
-
Ok so thinking about local sites again (this is on my mind from neglecting to really consider local sites when implementing document review and then having to go in and add it later), is the idea for GitHub app-record accounts to be local-site dependent? This seems to be the current design because we pass
local_sitefor both the app-record accounts and installation accounts (e.g. inmake_connect_ui_context()we doHostingServiceAccount.objects.filter(service_name=..., local_site=request.local_site).But I know you mentioned possibly having a "GitHub App for all of RBCommons" in a reply to one of my other reviews. And we have
staff_member_requireddecorators on the views, which means only server admins (not local site team admins) will be able to access the views.So even though RBCommons will require its own development effort, I think we need to make some design decisions at this stage since we are currently passing around local sites in this code.
-
-
-
-
-
-
-
We do these
isinstancechecks on the app data a lot more than we dois_installation_account()oris_app_record_account(). Might be worth making someis_installation_data()is_app_record_data()functions? -
We can remove
local_sitehere because we do alocal_site = app_account.local_sitein the view instead of using this value that was passed in. -
-
I'm getting a bit mixed up with the namings. We have "app-record accounts" and "installation accounts". A hosting service account can be either of those.
Can we rename
_create_app_record()to_create_app_record_account()to clarify that. And then rename_create_app_account()to_create_app_installation_account()to specify that its an installation account. Because "app account" could really mean either a record account or installation account, you even name the record account variableapp_account.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 14 (+9080 -68)
Checks run (2 succeeded)
- Change Summary:
-
Reduce some duplication across test cases.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 15 (+9096 -68)
Checks run (2 succeeded)
-
This change is far too big to reason about. I had to stop part-way through views.py after 3 review sessions, and I'm not even half-way through
I have a lot of higher-level questions and lots of code style issues. It's all blurring together and I can't fit this into my head. What I'd suggest is breaking this up into:
- The base shared account state stuff (and I really need to spend time just going over all that)
- Management for that state
- Flow
- Templates
- UI
Something like that.
A lot of the comments I have left are applicable across the change as well.
-
To check, is there a situation where we'd want to establish multiple GitHub hidden app accounts? What's the story on DB import/export there?
-
-
-
Can we move the encryption method to the description? We'll ultimately want to move away from
encrypt_password()entirely and may need to describe that here. -
-
-
-
-
-
What happens if we need to change this data in the future? Will we hit validation problems?
We probably should version the state as well.
-
-
-
-
-
-
-
-
Can we pull this out? It's easier to read function calls when they're not nested in f-strings.
Also would help keep this header on one line.
-
-
-
-
-
-
-
-
-
-
-
-
We're mixing up locations for classmethods. These should be higher up.
But I have a question about the design here. Are these part of the hosting service interface now, these methods?
I think rather than putting connect-related state here, we should do like we do with the forms, have a class that implements the interface and point to it. That keeps things manageable as hosting services grow.
-
-
-
This is missing a blank line. But this block would all be better as:
if is_app_record(...): app_account = account else: ... connected_accounts.append(...) -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Can you pull these out into separate statements? Upcoming work on stronger encryption's going to require more complex statements here.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Let's pull this out, because the newer cryptography stuff will involve some multi-line statements here eventually.
Same below.
-
Let's pull out the
encrypt_passwordcalls, because the newer cryptography stuff will involve some multi-line statements here eventually and it won't be able to be inlined. -
-
-
-
We never type this. That should belong to the parent anyway, since we'd only retype in this case to aid subclasses to avoid typing as a literal.
-
-
-
-
-
-
-
-
-
-
-
Everywhere else, this is capitalized "Personal Access Tokens".
Some pages also bold this and "GitHub App", and some don't. Not sure which we'd want to standardize on.
-
- Change Summary:
-
New revision for interdiff purposes on refactors and fixes before I work on splitting up into multiple changes.
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 16 (+10764 -90)
Checks run (2 failed)
flake8
JSHint
-
Warning: Showing 30 of 154 failures.
-
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 18 Error code: W040
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 26 Error code: W040
-
-
-
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 25 Error code: W040
-
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 29 Error code: W040
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 37 Error code: W040
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 21 Error code: W040
-
If a strict mode function is executed using function invocation, its 'this' value will be undefined. Column: 29 Error code: W040
-
-
-
-
-
-
Functions declared within loops referencing an outer scoped variable may lead to confusing semantics. (done, extraInitializers, accept) Column: 34 Error code: W083
-
-
-
-
-
-
-
-
-
-
-
-
- Commits:
-
Summary ID qmxulzkloxrsnquyqwrxwlrrquzplqpr qmxulzkloxrsnquyqwrxwlrrquzplqpr - Diff:
-
Revision 17 (+9466 -90)
Checks run (2 succeeded)
-
Split up into the following changes:
- /r/15221 Refactor connect UI into a helper object
- /r/15222 Allow linking directly to a specific page for the connect wizard
- /r/15223 GH app data model
- /r/15224 GH app authentication
- /r/15225 Customization capabilities for connect wizard
- /r/15226 GH app webhook view
- /r/15227 GH app connection flow and UI
