Merge ~edugomez/ubuntu/+source/apport:gihub-backend into ubuntu/+source/apport:ubuntu/devel

Proposed by Edu Gómez Escandell
Status: Work in progress
Proposed branch: ~edugomez/ubuntu/+source/apport:gihub-backend
Merge into: ubuntu/+source/apport:ubuntu/devel
Diff against target: 208 lines (+185/-0)
3 files modified
apport/crashdb_impl/github.py (+170/-0)
debian/changelog (+7/-0)
etc/apport/crashdb.conf (+8/-0)
Reviewer Review Type Date Requested Status
git-ubuntu import Pending
Review via email: mp+426557@code.launchpad.net

Commit message

Adding Github backend to apport.

Description of the change

This is still a Work In Progress.

Uses Github's Device flow:
https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flow

This means that the user is given this message:

> Input the following code in your browser:
> URL: https://github.com/login/device
> Code: B45A-7491

And this is used to authenticate.

To post a comment you must log in.
e9761b3... by Edu Gómez Escandell

Reverted unrelated changes

f995d89... by Edu Gómez Escandell

Added relevant TODO comments

869b1d4... by Edu Gómez Escandell

Fixed docstrings and comments

b96d6e8... by Edu Gómez Escandell

Misc imporvements

b7e541a... by Edu Gómez Escandell

Bugfix

Unmerged commits

b7e541a... by Edu Gómez Escandell

Bugfix

b96d6e8... by Edu Gómez Escandell

Misc imporvements

869b1d4... by Edu Gómez Escandell

Fixed docstrings and comments

f995d89... by Edu Gómez Escandell

Added relevant TODO comments

e9761b3... by Edu Gómez Escandell

Reverted unrelated changes

793deca... by Edu Gómez Escandell

Updated changelog

c40550f... by Edu Gómez Escandell

Reverted change to default database

0f7099e... by Edu Gómez Escandell

