Skip to content

Commit 7845c2a

Browse files
committed
feat(github): register Weblate GitHub App via manifest flow
Add one-click registration from Manage → Weblate GitHub Apps. The form POSTs a GitHub App manifest containing the required permissions, events, and webhook URL; GitHub returns the App ID, slug, private key, and webhook secret, which Weblate stores in a new GitHubAppCredentials table. Also, move the github app creation from project level to workspace level.
1 parent e338631 commit 7845c2a

25 files changed

Lines changed: 3631 additions & 415 deletions

docs/admin/code-hosting.rst

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -237,22 +237,127 @@ access to the repository. To push changes back, you still need to add
237237
the Hosted Weblate :guilabel:`weblate` GitHub user as a collaborator with write
238238
access, see :ref:`hosted-push`.
239239

240-
For self-hosted Weblate, create a GitHub App on each GitHub host you want
241-
Weblate to connect to and configure :setting:`GITHUB_APP_CREDENTIALS` with the
242-
app ID, app slug, private key, and webhook secret from that app.
243-
244-
When creating the GitHub App, copy :guilabel:`App ID` from the app settings,
245-
use the slug from the app URL as ``app_slug``, generate a private key and use
246-
its PEM contents as ``private_key``, and choose a webhook secret to store as
247-
``webhook_secret``.
240+
For self-hosted Weblate, you can either use the in-app registration flow to
241+
create the GitHub App with one click (recommended), or create the App manually
242+
on each GitHub host you want Weblate to connect to and configure
243+
:setting:`GITHUB_APP_CREDENTIALS` with the app ID, app slug, private key, and
244+
webhook secret from that app.
245+
246+
Registering the GitHub App from Weblate
247+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
248+
249+
The fastest way to add the GitHub App is to let Weblate generate a GitHub App
250+
manifest with the correct permissions, events, and webhook URL pre-filled:
251+
252+
1. Sign in to Weblate with an account that has management access.
253+
2. Open :guilabel:`Manage → Connected GitHub accounts → Register Weblate GitHub
254+
App`.
255+
3. Fill in the form. The :guilabel:`GitHub host` defaults to ``github.com``;
256+
change it to your GitHub Enterprise hostname if needed. Leave
257+
:guilabel:`Organization` blank to register the App under your personal
258+
account, or enter an organization slug to register it under that org.
259+
4. Click :guilabel:`Continue to GitHub` and confirm on GitHub's
260+
:guilabel:`Create GitHub App` page (you can still rename the App there).
261+
5. GitHub redirects back to Weblate, which exchanges the temporary code for
262+
the App ID, private key, webhook secret, and slug and stores them in the
263+
database. The :guilabel:`Connect GitHub account` button is available
264+
immediately afterwards.
265+
266+
Database-stored credentials always take precedence over values in
267+
:setting:`GITHUB_APP_CREDENTIALS`, so the in-app flow can be used to update
268+
credentials on a Weblate instance that originally configured the App through
269+
settings.
270+
271+
Registering the GitHub App manually
272+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
273+
274+
Open :guilabel:`Settings → Developer settings → GitHub Apps → New GitHub App`
275+
on the GitHub host where the app should live, and fill in the following:
276+
277+
* :guilabel:`GitHub App name` — for example ``Weblate``.
278+
* :guilabel:`Homepage URL` — your Weblate instance URL.
279+
* :guilabel:`Callback URL` —
280+
``https://weblate.example.com/integrations/github/setup/``. This is where
281+
GitHub sends users after they pick the account to install on; without it,
282+
GitHub leaves them on the GitHub installation settings page.
283+
* :guilabel:`Redirect on update` — check this so updates to an existing
284+
installation also redirect to the callback URL above and refresh the cached
285+
repository list in Weblate.
286+
* :guilabel:`Setup URL (optional)` — same value as the callback URL.
287+
* :guilabel:`Webhook URL` — ``https://weblate.example.com/hooks/github-app/``.
288+
For multiple GitHub hosts add ``?host=github.example.com`` (see the
289+
:ref:`webhook section <code-hosting-github-app-webhook>` below).
290+
* :guilabel:`Webhook secret` — a long random string. Store the same value as
291+
``webhook_secret`` in :setting:`GITHUB_APP_CREDENTIALS`.
292+
293+
Required :guilabel:`Repository permissions`:
294+
295+
* :guilabel:`Contents` — :guilabel:`Read and write` (clone, push translation
296+
branches).
297+
* :guilabel:`Metadata` — :guilabel:`Read-only` (mandatory baseline).
298+
* :guilabel:`Pull requests` — :guilabel:`Read and write` (open translation
299+
pull requests).
300+
* :guilabel:`Workflows` — :guilabel:`Read and write` (only required if any of
301+
the synced repositories contain GitHub Actions workflow files; otherwise
302+
pushes to those paths are rejected).
303+
304+
Required :guilabel:`Subscribe to events`:
305+
306+
* :guilabel:`Installation target` and :guilabel:`Meta` (account/app lifecycle).
307+
* :guilabel:`Push` (translation source updates).
308+
309+
Choose :guilabel:`Any account` if you want the app to be installable on other
310+
accounts, or :guilabel:`Only on this account` to restrict it.
311+
312+
After registering, generate a private key, copy :guilabel:`App ID` and the slug
313+
from the app URL, and feed all four values into
314+
:setting:`GITHUB_APP_CREDENTIALS`:
315+
316+
.. code-block:: python
317+
318+
GITHUB_APP_CREDENTIALS = {
319+
"github.com": {
320+
"app_id": "12345",
321+
"app_slug": "your-app-slug",
322+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
323+
"webhook_secret": "the same secret you set in the app",
324+
},
325+
}
326+
327+
GitHub only offers accounts where the signed-in GitHub user can install or
328+
request the app. If an organization is not shown during the install flow, check
329+
the user's organization role and the organization's GitHub App installation
330+
restrictions. On GitHub.com, public apps can be installed on other accounts;
331+
private apps can only be installed on the account that owns the app.
332+
333+
Connecting a workspace
334+
^^^^^^^^^^^^^^^^^^^^^^
335+
336+
Connected GitHub accounts are bound to a Weblate :ref:`workspace`. A user with
337+
project administration rights for any project in a workspace can connect a
338+
GitHub account on that workspace. After connecting, every project in the
339+
workspace can import components from repositories the app installation has
340+
access to.
341+
342+
Projects that are not in a workspace cannot connect a GitHub App installation.
343+
344+
Components imported through the GitHub App flow use the dedicated
345+
:guilabel:`GitHub (via Weblate GitHub app)` VCS backend. The component
346+
settings UI keeps the repository URL read-only to prevent the App-issued
347+
credentials from being redirected to an unrelated repository.
348+
349+
.. _code-hosting-github-app-webhook:
350+
351+
App webhook URL
352+
^^^^^^^^^^^^^^^
248353

