squad 1.65__py3-none-any.whl → 1.67__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
squad/api/ci.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from django.http import HttpResponse, HttpResponseBadRequest
2
4
  from django.shortcuts import get_object_or_404
3
5
  from django.views.decorators.csrf import csrf_exempt
@@ -8,6 +10,7 @@ from squad.ci.exceptions import SubmissionIssue
8
10
  from squad.ci.tasks import submit, fetch
9
11
  from squad.ci.models import Backend, TestJob
10
12
  from squad.core.utils import log_addition
13
+ from squad.core.models import Project
11
14
 
12
15
 
13
16
  @require_http_methods(["POST"])
@@ -149,3 +152,42 @@ def resubmit_job(request, test_job_id, method='resubmit'):
149
152
  @csrf_exempt
150
153
  def force_resubmit_job(request, test_job_id):
151
154
  return resubmit_job(request, test_job_id, method='force_resubmit')
155
+
156
+
157
+ @require_http_methods(["POST"])
158
+ @csrf_exempt
159
+ def fetch_job(request, group_slug, project_slug, version, environment_slug, backend_name):
160
+ try:
161
+ backend = Backend.objects.get(name=backend_name)
162
+ except Backend.DoesNotExist:
163
+ return HttpResponseBadRequest("requested backend does not exist")
164
+
165
+ if not backend.supports_callbacks():
166
+ return HttpResponseBadRequest("requested backend does not support callbacks")
167
+
168
+ try:
169
+ project = Project.objects.get(slug=project_slug, group__slug=group_slug)
170
+ except Project.DoesNotExist:
171
+ return HttpResponseBadRequest("group/project does not exist")
172
+
173
+ try:
174
+ backend.validate_callback(request, project)
175
+ except Exception as e:
176
+ return HttpResponseBadRequest(f"request is not valid for this backend: {e}")
177
+
178
+ environment, _ = project.environments.get_or_create(slug=environment_slug)
179
+ build, _ = project.builds.get_or_create(version=version)
180
+
181
+ try:
182
+ payload = json.loads(request.body)
183
+ except Exception as e:
184
+ return HttpResponseBadRequest(f"payload failed to parse as json: {e}")
185
+
186
+ try:
187
+ test_job = backend.process_callback(payload, build, environment)
188
+ except Exception as e:
189
+ return HttpResponseBadRequest(f"malformed callback payload: {e}")
190
+
191
+ fetch.delay(test_job.id)
192
+
193
+ return HttpResponse(test_job.id, status=201)
squad/api/urls.py CHANGED
@@ -21,6 +21,7 @@ urlpatterns = [
21
21
  url(r'^submit/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), views.add_test_run),
22
22
  url(r'^submitjob/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.submit_job),
23
23
  url(r'^watchjob/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.watch_job),
24
+ url(r'^fetchjob/(%s)/(%s)/(%s)/(%s)/(%s)' % (group_slug_pattern, slug_pattern, slug_pattern, slug_pattern, slug_pattern), ci.fetch_job),
24
25
  url(r'^data/(%s)/(%s)' % (group_slug_pattern, slug_pattern), data.get),
25
26
  url(r'^resubmit/([0-9]+)', ci.resubmit_job),
26
27
  url(r'^forceresubmit/([0-9]+)', ci.force_resubmit_job),
squad/ci/backend/fake.py CHANGED
@@ -65,3 +65,6 @@ class Backend(object):
65
65
 
66
66
  def check_job_definition(self, definition):
67
67
  return True
68
+
69
+ def supports_callbacks(self):
70
+ return False
squad/ci/backend/lava.py CHANGED
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+ import aiohttp
1
3
  import json
2
4
  import re
3
5
  import requests
@@ -8,13 +10,14 @@ import yaml
8
10
  import xmlrpc
9
11
  import zmq
10
12
 
13
+ from asgiref.sync import sync_to_async
11
14
  from dateutil.parser import isoparse
12
15
  from contextlib import contextmanager
13
16
  from io import BytesIO, TextIOWrapper, StringIO
14
17
  from zmq.utils.strtypes import u
15
18
 
16
19
  from xmlrpc import client as xmlrpclib
17
- from urllib.parse import urlsplit, urljoin
20
+ from urllib.parse import urlsplit, urljoin, urlparse
18
21
 
19
22
 
20
23
  from squad.ci.models import TestJob
@@ -160,6 +163,41 @@ class Backend(BaseBackend):
160
163
  raise FetchIssue(self.url_remove_token(str(fault)))
161
164
 
162
165
  def listen(self):
166
+ if not self.listen_websocket():
167
+ self.listen_zmq()
168
+
169
+ def listen_websocket(self):
170
+ async def handler():
171
+ url = urlparse(self.data.url)
172
+ ws_url = f"{url.scheme}://{self.data.username}:{self.data.token}@{url.netloc}/ws/"
173
+ try:
174
+ while True:
175
+ try:
176
+ async with aiohttp.ClientSession() as session:
177
+ self.log_debug(f"connecting to {url.scheme}://{url.netloc}/ws/")
178
+ async with session.ws_connect(ws_url, heartbeat=30) as ws:
179
+ async for msg in ws:
180
+ if msg.type == aiohttp.WSMsgType.TEXT:
181
+ try:
182
+ (topic, uuid, dt, username, data) = (m for m in msg.json()[:])
183
+ data = json.loads(data)
184
+ if "error" in data:
185
+ raise aiohttp.ClientError(data["error"])
186
+ except ValueError:
187
+ continue
188
+ await sync_to_async(self.receive_event)(topic, data)
189
+ await asyncio.sleep(1)
190
+ except aiohttp.ClientError as e:
191
+ self.log_warn(f"Failed to start client: {e}")
192
+ return False
193
+ except Exception as e:
194
+ # Fall back to ZMQ
195
+ self.log_warn(f"Failed to maintain websocket connection: {e}")
196
+ return False
197
+
198
+ asyncio.run(handler())
199
+
200
+ def listen_zmq(self):
163
201
  listener_url = self.get_listener_url()
164
202
  if not listener_url:
165
203
  self.log_warn("Can't connect, no listener URL")
squad/ci/backend/null.py CHANGED
@@ -104,6 +104,25 @@ class Backend(object):
104
104
  """
105
105
  raise NotImplementedError
106
106
 
107
+ def supports_callbacks(self):
108
+ """
109
+ Returns True if this backend supports callbacks, False otherwise
110
+ """
111
+ return False
112
+
113
+ def validate_callback(self, request, project):
114
+ """
115
+ Raises an exception in case the request does not pass the validation
116
+ """
117
+ raise NotImplementedError
118
+
119
+ def process_callback(self, json_payload, build, environment, backend):
120
+ """
121
+ Returns a test_job if processing callback's payload fine, or raise exceptions
122
+ if something isn't right
123
+ """
124
+ raise NotImplementedError
125
+
107
126
  def format_message(self, msg):
108
127
  if self.data and hasattr(self.data, "name"):
109
128
  return self.data.name + ': ' + msg
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import hashlib
2
3
  import logging
3
4
  import re
@@ -8,8 +9,15 @@ import json
8
9
  from functools import reduce
9
10
  from urllib.parse import urljoin
10
11
 
12
+ from cryptography.hazmat.primitives.asymmetric import ec
13
+ from cryptography.hazmat.primitives import (
14
+ hashes,
15
+ serialization,
16
+ )
17
+
11
18
  from squad.ci.backend.null import Backend as BaseBackend
12
19
  from squad.ci.exceptions import FetchIssue, TemporaryFetchIssue
20
+ from squad.ci.models import TestJob
13
21
 
14
22
 
15
23
  logger = logging.getLogger('squad.ci.backend.tuxsuite')
@@ -83,16 +91,43 @@ class Backend(BaseBackend):
83
91
  # The regex below is supposed to find only one match
84
92
  return matches[0]
85
93
 
94
+ def generate_job_id(self, result_type, result):
95
+ """
96
+ The job id for TuxSuite results is generated using 3 pieces of info:
97
+ 1. If it's either "BUILD" or "TEST" result;
98
+ 2. The TuxSuite project. Ex: "linaro/anders"
99
+ 3. The ksuid of the object. Ex: "1yPYGaOEPNwr2pfqBgONY43zORp"
100
+
101
+ A couple examples for job_id are:
102
+ - BUILD:linaro@anders#1yPYGaOEPNwr2pCqBgONY43zORq
103
+ - TEST:arm@bob#1yPYGaOEPNwr2pCqBgONY43zORp
104
+
105
+ Then it's up to SQUAD's TuxSuite backend to parse the job_id
106
+ and fetch results properly.
107
+ """
108
+ _type = "TEST" if result_type == "test" else "BUILD"
109
+ project = result["project"].replace("/", "@")
110
+ uid = result["uid"]
111
+ return f"{_type}:{project}#{uid}"
112
+
86
113
  def fetch_url(self, *urlbits):
87
114
  url = reduce(urljoin, urlbits)
88
115
 
89
116
  try:
90
117
  response = requests.get(url)
91
118
  except Exception as e:
92
- raise TemporaryFetchIssue(f"Can't retrieve from {url}: %s" % e)
119
+ raise TemporaryFetchIssue(f"Can't retrieve from {url}: {e}")
93
120
 
94
121
  return response
95
122
 
123
+ def fetch_from_results_input(self, test_job):
124
+ try:
125
+ return json.loads(test_job.input)
126
+ except Exception as e:
127
+ logger.error(f"Can't parse results from job's input: {e}")
128
+
129
+ return None
130
+
96
131
  def parse_build_results(self, test_job, job_url, results, settings):
97
132
  required_keys = ['build_status', 'warnings_count', 'download_url', 'retry']
98
133
  self.__check_required_keys__(required_keys, results)
@@ -163,6 +198,10 @@ class Backend(BaseBackend):
163
198
  if results['result'] == 'fail':
164
199
  test_job.failure = str(results['results'])
165
200
 
201
+ elif results['result'] == 'error':
202
+ test_job.failure = 'tuxsuite infrastructure error'
203
+ return 'Incomplete', completed, metadata, tests, metrics, logs
204
+
166
205
  # If boot result is unkown, a retry is needed, otherwise, it either passed or failed
167
206
  if 'unknown' == results['results']['boot']:
168
207
  return None
@@ -175,6 +214,9 @@ class Backend(BaseBackend):
175
214
  _, _, test_id = self.parse_job_id(test_job.job_id)
176
215
  build_id = results['waiting_for']
177
216
  build_url = job_url.replace(test_id, build_id).replace('tests', 'builds')
217
+
218
+ # TODO: check if we can save a few seconds by querying a testjob that
219
+ # already contains build results
178
220
  build_metadata = self.fetch_url(build_url).json()
179
221
 
180
222
  build_metadata_keys = settings.get('TEST_BUILD_METADATA_KEYS', [])
@@ -207,7 +249,12 @@ class Backend(BaseBackend):
207
249
 
208
250
  def fetch(self, test_job):
209
251
  url = self.job_url(test_job)
210
- results = self.fetch_url(url).json()
252
+ if test_job.input:
253
+ results = self.fetch_from_results_input(test_job)
254
+ test_job.input = None
255
+ else:
256
+ results = self.fetch_url(url).json()
257
+
211
258
  if results.get('state') != 'finished':
212
259
  return None
213
260
 
@@ -249,3 +296,49 @@ class Backend(BaseBackend):
249
296
  url = urljoin(self.data.url, endpoint)
250
297
  response = requests.post(url)
251
298
  return response.status_code == 200
299
+
300
+ def supports_callbacks(self):
301
+ return True
302
+
303
+ def validate_callback(self, request, project):
304
+ signature = request.headers.get("x-tux-payload-signature", None)
305
+ if signature is None:
306
+ raise Exception("tuxsuite request is missing signature headers")
307
+
308
+ public_key = project.get_setting("TUXSUITE_PUBLIC_KEY")
309
+ if public_key is None:
310
+ raise Exception("missing tuxsuite public key for this project")
311
+
312
+ payload = request.body
313
+ signature = base64.urlsafe_b64decode(signature)
314
+ key = serialization.load_ssh_public_key(public_key.encode("ascii"))
315
+ key.verify(
316
+ signature,
317
+ payload,
318
+ ec.ECDSA(hashes.SHA256()),
319
+ )
320
+
321
+ def process_callback(self, json_payload, build, environment, backend):
322
+ if "kind" not in json_payload or "status" not in json_payload:
323
+ raise Exception("`kind` and `status` are required in the payload")
324
+
325
+ kind = json_payload["kind"]
326
+ status = json_payload["status"]
327
+ job_id = self.generate_job_id(kind, status)
328
+ try:
329
+ # Tuxsuite's job id DO NOT repeat, like ever
330
+ testjob = TestJob.objects.get(job_id=job_id, target_build=build, environment=environment.slug)
331
+ except TestJob.DoesNotExist:
332
+ testjob = TestJob.objects.create(
333
+ backend=backend,
334
+ target=build.project,
335
+ target_build=build,
336
+ environment=environment.slug,
337
+ submitted=True,
338
+ job_id=job_id
339
+ )
340
+
341
+ # Saves the input so it can be processed by the queue
342
+ testjob.input = json.dumps(status)
343
+
344
+ return testjob
@@ -0,0 +1,22 @@
1
+ # Generated by Django 4.2 on 2023-04-26 16:50
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('ci', '0028_create_testjob_indexes'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='ResultsInput',
16
+ fields=[
17
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('text', models.TextField(blank=True, null=True)),
19
+ ('test_job', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='results_input', to='ci.testjob')),
20
+ ],
21
+ ),
22
+ ]
squad/ci/models.py CHANGED
@@ -164,6 +164,15 @@ class Backend(models.Model):
164
164
  def check_job_definition(self, definition):
165
165
  return self.get_implementation().check_job_definition(definition)
166
166
 
167
+ def supports_callbacks(self):
168
+ return self.get_implementation().supports_callbacks()
169
+
170
+ def validate_callback(self, request, project):
171
+ self.get_implementation().validate_callback(request, project)
172
+
173
+ def process_callback(self, payload, build, environment):
174
+ return self.get_implementation().process_callback(payload, build, environment, self)
175
+
167
176
  def __str__(self):
168
177
  return '%s (%s)' % (self.name, self.implementation_type)
169
178
 
@@ -241,6 +250,21 @@ class TestJob(models.Model):
241
250
  return self.backend.get_implementation().job_url(self)
242
251
  return None
243
252
 
253
+ @property
254
+ def input(self):
255
+ try:
256
+ return self.results_input.text
257
+ except ResultsInput.DoesNotExist:
258
+ return None
259
+
260
+ @input.setter
261
+ def input(self, value):
262
+ if value:
263
+ self.results_input = ResultsInput(text=value)
264
+ self.results_input.save()
265
+ else:
266
+ self.results_input.delete()
267
+
244
268
  def resubmit(self):
245
269
  ret_value = False
246
270
  if self.can_resubmit:
@@ -311,3 +335,8 @@ class TestJob(models.Model):
311
335
  indexes = [
312
336
  models.Index(fields=['submitted', 'fetched']),
313
337
  ]
338
+
339
+
340
+ class ResultsInput(models.Model):
341
+ test_job = models.OneToOneField(TestJob, related_name='results_input', on_delete=models.CASCADE, null=True)
342
+ text = models.TextField(null=True, blank=True)
squad/plugins/github.py CHANGED
@@ -4,6 +4,7 @@ from django.conf import settings
4
4
  from squad.core.models import ProjectStatus
5
5
  from squad.core.plugins import Plugin as BasePlugin
6
6
  from squad.frontend.templatetags.squad import project_url
7
+ from urllib.parse import urljoin
7
8
 
8
9
 
9
10
  def build_url(build):
@@ -11,7 +12,6 @@ def build_url(build):
11
12
 
12
13
 
13
14
  class Plugin(BasePlugin):
14
-
15
15
  @staticmethod
16
16
  def __github_post__(build, endpoint, payload):
17
17
  api_url = build.patch_source.url
@@ -22,10 +22,11 @@ class Plugin(BasePlugin):
22
22
  "Authorization": "token %s" % api_token,
23
23
  }
24
24
 
25
- url = api_url + endpoint.format(
26
- owner=owner,
27
- repository=repository,
28
- commit=commit
25
+ url = urljoin(
26
+ api_url, endpoint.format(
27
+ owner=owner,
28
+ repository=repository,
29
+ commit=commit)
29
30
  )
30
31
  return requests.post(url, headers=headers, json=payload)
31
32
 
squad/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.65'
1
+ __version__ = '1.67'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: squad
3
- Version: 1.65
3
+ Version: 1.67
4
4
  Summary: Software Quality Dashboard
5
5
  Home-page: https://github.com/Linaro/squad
6
6
  Author: Antonio Terceiro
@@ -8,6 +8,7 @@ Author-email: antonio.terceiro@linaro.org
8
8
  License: GPLv3+
9
9
  Platform: any
10
10
  License-File: COPYING
11
+ Requires-Dist: aiohttp
11
12
  Requires-Dist: celery (<5.0,>=4.4)
12
13
  Requires-Dist: cryptography
13
14
  Requires-Dist: coreapi
@@ -10,29 +10,29 @@ squad/manage.py,sha256=Z-LXT67p0R-IzwJ9fLIAacEZmU0VUjqDOSg7j2ZSxJ4,1437
10
10
  squad/settings.py,sha256=EzUgd8Egzp6GrZd-Vx6OlAorhlVavT84bxvsqD58oZg,14051
11
11
  squad/socialaccount.py,sha256=vySqPwQ3qVVpahuJ-Snln8K--yzRL3bw4Nx27AsB39A,789
12
12
  squad/urls.py,sha256=JiEfVW8YlzLPE52c2aHzdn5kVVKK4o22w8h5KOA6QhQ,2776
13
- squad/version.py,sha256=bTjyVHEwwP5hAXFd8YXRwXbq2V03vOc9cmuvCB7cx_w,21
13
+ squad/version.py,sha256=cyth8cQRyIgeHOgLyFVNnADaoVSn0Rrc5rl9Qs7F8gs,21
14
14
  squad/wsgi.py,sha256=SF8T0cQ0OPVyuYjO5YXBIQzvSXQHV0M2BTmd4gP1rPs,387
15
15
  squad/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  squad/api/apps.py,sha256=Trk72p-iV1uGn0o5mdJn5HARUoHGbfgO49jwXvpkmdQ,141
17
- squad/api/ci.py,sha256=lglQch5iOeGRu6zdjLss2nkPVxer_vDFqBOIlhFMWPs,5114
17
+ squad/api/ci.py,sha256=7eJvUwUbQ7sJzUxcxYURDV1tOT1ouqo0M3L7ojWVAFE,6536
18
18
  squad/api/data.py,sha256=obKDV0-neEvj5lPF9VED2gy_hpfhGtLJABYvSY38ing,2379
19
19
  squad/api/filters.py,sha256=Zvp8DCJmiNquFWqvfVseEAAMYYPiT95RUjqKdzcqSnw,6917
20
20
  squad/api/rest.py,sha256=aVHqq-mXLctPjro29miLqWmR1gMEAIMBeXdbZX-7rQI,74991
21
- squad/api/urls.py,sha256=bud5wHWbHes-klZ7rzBuF7wkKsctDzPxI406I-EwEHI,1202
21
+ squad/api/urls.py,sha256=rmsdaL1uOCVSZ5x1redup9RliICmijaBjRK5ObsTkG8,1343
22
22
  squad/api/utils.py,sha256=Sa8QFId3_oSqD2UOoY3Kuh54LLDLPNMq2sub5ktd6Fs,1160
23
23
  squad/api/views.py,sha256=yGLUp6RtNI5vuae6cOStMuUpSia46LcEVam3eMXmEqY,3885
24
24
  squad/ci/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  squad/ci/admin.py,sha256=7yB-6F0cvt0NVvzGOTlZCyGPV_YHarmbKJZTTzataT4,2255
26
26
  squad/ci/apps.py,sha256=6OVnzTdJkxdqEJnKWYE9dZgUcc29_T1LrDw41cK4EQk,139
27
27
  squad/ci/exceptions.py,sha256=a1sccygniTYDSQi7FRn_6doapddFFiMf55AwGUh5Y80,227
28
- squad/ci/models.py,sha256=1Cwe7RBtftXl7T98GBHzmps8rgqE2prDZJ1RtP4k4zk,11379
28
+ squad/ci/models.py,sha256=RoXJY4Xgu3-YA4FtcK-kem8HR8paqOb5648NByAlzDo,12325
29
29
  squad/ci/tasks.py,sha256=yrtxfPuYEqqGCDYRwLz2XyIp9a7LJ-K6Zm3VS1ymdZk,2728
30
30
  squad/ci/utils.py,sha256=38zHpw8xkZDSFlkG-2BwSK6AkcddK9OkN9LXuQ3SHR0,97
31
31
  squad/ci/backend/__init__.py,sha256=yhpotXT9F4IdAOXvGQ3-17eOHAFwoaqf9SnMX17ab30,534
32
- squad/ci/backend/fake.py,sha256=jVFwriX947DJKplwYD2cKW9pT3p1C7KGwPYROS7wxf0,2103
33
- squad/ci/backend/lava.py,sha256=9FIco3AOmo4dmWMpZsEaTcQpxvlHY6MlDmUzunztN0Y,31249
34
- squad/ci/backend/null.py,sha256=AzACuhYP0bsZi_OzDEJs_JvkqjczpVjnrgcQLV-OEgg,4356
35
- squad/ci/backend/tuxsuite.py,sha256=1EiAJ4e2ykI-93q_n638EqX9zy_kezkiYDzEPFBcpK0,9373
32
+ squad/ci/backend/fake.py,sha256=zzOXGesDCW9xiMQvXGD_jqCQF32yEd7hPM8DgfZxUok,2159
33
+ squad/ci/backend/lava.py,sha256=InqmLjf_txjKpMDpmsYbkPJEhOVADBGnTzwaA76fr1Y,33076
34
+ squad/ci/backend/null.py,sha256=vP77Xj7roruehSjX8fJs7xK2aWxgaUA2id3P8nHNrEY,4949
35
+ squad/ci/backend/tuxsuite.py,sha256=m4Hgpb0twM4icOrPdEHUgvezmVq9DDvtEloojit_2pg,12759
36
36
  squad/ci/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  squad/ci/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  squad/ci/management/commands/create_tuxsuite_boot_tests.py,sha256=JvjNusebLX71eyz9d-kaeCyekYSpzc1eXoeIqWK9ygo,4045
@@ -66,6 +66,7 @@ squad/ci/migrations/0025_backend_listen_enabled.py,sha256=t7Tx7URhsz-Q4GGuoKYNJY
66
66
  squad/ci/migrations/0026_job_start_end_time.py,sha256=18swRRnDXIhHpr2ykMFns04VS1_Fbs7Zdc0HOrikSSY,543
67
67
  squad/ci/migrations/0027_add_tuxsuite_implementation_type.py,sha256=5Max0wE_jnU5FXdJzbrPGr73N0pdZlA321pZKE-yDtE,502
68
68
  squad/ci/migrations/0028_create_testjob_indexes.py,sha256=VYT2wmrvjHJLOazBXZb15vKzGJn8jgKCVNNSSj4Nct4,612
69
+ squad/ci/migrations/0029_create_testjob_results_input.py,sha256=Ax0jBSthCEDWUp7tLfdCgwa0nXgC2JDXOdQ26Kl9_fA,712
69
70
  squad/ci/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  squad/ci/templates/squad/ci/testjob_resubmit.html.jinja2,sha256=ys8zb4E2CJSsi4-uP57glkOFhWVK5x8-yUZcDdi81sY,1693
71
72
  squad/ci/templates/squad/ci/testjob_resubmit.txt.jinja2,sha256=yKYtixlO5zWh6ChD_mk8yDQly_iQae5slB7a0FwNPv0,610
@@ -422,16 +423,16 @@ squad/frontend/templatetags/squad.py,sha256=YSDgZu8e6e99rFhHudzVKh-6cIfoeq5fC0lW
422
423
  squad/plugins/__init__.py,sha256=9BSzy2jFIoDpWlhD7odPPrLdW4CC3btBhdFCvB651dM,152
423
424
  squad/plugins/example.py,sha256=BKpwd315lHRIuNXJPteibpwfnI6C5eXYHYdFYBtVmsI,89
424
425
  squad/plugins/gerrit.py,sha256=CqO2KnFQzu9utr_TQ-sGr1wg3ln0B-bS2-c0_i8T5-c,7009
425
- squad/plugins/github.py,sha256=e-TbWYcPK1r3HnYj3Y-eQ4zdBW4fRO2eBvWvE4CjxeI,2348
426
+ squad/plugins/github.py,sha256=4ZXhR-eBVdmLUK-dBJVMRHOyjue9oknNrUfkuUqkhY0,2413
426
427
  squad/plugins/linux_log_parser.py,sha256=Dfcvh1-t5380QS_tvNIpzW_UD-4gptT9I4FUHVbb0PU,5503
427
428
  squad/run/__init__.py,sha256=ssE8GPAGFiK6V0WpZYowav6Zqsd63dfDMMYasNa1sQg,1410
428
429
  squad/run/__main__.py,sha256=DOl8JOi4Yg7DdtwnUeGqtYBJ6P2k-D2psAEuYOjWr8w,66
429
430
  squad/run/listener.py,sha256=jBeOQhPGb4EdIREB1QsCzYuumsfJ-TqJPd3nR-0m59g,200
430
431
  squad/run/scheduler.py,sha256=CDJG3q5C0GuQuxwlMOfWTSSJpDdwbR6rzpbJfuA0xuw,277
431
432
  squad/run/worker.py,sha256=jtML0h5qKDuSbpJ6_rpWP4MT_rsGA7a24AhwGxBquzk,594
432
- squad-1.65.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
433
- squad-1.65.dist-info/METADATA,sha256=yHhRcjEQeZsr8w07JL30FmzCAlAvTYUdFA3UJKGxZqA,1244
434
- squad-1.65.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
435
- squad-1.65.dist-info/entry_points.txt,sha256=apCDQydHZtvqV334ql6NhTJUAJeZRdtAm0TVcbbAi5Q,194
436
- squad-1.65.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
437
- squad-1.65.dist-info/RECORD,,
433
+ squad-1.67.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
434
+ squad-1.67.dist-info/METADATA,sha256=PA2cyjXXMPX2KVIyIzo6xcLtGugZFDHfLmqvpN8TEvU,1267
435
+ squad-1.67.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
436
+ squad-1.67.dist-info/entry_points.txt,sha256=apCDQydHZtvqV334ql6NhTJUAJeZRdtAm0TVcbbAi5Q,194
437
+ squad-1.67.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
438
+ squad-1.67.dist-info/RECORD,,
File without changes
File without changes