Started backend

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/apport/crashdb_impl/github.py b/apport/crashdb_impl/github.py
2new file mode 100644
3index 0000000..7e87179
4--- /dev/null
5+++ b/apport/crashdb_impl/github.py
6@@ -0,0 +1,170 @@
7+"""Crash database implementation for Github."""
8+
9+# Copyright (C) 2022 - 2022 Canonical Ltd.
10+# Author: Eduard Gómez Escanell <eduard.gomez.escandell@canonical.com>
11+#
12+# This program is free software; you can redistribute it and/or modify it
13+# under the terms of the GNU General Public License as published by the
14+# Free Software Foundation; either version 2 of the License, or (at your
15+# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
16+# the full text of the license.
17+
18+from dataclasses import dataclass
19+import apport
20+import apport.crashdb
21+import requests
22+import json
23+import time
24+
25+
26+class Github:
27+ __last_request: float = time.time()
28+
29+ @staticmethod
30+ def _stringify(data: dict) -> str:
31+ "Takes a dict and returns it as a string"
32+ string = ""
33+ for key, value in data.items():
34+ string = f"{string}&{key}={value}"
35+ return string
36+
37+ def post(self, url: str, data: str):
38+ headers = {
39+ 'Accept': 'application/vnd.github.v3+json'
40+ }
41+ if self.__access_token:
42+ headers['Authorization'] = f'token {self.__access_token}'
43+ result = requests.post(url, headers=headers, data=data)
44+ self.__last_request = time.time()
45+ result.raise_for_status()
46+ return json.loads(result.text)
47+
48+ def api_authentication(self, url: str, data: dict):
49+ return self.post(url, self._stringify(data))
50+
51+ def api_open_issue(self, owner: str, repo: str, data: dict):
52+ url = f"https://api.github.com/repos/{owner}/{repo}/issues"
53+ return self.post(url, json.dumps(data))
54+
55+ def __init__(self, client_id):
56+ self.__client_id = client_id
57+ self.__authentication_data = None
58+ self.__access_token = None
59+
60+ def __enter__(self):
61+ data = {
62+ "client_id": self.__client_id,
63+ "scope": "public_repo"
64+ }
65+ url = "https://github.com/login/device/code"
66+ response = self.api_authentication(url, data)
67+
68+ # TODO: This should use UI!
69+ print("Input the following code in your browser:")
70+ print(f'URL: {response["verification_uri"]}')
71+ print(f'Code: {response["user_code"]}')
72+
73+ self.__authentication_data = {
74+ "client_id": self.__client_id,
75+ "device_code": f'{response["device_code"]}',
76+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
77+ }
78+ self.__cooldown = response["interval"]
79+ self.__expiry = int(response["expires_in"]) + time.time()
80+
81+ return self
82+
83+ def __exit__(self, *_) -> None:
84+ self.__authentication_data = None
85+ self.__cooldown = 0
86+ self.__expiry = 0
87+
88+ def authentication_complete(self) -> bool:
89+ """
90+ Asks Github if the user has introduced the code already.
91+ It respects the wait-time requested by Github.
92+ """
93+ if not self.__authentication_data:
94+ raise RuntimeError("Authentication not started. Use a with statement to do so")
95+
96+ t = time.time()
97+ waittime = self.__cooldown - (t - self.__last_request)
98+ if t + waittime > self.__expiry:
99+ raise RuntimeError("Failed to log into Github: too much time elapsed.")
100+ if waittime > 0:
101+ time.sleep(waittime)
102+
103+ url = "https://github.com/login/oauth/access_token"
104+ response = self.api_authentication(url, self.__authentication_data)
105+
106+ if "error" in response:
107+ if response["error"] == "authorization_pending":
108+ return False
109+ if response["error"] == "slow_down":
110+ self.__cooldown = int(response["interval"])
111+ return False
112+ raise RuntimeError(f"Unknown error from Github: {response}")
113+ elif "access_token" in response:
114+ self.__access_token = response["access_token"]
115+ return True
116+ raise RuntimeError(f"Unknown response from Github: {response}")
117+
118+@dataclass(frozen=True)
119+class IssueHandle:
120+ url: str
121+
122+class CrashDatabase(apport.crashdb.CrashDatabase):
123+ """
124+ Github crash database.
125+ This is a Apport CrashDB implementation for interacting with Github issues
126+ """
127+
128+ def __init__(self, auth_file, options):
129+ """
130+ Initialize some variables. Login is delayed until necessary.
131+ """
132+ apport.crashdb.CrashDatabase.__init__(self, auth_file, options)
133+ self.repository_owner = options["repository_owner"]
134+ self.repository_name = options["repository_name"]
135+ self.app_id = options["github_app_id"]
136+ self.labels = set(options["labels"])
137+ self.issue_url = None
138+
139+ def _github_login(self) -> Github:
140+ with Github(self.app_id) as github:
141+ while not github.authentication_complete():
142+ pass
143+ return github
144+
145+ def _format_report(self, report: apport.Report) -> dict:
146+ """
147+ Formats report info as markdown and creates Github issue JSON.
148+ """
149+ body = ""
150+ for key, value in report.items():
151+ body += f"**{key}**\n{value}\n\n"
152+
153+ return {
154+ "title": "Issue submitted via apport",
155+ "body": body,
156+ "labels": [l for l in self.labels]
157+ }
158+
159+ def upload(self, report: apport.Report, progress_callback=None) -> IssueHandle:
160+ """Upload given problem report return a handle for it.
161+
162+ In Github, we open an issue.
163+ """
164+ assert self.accepts(report)
165+
166+ data = self._format_report(report)
167+ response = self._github_login().api_open_issue(self.repository_owner, self.repository_name, data)
168+ return IssueHandle(url=response["html_url"])
169+
170+ def get_comment_url(self, report: apport.Report, handle: IssueHandle) -> str:
171+ """
172+ Return an URL that should be opened after report has been uploaded
173+ and upload() returned handle.
174+ """
175+ return handle.url
176+
177diff --git a/debian/changelog b/debian/changelog
178index 78813b6..fa8c39d 100644
179--- a/debian/changelog
180+++ b/debian/changelog
181@@ -1,3 +1,10 @@
182+apport (2.22.0-0ubuntu5) UNRELEASED; urgency=medium
183+
184+ * apport/crashdb_impl: Create Github backend
185+ * etc/apport: Add github backend to crahsdb.conf
186+
187+ -- LAPTOP-AKHDCDJ9 <eduard.gomez.escandell@canonical.com> Fri, 08 Jul 2022 11:44:40 +0200
188+
189 apport (2.22.0-0ubuntu4) kinetic; urgency=medium
190
191 * tests: Use sleep instead of yes for tests
192diff --git a/etc/apport/crashdb.conf b/etc/apport/crashdb.conf
193index 0e7a71a..9f20465 100644
194--- a/etc/apport/crashdb.conf
195+++ b/etc/apport/crashdb.conf
196@@ -30,4 +30,12 @@ databases = {
197 'bug_pattern_url': '/tmp/bugpatterns.xml',
198 'distro': 'debug'
199 },
200+ 'ubuntu-wsl': {
201+ 'impl': 'github',
202+ 'repository_owner': 'EduardGomezEscandell', # TODO: Change to an Ubuntu account
203+ 'repository_name': 'GithubAPI', # TODO: Change to "WSL"
204+ 'github_app_id': 'a654870577ad2a2ab5b1', # TODO: Give App ownership to a Canonical github account
205+ 'labels': ['apport'],
206+ 'distro': 'ubuntu wsl'
207+ },
208 }

Subscribers

People subscribed via source and target branches