249354
In the GitHub App webhook settings, enable webhooks and use the Weblate
250-
GitHub hook URL as the :guilabel:`Webhook URL`. When Weblate is configured for
251-
multiple GitHub hosts, include the GitHub host in the URL query string:
355+
GitHub App hook URL as the :guilabel:`Webhook URL`. When Weblate is configured
356+
for multiple GitHub hosts, include the GitHub host in the URL query string:
252357

253358
.. code-block:: text
254359
255-
https://weblate.example.com/hooks/github/?host=github.example.com
360+
https://weblate.example.com/hooks/github-app/?host=github.example.com
256361
257362
The ``host`` value has to match the host key in :setting:`GITHUB_APP_CREDENTIALS`.
258363
Use ``github.com`` for GitHub.com and the web hostname for GitHub Enterprise
@@ -284,6 +389,7 @@ types and consumes just the :guilabel:`push` event.
284389
.. seealso::
285390

286391
* :http:post:`/hooks/github/`
392+
* :http:post:`/hooks/github-app/`
287393
* :setting:`GITHUB_APP_CREDENTIALS`
288394
* :ref:`hosted-push`
289395

docs/admin/config.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,13 +1092,23 @@ Server.
10921092
Use the :guilabel:`App ID` from the GitHub App settings as ``app_id``, the
10931093
slug from the app URL as ``app_slug``, a generated private key PEM as
10941094
``private_key``, and the configured app webhook secret as ``webhook_secret``.
1095+
GitHub only offers accounts where the signed-in GitHub user can install or
1096+
request the app. If an organization is not shown during the install flow, check
1097+
the user's organization role and the organization's GitHub App installation
1098+
restrictions. On GitHub.com, public apps can be installed on other accounts;
1099+
private apps can only be installed on the account that owns the app.
10951100

1096-
The :guilabel:`Webhook URL` configured in the GitHub App should include the
1097-
matching host when Weblate is configured with multiple GitHub hosts:
1101+
Each connected GitHub account is associated with one Weblate project. Project
1102+
managers can then add components from repositories exposed by that app
1103+
installation.
1104+
1105+
The :guilabel:`Webhook URL` configured in the GitHub App should use the
1106+
GitHub App hook endpoint and include the matching host when Weblate is
1107+
configured with multiple GitHub hosts:
10981108

10991109
.. code-block:: text
11001110
1101-
https://weblate.example.com/hooks/github/?host=github.example.com
1111+
https://weblate.example.com/hooks/github-app/?host=github.example.com
11021112
11031113
.. seealso::
11041114

weblate/templates/trans/component_create.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,15 @@
192192
class="btn btn-outline-primary btn-sm float-end">{% translate "Add or configure account" %}</a>
193193
{% endif %}
194194
</p>
195+
{% if github_app_install_url %}
196+
{% include "vcs/github_install_help.html" %}
197+
{% endif %}
195198
{% if github_app_repositories %}
196199
<table class="table table-striped">
197200
<thead>
198201
<tr>
199202
<th>{% translate "Repository" %}</th>
203+
<th>{% translate "Workspace" %}</th>
200204
<th>{% translate "Account" %}</th>
201205
<th>{% translate "Branch" %}</th>
202206
<th>{% translate "Visibility" %}</th>
@@ -213,6 +217,7 @@
213217
<small class="text-body-secondary">{{ repo.description }}</small>
214218
{% endif %}
215219
</td>
220+
<td>{{ repo.workspace_name }}</td>
216221
<td>{{ repo.account_name }}</td>
217222
<td>{{ repo.default_branch }}</td>
218223
<td>
@@ -223,7 +228,7 @@
223228
{% endif %}
224229
</td>
225230
<td>
226-
<a href="{% url 'create-component-vcs' %}?repo={{ repo.clone_url }}&branch={{ repo.default_branch }}&vcs=github"
231+
<a href="{% url 'create-component-vcs' %}?repo={{ repo.clone_url }}&branch={{ repo.default_branch }}&vcs=github-app{% if github_app_target_project %}&project={{ github_app_target_project }}{% endif %}{% if selected_category %}&category={{ selected_category }}{% endif %}"
227232
class="btn btn-outline-primary btn-sm">{% translate "Import" %}</a>
228233
</td>
229234
</tr>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
{% extends "base.html" %}
2+
3+
{% load i18n %}
4+
5+
{% block breadcrumbs %}
6+
<li class="breadcrumb-item">
7+
<a href="{% url 'manage' %}">{% translate "Manage" %}</a>
8+
</li>
9+
<li class="breadcrumb-item">
10+
<a href="{% url 'manage-github-accounts' %}">{% translate "Connected GitHub accounts" %}</a>
11+
</li>
12+
<li class="breadcrumb-item">{% translate "Register Weblate GitHub App" %}</li>
13+
{% endblock breadcrumbs %}
14+
15+
{% block content %}
16+
17+
<div class="card mb-3">
18+
<div class="card-header">
19+
<h4 class="card-title">{% translate "Register Weblate GitHub App" %}</h4>
20+
</div>
21+
<div class="card-body">
22+
<p>
23+
{% blocktranslate trimmed %}
24+
Submit the form below to create a GitHub App with all the permissions
25+
and events Weblate needs already filled in. After you confirm the
26+
App's name on GitHub, you will be redirected back here and the
27+
credentials will be stored automatically.
28+
{% endblocktranslate %}
29+
</p>
30+
31+
{% if existing_hosts %}
32+
<div class="alert {% if host_already_registered %}alert-danger{% else %}alert-info{% endif %}">
33+
{% if host_already_registered %}
34+
{% blocktranslate trimmed with host=hostname %}
35+
A Weblate GitHub App is already registered for <code>{{ host }}</code>.
36+
Remove it from the Weblate GitHub Apps page before registering a
37+
replacement.
38+
{% endblocktranslate %}
39+
{% else %}
40+
{% translate "GitHub hosts that already have a Weblate GitHub App registered:" %}
41+
{% for host in existing_hosts %}
42+
<code>{{ host }}</code>
43+
{% if not forloop.last %},{% endif %}
44+
{% endfor %}
45+
{% endif %}
46+
</div>
47+
{% endif %}
48+
49+
<form method="post" action="{% url 'github-app-register-submit' %}">
50+
{% csrf_token %}
51+
<div class="mb-3 row">
52+
<label for="register-host" class="col-sm-3 col-form-label">{% translate "GitHub host" %}</label>
53+
<div class="col-sm-9">
54+
<input type="text"
55+
id="register-host"
56+
name="host"
57+
class="form-control"
58+
value="{{ hostname }}"
59+
placeholder="github.com">
60+
<div class="form-text">
61+
{% blocktranslate trimmed %}
62+
Use github.com for GitHub.com, or the web hostname for GitHub
63+
Enterprise Server. The webhook URL will use this host
64+
automatically.
65+
{% endblocktranslate %}
66+
</div>
67+
</div>
68+
</div>
69+
<div class="mb-3 row">
70+
<label for="register-org" class="col-sm-3 col-form-label">{% translate "Organization (optional)" %}</label>
71+
<div class="col-sm-9">
72+
<input type="text"
73+
id="register-org"
74+
name="org"
75+
class="form-control"
76+
value="{{ org }}"
77+
placeholder="{% translate "Leave blank to register on your personal account" %}">
78+
<div class="form-text">
79+
{% blocktranslate trimmed %}
80+
Enter a GitHub organization slug to register the App under
81+
that organization instead of your personal account.
82+
{% endblocktranslate %}
83+
</div>
84+
</div>
85+
</div>
86+
<div class="mb-3 row">
87+
<label for="register-name" class="col-sm-3 col-form-label">{% translate "App name" %}</label>
88+
<div class="col-sm-9">
89+
<input type="text"
90+
id="register-name"
91+
name="name"
92+
class="form-control"
93+
value="{{ name }}"
94+
maxlength="{{ name_max_length }}"
95+
required>
96+
<div class="form-text">
97+
{% blocktranslate trimmed with limit=name_max_length %}
98+
GitHub App names must be unique and at most {{ limit }}
99+
characters. You will still be able to change the name on
100+
GitHub before confirming.
101+
{% endblocktranslate %}
102+
</div>
103+
</div>
104+
</div>
105+
<div class="mb-3 row">
106+
<label class="col-sm-3 col-form-label">{% translate "Visibility" %}</label>
107+
<div class="col-sm-9">
108+
<div class="form-check">
109+
<input type="checkbox"
110+
id="register-public"
111+
name="public"
112+
value="1"
113+
class="form-check-input"
114+
{% if public %}checked{% endif %}>
115+
<label class="form-check-label" for="register-public">
116+
{% translate "Public (any GitHub account can install it)" %}
117+
</label>
118+
</div>
119+
<div class="form-text">
120+
{% blocktranslate trimmed %}
121+
Required to install the App on more than one user or
122+
organization. Public here only means the App is not
123+
restricted to its owner; it does not list the App on the
124+
GitHub Marketplace.
125+
{% endblocktranslate %}
126+
</div>
127+
</div>
128+
</div>
129+
<div class="mb-3 row">
130+
<label class="col-sm-3 col-form-label">{% translate "Webhook URL" %}</label>
131+
<div class="col-sm-9">
132+
<input type="text" class="form-control" value="{{ webhook_url }}" disabled>
133+
</div>
134+
</div>
135+
<div class="mb-3 row">
136+
<label class="col-sm-3 col-form-label">{% translate "Permissions" %}</label>
137+
<div class="col-sm-9">
138+
<ul class="list-unstyled mb-0">
139+
{% for permission, level in permissions.items %}
140+
<li>
141+
<code>{{ permission }}</code> — {{ level }}
142+
</li>
143+
{% endfor %}
144+
</ul>
145+
</div>
146+
</div>
147+
<div class="mb-3 row">
148+
<label class="col-sm-3 col-form-label">{% translate "Subscribed events" %}</label>
149+
<div class="col-sm-9">
150+
<ul class="list-unstyled mb-0">
151+
{% for event in events %}
152+
<li>
153+
<code>{{ event }}</code>
154+
</li>
155+
{% endfor %}
156+
</ul>
157+
</div>
158+
</div>
159+
<div class="d-flex gap-2">
160+
<button type="submit"
161+
class="btn btn-primary"
162+
{% if host_already_registered %}disabled{% endif %}>{% translate "Continue to GitHub" %}</button>
163+
<a href="{% url 'manage-github-accounts' %}" class="btn btn-outline-secondary">{% translate "Cancel" %}</a>
164+
</div>
165+
</form>
166+
</div>
167+
</div>
168+
169+
<div class="card">
170+
<div class="card-header">
171+
<h4 class="card-title">{% translate "Manifest preview" %}</h4>
172+
</div>
173+
<div class="card-body">
174+
<p class="text-body-secondary small">
175+
{% translate "The JSON document below is the manifest that GitHub will use to pre-fill the new App's settings." %}
176+
</p>
177+
<pre class="mb-0"><code>{{ manifest_json }}</code></pre>
178+
</div>
179+
</div>
180+
181+
{% endblock content %}

0 commit comments

Comments
 (0)