scriptworker 60.0.0__tar.gz → 60.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scriptworker-60.0.0 → scriptworker-60.2.0}/HISTORY.rst +24 -0
- scriptworker-60.2.0/PKG-INFO +86 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/setup.py +6 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/artifacts.py +28 -2
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/client.py +2 -3
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/constants.py +27 -9
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/context.py +5 -3
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/cot/verify.py +44 -12
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/task.py +18 -2
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/utils.py +7 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/version.py +1 -1
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/worker.py +2 -2
- scriptworker-60.2.0/src/scriptworker.egg-info/PKG-INFO +86 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/entry_points.txt +0 -1
- {scriptworker-60.0.0 → scriptworker-60.2.0}/version.json +2 -2
- scriptworker-60.0.0/PKG-INFO +0 -19
- scriptworker-60.0.0/src/scriptworker.egg-info/PKG-INFO +0 -19
- {scriptworker-60.0.0 → scriptworker-60.2.0}/CONTRIBUTING.rst +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/LICENSE +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/MANIFEST.in +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/README.rst +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/pyproject.toml +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/requirements.txt +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/scripts/gen_ed25519_key.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/scriptworker.yaml.tmpl +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/setup.cfg +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/__init__.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/config.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/cot/__init__.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/cot/generate.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/data/cot_v1_schema.json +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/ed25519.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/exceptions.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/github.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/log.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker/task_process.py +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/SOURCES.txt +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/dependency_links.txt +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/not-zip-safe +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/requires.txt +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/src/scriptworker.egg-info/top_level.txt +0 -0
- {scriptworker-60.0.0 → scriptworker-60.2.0}/tox.ini +0 -0
|
@@ -4,6 +4,30 @@ Change Log
|
|
|
4
4
|
All notable changes to this project will be documented in this file.
|
|
5
5
|
This project adheres to `Semantic Versioning <http://semver.org/>`__.
|
|
6
6
|
|
|
7
|
+
60.2.0 - 2024-07-12
|
|
8
|
+
-------------------
|
|
9
|
+
|
|
10
|
+
Added
|
|
11
|
+
~~~~~
|
|
12
|
+
- Set User-Agent in http requests (#661)
|
|
13
|
+
- Support github-pull-request-untrusted in CoT verification (https://bugzilla.mozilla.org/show_bug.cgi?id=1906748)
|
|
14
|
+
|
|
15
|
+
Fixed
|
|
16
|
+
~~~~~
|
|
17
|
+
- Pull project from the default branch in projects.yml (#665)
|
|
18
|
+
|
|
19
|
+
60.1.0 - 2024-06-24
|
|
20
|
+
-------------------
|
|
21
|
+
|
|
22
|
+
Added
|
|
23
|
+
~~~~~
|
|
24
|
+
- Values for Firefox Translations training repository scriptworker constants
|
|
25
|
+
- Support for deferring upstream artifact selection until runtime
|
|
26
|
+
|
|
27
|
+
Fixed
|
|
28
|
+
~~~~~
|
|
29
|
+
- Ensure GitHub pull request head sha is set correctly during chain of trust verification
|
|
30
|
+
|
|
7
31
|
60.0.0 - 2024-05-27
|
|
8
32
|
-------------------
|
|
9
33
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scriptworker
|
|
3
|
+
Version: 60.2.0
|
|
4
|
+
Summary: TaskCluster Script Worker
|
|
5
|
+
Home-page: https://github.com/mozilla-releng/scriptworker
|
|
6
|
+
Author: Mozilla Release Engineering
|
|
7
|
+
Author-email: release+python@mozilla.com
|
|
8
|
+
License: MPL 2.0
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
|
|
17
|
+
===================
|
|
18
|
+
Scriptworker Readme
|
|
19
|
+
===================
|
|
20
|
+
|
|
21
|
+
.. image:: https://travis-ci.org/mozilla-releng/scriptworker.svg?branch=master
|
|
22
|
+
:target: https://travis-ci.org/mozilla-releng/scriptworker
|
|
23
|
+
|
|
24
|
+
.. image:: https://coveralls.io/repos/github/mozilla-releng/scriptworker/badge.svg?branch=master
|
|
25
|
+
:target: https://coveralls.io/github/mozilla-releng/scriptworker?branch=master
|
|
26
|
+
|
|
27
|
+
.. image:: https://readthedocs.org/projects/scriptworker/badge/?version=latest
|
|
28
|
+
:target: http://scriptworker.readthedocs.io/en/latest/?badge=latest
|
|
29
|
+
:alt: Documentation Status
|
|
30
|
+
|
|
31
|
+
Scriptworker implements the `TaskCluster worker model`_, then launches a pre-defined script.
|
|
32
|
+
|
|
33
|
+
.. _TaskCluster worker model: https://firefox-ci-tc.services.mozilla.com/docs/reference/platform/queue/worker-interaction
|
|
34
|
+
|
|
35
|
+
This worker was designed for `Releng processes`_ that need specific, limited, and pre-defined capabilities.
|
|
36
|
+
|
|
37
|
+
.. _Releng processes: https://bugzilla.mozilla.org/show_bug.cgi?id=1245837
|
|
38
|
+
|
|
39
|
+
Free software: MPL2 License
|
|
40
|
+
|
|
41
|
+
-----
|
|
42
|
+
Usage
|
|
43
|
+
-----
|
|
44
|
+
* Create a config file. By default scriptworker will look in ``./scriptworker.yaml``, but this config path can be specified as the first and only commandline argument. There is an `example config file`_, and all config items are specified in `scriptworker.constants.DEFAULT_CONFIG`_.
|
|
45
|
+
|
|
46
|
+
.. _example config file: https://github.com/mozilla-releng/scriptworker/blob/master/scriptworker.yaml.tmpl
|
|
47
|
+
.. _scriptworker.constants.DEFAULT_CONFIG: https://github.com/mozilla-releng/scriptworker/blob/master/src/scriptworker/constants.py
|
|
48
|
+
|
|
49
|
+
Credentials can live in ``./scriptworker.yaml``, ``./secrets.json``, ``~/.scriptworker``.
|
|
50
|
+
|
|
51
|
+
* Launch: ``scriptworker [config_path]``
|
|
52
|
+
|
|
53
|
+
-------
|
|
54
|
+
Testing
|
|
55
|
+
-------
|
|
56
|
+
|
|
57
|
+
Without integration tests install tox, then
|
|
58
|
+
|
|
59
|
+
``NO_CREDENTIALS_TESTS=1 tox -e py36``
|
|
60
|
+
|
|
61
|
+
Without any tests connecting to the net, then ``NO_TESTS_OVER_WIRE=1 tox -e py36``
|
|
62
|
+
|
|
63
|
+
With integration tests, first create a client in the Taskcluster UI with the scopes::
|
|
64
|
+
|
|
65
|
+
queue:cancel-task:test-dummy-scheduler/*
|
|
66
|
+
queue:claim-work:test-dummy-provisioner/dummy-worker-*
|
|
67
|
+
queue:create-task:lowest:test-dummy-provisioner/dummy-worker-*
|
|
68
|
+
queue:define-task:test-dummy-provisioner/dummy-worker-*
|
|
69
|
+
queue:get-artifact:SampleArtifacts/_/X.txt
|
|
70
|
+
queue:scheduler-id:test-dummy-scheduler
|
|
71
|
+
queue:schedule-task:test-dummy-scheduler/*
|
|
72
|
+
queue:task-group-id:test-dummy-scheduler/*
|
|
73
|
+
queue:worker-id:test-dummy-workers/dummy-worker-*
|
|
74
|
+
|
|
75
|
+
Then generate a no priviledge personal access token in Github for the scriptworker_github_token (to avoid rate limiting) and create a ``./secrets.json`` or ``~/.scriptworker`` that looks like::
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
"integration_credentials": {
|
|
79
|
+
"clientId": "...",
|
|
80
|
+
"accessToken": "...",
|
|
81
|
+
}
|
|
82
|
+
"scriptworker_github_token": "..."
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
then to run all tests: ``tox``
|
|
@@ -7,6 +7,8 @@ import sys
|
|
|
7
7
|
from setuptools import setup
|
|
8
8
|
from setuptools.command.test import test as TestCommand
|
|
9
9
|
|
|
10
|
+
project_dir = os.path.abspath(os.path.dirname(__file__))
|
|
11
|
+
|
|
10
12
|
if {"register", "upload"}.intersection(set(sys.argv)):
|
|
11
13
|
print(
|
|
12
14
|
" ***** WARNING *****\n"
|
|
@@ -39,6 +41,9 @@ with open(PATH) as filehandle:
|
|
|
39
41
|
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "requirements.txt")) as f:
|
|
40
42
|
install_requires = f.readlines()
|
|
41
43
|
|
|
44
|
+
with open(os.path.join(project_dir, "README.rst")) as fh:
|
|
45
|
+
long_description = fh.read()
|
|
46
|
+
|
|
42
47
|
|
|
43
48
|
class Tox(TestCommand):
|
|
44
49
|
"""http://bit.ly/1T0dwvG"""
|
|
@@ -71,6 +76,7 @@ setup(
|
|
|
71
76
|
name="scriptworker",
|
|
72
77
|
version=VERSION,
|
|
73
78
|
description="TaskCluster Script Worker",
|
|
79
|
+
long_description=long_description,
|
|
74
80
|
author="Mozilla Release Engineering",
|
|
75
81
|
author_email="release+python@mozilla.com",
|
|
76
82
|
url="https://github.com/mozilla-releng/scriptworker",
|
|
@@ -6,6 +6,7 @@ in S3.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import fnmatch
|
|
9
10
|
import gzip
|
|
10
11
|
import logging
|
|
11
12
|
import mimetypes
|
|
@@ -15,6 +16,7 @@ from pathlib import Path
|
|
|
15
16
|
import aiohttp
|
|
16
17
|
import arrow
|
|
17
18
|
import async_timeout
|
|
19
|
+
from taskcluster.exceptions import TaskclusterFailure
|
|
18
20
|
|
|
19
21
|
from scriptworker.client import validate_artifact_url
|
|
20
22
|
from scriptworker.exceptions import DownloadError, ScriptWorkerRetryException, ScriptWorkerTaskException
|
|
@@ -221,6 +223,16 @@ def get_artifact_url(context, task_id, path):
|
|
|
221
223
|
return url
|
|
222
224
|
|
|
223
225
|
|
|
226
|
+
# list_latest_artifacts {{{1
|
|
227
|
+
async def list_latest_artifacts(queue, task_id, exception=TaskclusterFailure):
|
|
228
|
+
return await queue.listLatestArtifacts(task_id)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def retry_list_latest_artifacts(queue, task_id, exception=TaskclusterFailure, **kwargs):
|
|
232
|
+
kwargs.setdefault("retry_exceptions", tuple(set([TaskclusterFailure, exception])))
|
|
233
|
+
return await retry_async(list_latest_artifacts, args=(queue, task_id), kwargs={"exception": exception}, **kwargs)
|
|
234
|
+
|
|
235
|
+
|
|
224
236
|
# get_expiration_arrow {{{1
|
|
225
237
|
def get_expiration_arrow(context):
|
|
226
238
|
"""Return an arrow matching `context.task['expires']`.
|
|
@@ -321,8 +333,12 @@ def get_upstream_artifacts_full_paths_per_task_id(context):
|
|
|
321
333
|
for task_id, paths in task_ids_and_relative_paths:
|
|
322
334
|
for path in paths:
|
|
323
335
|
try:
|
|
324
|
-
|
|
325
|
-
|
|
336
|
+
if "*" in path:
|
|
337
|
+
for path_to_add in get_artifacts_matching_glob(context, task_id, path):
|
|
338
|
+
add_enumerable_item_to_dict(dict_=upstream_artifacts_full_paths_per_task_id, key=task_id, item=path_to_add)
|
|
339
|
+
else:
|
|
340
|
+
path_to_add = get_and_check_single_upstream_artifact_full_path(context, task_id, path)
|
|
341
|
+
add_enumerable_item_to_dict(dict_=upstream_artifacts_full_paths_per_task_id, key=task_id, item=path_to_add)
|
|
326
342
|
except ScriptWorkerTaskException:
|
|
327
343
|
if path in optional_artifacts_per_task_id.get(task_id, []):
|
|
328
344
|
log.warning('Optional artifact "{}" of task "{}" not found'.format(path, task_id))
|
|
@@ -417,3 +433,13 @@ def assert_is_parent(path, parent_dir):
|
|
|
417
433
|
p2 = Path(os.path.realpath(parent_dir))
|
|
418
434
|
if p1 != p2 and p2 not in p1.parents:
|
|
419
435
|
raise ScriptWorkerTaskException("{} is not under {}!".format(p1, p2))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def get_artifacts_matching_glob(context, task_id, pattern):
|
|
439
|
+
parent_dir = os.path.abspath(os.path.join(context.config["work_dir"], "cot", task_id))
|
|
440
|
+
matching = []
|
|
441
|
+
for root, _, files in os.walk(parent_dir):
|
|
442
|
+
for f in files:
|
|
443
|
+
if fnmatch.fnmatch(f, pattern):
|
|
444
|
+
matching.append(os.path.join(root, f))
|
|
445
|
+
return matching
|
|
@@ -17,13 +17,12 @@ from asyncio import AbstractEventLoop
|
|
|
17
17
|
from typing import Any, Awaitable, Callable, Dict, List, Match, NoReturn, Optional, Tuple
|
|
18
18
|
from urllib.parse import unquote
|
|
19
19
|
|
|
20
|
-
import aiohttp
|
|
21
20
|
import jsonschema
|
|
22
21
|
|
|
23
22
|
from scriptworker.constants import STATUSES
|
|
24
23
|
from scriptworker.context import Context
|
|
25
24
|
from scriptworker.exceptions import ScriptWorkerException, ScriptWorkerTaskException, TaskVerificationError
|
|
26
|
-
from scriptworker.utils import load_json_or_yaml, match_url_regex
|
|
25
|
+
from scriptworker.utils import load_json_or_yaml, match_url_regex, scriptworker_session
|
|
27
26
|
|
|
28
27
|
log = logging.getLogger(__name__)
|
|
29
28
|
|
|
@@ -199,7 +198,7 @@ def _init_logging(context: Any) -> None:
|
|
|
199
198
|
|
|
200
199
|
|
|
201
200
|
async def _handle_asyncio_loop(async_main: Callable[[Any], Awaitable[None]], context: Any) -> None:
|
|
202
|
-
async with
|
|
201
|
+
async with scriptworker_session() as session:
|
|
203
202
|
context.session = session
|
|
204
203
|
try:
|
|
205
204
|
await async_main(context)
|
|
@@ -140,6 +140,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
140
140
|
"xpi": "github",
|
|
141
141
|
"adhoc": "github",
|
|
142
142
|
"scriptworker": "github",
|
|
143
|
+
"translations": "github",
|
|
143
144
|
}
|
|
144
145
|
)
|
|
145
146
|
}
|
|
@@ -172,6 +173,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
172
173
|
"xpi": ("xpi-1/decision", "xpi-3/decision", "xpi-1/decision-gcp", "xpi-3/decision-gcp"),
|
|
173
174
|
"adhoc": ("adhoc-1/decision", "adhoc-3/decision", "adhoc-1/decision-gcp", "adhoc-3/decision-gcp"),
|
|
174
175
|
"scriptworker": ("scriptworker-1/decision", "scriptworker-3/decision", "scriptworker-1/decision-gcp", "scriptworker-3/decision-gcp"),
|
|
176
|
+
"translations": ("translations-1/decision-gcp",),
|
|
175
177
|
}
|
|
176
178
|
)
|
|
177
179
|
}
|
|
@@ -190,6 +192,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
190
192
|
"xpi": ("xpi-1/images", "xpi-3/images", "xpi-1/images-gcp", "xpi-3/images-gcp"),
|
|
191
193
|
"adhoc": ("adhoc-1/images", "adhoc-3/images", "adhoc-1/images-gcp", "adhoc-3/images-gcp"),
|
|
192
194
|
"scriptworker": ("scriptworker-1/images", "scriptworker-3/images", "scriptworker-1/images-gcp", "scriptworker-3/images-gcp"),
|
|
195
|
+
"translations": ("translations-1/images-gcp",),
|
|
193
196
|
}
|
|
194
197
|
)
|
|
195
198
|
}
|
|
@@ -277,6 +280,15 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
277
280
|
}
|
|
278
281
|
),
|
|
279
282
|
),
|
|
283
|
+
"translations": (
|
|
284
|
+
immutabledict(
|
|
285
|
+
{
|
|
286
|
+
"schemes": ("https", "ssh"),
|
|
287
|
+
"netlocs": ("github.com",),
|
|
288
|
+
"path_regexes": tuple([r"^(?P<path>/mozilla/firefox-translations-training)(/|.git|$)"]),
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
),
|
|
280
292
|
}
|
|
281
293
|
)
|
|
282
294
|
},
|
|
@@ -288,10 +300,8 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
288
300
|
"mobile": (
|
|
289
301
|
"action",
|
|
290
302
|
"cron",
|
|
291
|
-
# On staging releases, level 1 docker images may be built in the pull-request graph
|
|
292
303
|
"github-pull-request",
|
|
293
|
-
|
|
294
|
-
# for level 3 images
|
|
304
|
+
"github-pull-request-untrusted",
|
|
295
305
|
"github-push",
|
|
296
306
|
"github-release",
|
|
297
307
|
),
|
|
@@ -299,26 +309,21 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
299
309
|
"app-services": (
|
|
300
310
|
"action",
|
|
301
311
|
"cron",
|
|
302
|
-
# On staging releases, level 1 docker images may be built in the pull-request graph
|
|
303
312
|
"github-pull-request",
|
|
304
|
-
# Similarly, docker images can be built on regular push. This is usually the case
|
|
305
|
-
# for level 3 images
|
|
306
313
|
"github-push",
|
|
307
314
|
"github-release",
|
|
308
315
|
),
|
|
309
316
|
"glean": (
|
|
310
317
|
"action",
|
|
311
318
|
"cron",
|
|
312
|
-
# On staging releases, level 1 docker images may be built in the pull-request graph
|
|
313
319
|
"github-pull-request",
|
|
314
|
-
# Similarly, docker images can be built on regular push. This is usually the case
|
|
315
|
-
# for level 3 images
|
|
316
320
|
"github-push",
|
|
317
321
|
"github-release",
|
|
318
322
|
),
|
|
319
323
|
"xpi": ("action", "cron", "github-pull-request", "github-push", "github-release"),
|
|
320
324
|
"adhoc": ("action", "github-pull-request", "github-push"),
|
|
321
325
|
"scriptworker": ("action", "cron", "github-pull-request", "github-push", "github-release"),
|
|
326
|
+
"translations": ("action", "github-pull-request", "github-push"),
|
|
322
327
|
}
|
|
323
328
|
)
|
|
324
329
|
},
|
|
@@ -334,6 +339,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
334
339
|
"xpi": "mozilla-extensions",
|
|
335
340
|
"adhoc": "mozilla-releng",
|
|
336
341
|
"scriptworker": "mozilla-releng",
|
|
342
|
+
"translations": "mozilla",
|
|
337
343
|
}
|
|
338
344
|
)
|
|
339
345
|
},
|
|
@@ -422,6 +428,11 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
422
428
|
"project:scriptworker:pypi:production": "all-production-repos",
|
|
423
429
|
}
|
|
424
430
|
),
|
|
431
|
+
"translations": immutabledict(
|
|
432
|
+
{
|
|
433
|
+
"project:translations:releng:beetmover:bucket:release": "translations-repo",
|
|
434
|
+
}
|
|
435
|
+
),
|
|
425
436
|
}
|
|
426
437
|
)
|
|
427
438
|
},
|
|
@@ -517,6 +528,11 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
517
528
|
"all-production-repos": ("/mozilla-releng/scriptworker", "/mozilla-releng/scriptworker-scripts"),
|
|
518
529
|
}
|
|
519
530
|
),
|
|
531
|
+
"translations": immutabledict(
|
|
532
|
+
{
|
|
533
|
+
"translations-repo": ("/mozilla/firefox-translations-training",),
|
|
534
|
+
}
|
|
535
|
+
),
|
|
520
536
|
}
|
|
521
537
|
)
|
|
522
538
|
},
|
|
@@ -533,6 +549,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
533
549
|
"xpi": "any", # all allowed
|
|
534
550
|
"adhoc": "any", # all allowed
|
|
535
551
|
"scriptworker": ("decision", "action", "docker-image"),
|
|
552
|
+
"translations": "any", # all allowed
|
|
536
553
|
}
|
|
537
554
|
)
|
|
538
555
|
},
|
|
@@ -548,6 +565,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
|
|
|
548
565
|
"xpi": "XPI",
|
|
549
566
|
"adhoc": "ADHOC",
|
|
550
567
|
"scriptworker": "SCRIPTWORKER",
|
|
568
|
+
"translations": "FIREFOX_TRANSLATIONS_TRAINING",
|
|
551
569
|
}
|
|
552
570
|
)
|
|
553
571
|
},
|
|
@@ -24,7 +24,7 @@ from taskcluster.aio import Queue
|
|
|
24
24
|
|
|
25
25
|
from scriptworker import task_process
|
|
26
26
|
from scriptworker.exceptions import CoTError
|
|
27
|
-
from scriptworker.utils import load_json_or_yaml_from_url, makedirs
|
|
27
|
+
from scriptworker.utils import load_json_or_yaml_from_url, makedirs, scriptworker_session
|
|
28
28
|
|
|
29
29
|
log = logging.getLogger(__name__)
|
|
30
30
|
|
|
@@ -108,7 +108,9 @@ class Context(object):
|
|
|
108
108
|
task_id: str = upstream_artifact["taskId"]
|
|
109
109
|
for path in upstream_artifact["paths"]:
|
|
110
110
|
if os.path.isabs(path) or ".." in path:
|
|
111
|
-
raise CoTError("upstreamArtifacts taskId {} has illegal path {}!"
|
|
111
|
+
raise CoTError(f"upstreamArtifacts taskId {task_id} has illegal path {path}!")
|
|
112
|
+
if "*" in path and not upstream_artifact.get("optional", False):
|
|
113
|
+
raise CoTError(f"upstreamArtifacts taskId {task_id} has globbed path {path} as a non-optional artifact!")
|
|
112
114
|
|
|
113
115
|
@property
|
|
114
116
|
def credentials(self) -> Optional[Dict[str, Any]]:
|
|
@@ -146,7 +148,7 @@ class Context(object):
|
|
|
146
148
|
"""
|
|
147
149
|
assert self.config
|
|
148
150
|
if credentials:
|
|
149
|
-
session = self.session or
|
|
151
|
+
session = self.session or scriptworker_session(loop=self.event_loop)
|
|
150
152
|
return Queue(options={"credentials": credentials, "rootUrl": self.config["taskcluster_root_url"]}, session=session)
|
|
151
153
|
return None
|
|
152
154
|
|
|
@@ -9,6 +9,7 @@ Attributes:
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import asyncio
|
|
12
|
+
import fnmatch
|
|
12
13
|
import logging
|
|
13
14
|
import os
|
|
14
15
|
import pprint
|
|
@@ -23,7 +24,13 @@ import jsone
|
|
|
23
24
|
from immutabledict import immutabledict
|
|
24
25
|
from taskcluster.aio import Queue
|
|
25
26
|
|
|
26
|
-
from scriptworker.artifacts import
|
|
27
|
+
from scriptworker.artifacts import (
|
|
28
|
+
download_artifacts,
|
|
29
|
+
get_artifact_url,
|
|
30
|
+
get_optional_artifacts_per_task_id,
|
|
31
|
+
get_single_upstream_artifact_full_path,
|
|
32
|
+
retry_list_latest_artifacts,
|
|
33
|
+
)
|
|
27
34
|
from scriptworker.config import apply_product_config, read_worker_creds
|
|
28
35
|
from scriptworker.constants import DEFAULT_CONFIG
|
|
29
36
|
from scriptworker.context import Context
|
|
@@ -39,6 +46,7 @@ from scriptworker.task import (
|
|
|
39
46
|
get_branch,
|
|
40
47
|
get_commit_message,
|
|
41
48
|
get_decision_task_id,
|
|
49
|
+
get_head_revision,
|
|
42
50
|
get_parent_task_id,
|
|
43
51
|
get_project,
|
|
44
52
|
get_provisioner_id,
|
|
@@ -70,6 +78,7 @@ from scriptworker.utils import (
|
|
|
70
78
|
read_from_file,
|
|
71
79
|
remove_empty_keys,
|
|
72
80
|
rm,
|
|
81
|
+
scriptworker_session,
|
|
73
82
|
write_to_file,
|
|
74
83
|
)
|
|
75
84
|
from scriptworker.version import __version_string__
|
|
@@ -761,14 +770,28 @@ async def download_cot_artifacts(chain):
|
|
|
761
770
|
|
|
762
771
|
mandatory_artifact_tasks = []
|
|
763
772
|
optional_artifact_tasks = []
|
|
773
|
+
latest_artifacts = {}
|
|
764
774
|
for task_id, paths in all_artifacts_per_task_id.items():
|
|
765
775
|
for path in paths:
|
|
766
|
-
|
|
776
|
+
if "*" in path:
|
|
777
|
+
# Paths with wildcards in them indicate that the concrete
|
|
778
|
+
# artifact names aren't known when the task definition is
|
|
779
|
+
# created. For these cases, we need to fetch the list of
|
|
780
|
+
# artifacts from the completed tasks and then determine
|
|
781
|
+
# which are needed based on the pattern given.
|
|
782
|
+
if not latest_artifacts.get(task_id):
|
|
783
|
+
latest_artifacts[task_id] = (await retry_list_latest_artifacts(chain.context.queue, task_id))["artifacts"]
|
|
784
|
+
coroutines = []
|
|
785
|
+
for artifact in latest_artifacts[task_id]:
|
|
786
|
+
if fnmatch.fnmatch(artifact["name"], path):
|
|
787
|
+
coroutines.append(asyncio.ensure_future(download_cot_artifact(chain, task_id, artifact["name"])))
|
|
788
|
+
else:
|
|
789
|
+
coroutines = [asyncio.ensure_future(download_cot_artifact(chain, task_id, path))]
|
|
767
790
|
|
|
768
791
|
if is_artifact_optional(chain, task_id, path):
|
|
769
|
-
optional_artifact_tasks.
|
|
792
|
+
optional_artifact_tasks.extend(coroutines)
|
|
770
793
|
else:
|
|
771
|
-
mandatory_artifact_tasks.
|
|
794
|
+
mandatory_artifact_tasks.extend(coroutines)
|
|
772
795
|
|
|
773
796
|
mandatory_artifacts_paths = await raise_future_exceptions(mandatory_artifact_tasks)
|
|
774
797
|
succeeded_optional_artifacts_paths, failed_optional_artifacts = await get_results_and_future_exceptions(optional_artifact_tasks)
|
|
@@ -1020,12 +1043,17 @@ async def get_scm_level(context, project):
|
|
|
1020
1043
|
"""
|
|
1021
1044
|
await context.populate_projects()
|
|
1022
1045
|
config = context.projects[project]
|
|
1023
|
-
if "
|
|
1046
|
+
if config["repo_type"] == "hg":
|
|
1024
1047
|
return config["access"].replace("scm_level_", "")
|
|
1025
|
-
elif "
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1048
|
+
elif config["repo_type"] == "git":
|
|
1049
|
+
# TODO: we should be using the branch that the task is actually
|
|
1050
|
+
# being run on
|
|
1051
|
+
default_branch = config.get("default_branch", "main")
|
|
1052
|
+
for branch in config["branches"]:
|
|
1053
|
+
if branch["name"] == default_branch:
|
|
1054
|
+
return str(branch["level"])
|
|
1055
|
+
|
|
1056
|
+
raise ValueError("Can't find level for project {}".format(project))
|
|
1029
1057
|
|
|
1030
1058
|
|
|
1031
1059
|
async def _get_additional_hg_action_jsone_context(parent_link, decision_link):
|
|
@@ -1193,6 +1221,10 @@ async def _get_additional_github_pull_request_jsone_context(decision_link):
|
|
|
1193
1221
|
pull_request_data["head"]["updated_at"] = get_push_date_time(task, source_env_prefix)
|
|
1194
1222
|
# Similarly, the base branch may get new commits by the time a PR job runs
|
|
1195
1223
|
pull_request_data["base"]["sha"] = get_base_revision(task, source_env_prefix)
|
|
1224
|
+
# The head sha may also be different. This commonly happens in pull requests that
|
|
1225
|
+
# have tasks that have cached tasks from an earlier revision of the PR, or even
|
|
1226
|
+
# another PR altogether, upstream of one from the latest revision of the PR.
|
|
1227
|
+
pull_request_data["head"]["sha"] = get_head_revision(task, source_env_prefix)
|
|
1196
1228
|
|
|
1197
1229
|
return {
|
|
1198
1230
|
"event": {
|
|
@@ -1319,7 +1351,7 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
|
|
|
1319
1351
|
jsone_context.update(await _get_additional_git_cron_jsone_context(decision_link))
|
|
1320
1352
|
elif tasks_for == "action":
|
|
1321
1353
|
jsone_context.update(await _get_additional_git_action_jsone_context(decision_link, parent_link))
|
|
1322
|
-
elif tasks_for
|
|
1354
|
+
elif tasks_for in ("github-pull-request", "github-pull-request-untrusted"):
|
|
1323
1355
|
jsone_context.update(await _get_additional_github_pull_request_jsone_context(decision_link))
|
|
1324
1356
|
elif tasks_for == "github-push":
|
|
1325
1357
|
jsone_context.update(await _get_additional_github_push_jsone_context(decision_link))
|
|
@@ -2044,7 +2076,7 @@ async def verify_chain_of_trust(chain, *, check_task=False):
|
|
|
2044
2076
|
|
|
2045
2077
|
# verify_cot_cmdln {{{1
|
|
2046
2078
|
async def _async_verify_cot_cmdln(opts, tmp):
|
|
2047
|
-
async with
|
|
2079
|
+
async with scriptworker_session() as session:
|
|
2048
2080
|
context = Context()
|
|
2049
2081
|
context.session = session
|
|
2050
2082
|
context.config = dict(deepcopy(DEFAULT_CONFIG))
|
|
@@ -2120,7 +2152,7 @@ SCRIPTWORKER_GITHUB_OAUTH_TOKEN to an OAUTH token with read permissions to the r
|
|
|
2120
2152
|
|
|
2121
2153
|
# create_test_workdir {{{1
|
|
2122
2154
|
async def _async_create_test_workdir(task_id, path, queue=None):
|
|
2123
|
-
async with
|
|
2155
|
+
async with scriptworker_session() as session:
|
|
2124
2156
|
context = Context()
|
|
2125
2157
|
context.session = session
|
|
2126
2158
|
context.config = dict(deepcopy(DEFAULT_CONFIG))
|
|
@@ -230,6 +230,22 @@ def get_base_revision(task, source_env_prefix):
|
|
|
230
230
|
return _extract_from_env_in_payload(task, source_env_prefix + "_BASE_REV")
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
def get_head_revision(task, source_env_prefix):
|
|
234
|
+
"""Get the base revision for a task.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
obj (ChainOfTrust or LinkOfTrust): the trust object to inspect
|
|
238
|
+
source_env_prefix (str): The environment variable prefix that is used
|
|
239
|
+
to get repository information.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
str: the revision.
|
|
243
|
+
None: if not defined for this task.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
return _extract_from_env_in_payload(task, source_env_prefix + "_HEAD_REV")
|
|
247
|
+
|
|
248
|
+
|
|
233
249
|
def get_branch(task, source_env_prefix):
|
|
234
250
|
"""Get the branch on top of which the graph was made.
|
|
235
251
|
|
|
@@ -487,7 +503,7 @@ async def is_pull_request(context, task):
|
|
|
487
503
|
|
|
488
504
|
This checks for the following things::
|
|
489
505
|
|
|
490
|
-
* ``task.extra.env.tasks_for``
|
|
506
|
+
* ``task.extra.env.tasks_for`` is "github-pull-request" or "github-pull-request-untrusted"
|
|
491
507
|
* ``task.payload.env.MOBILE_HEAD_REPOSITORY`` doesn't come from an official repo
|
|
492
508
|
* ``task.metadata.source`` doesn't come from an official repo, either
|
|
493
509
|
* The last 2 items are landed on the official repo
|
|
@@ -509,7 +525,7 @@ async def is_pull_request(context, task):
|
|
|
509
525
|
metadata_source_url = task["metadata"].get("source", "")
|
|
510
526
|
repo_from_source_url, revision_from_source_url = extract_github_repo_and_revision_from_source_url(metadata_source_url)
|
|
511
527
|
|
|
512
|
-
conditions = [tasks_for
|
|
528
|
+
conditions = [tasks_for in ("github-pull-request", "github-pull-request-untrusted")]
|
|
513
529
|
urls_revisions_and_can_skip = ((repo_url_from_payload, revision_from_payload, True), (repo_from_source_url, revision_from_source_url, False))
|
|
514
530
|
for repo_url, revision, can_skip in urls_revisions_and_can_skip:
|
|
515
531
|
# XXX In the case of scriptworker tasks, neither the repo nor the revision is defined
|
|
@@ -25,6 +25,7 @@ import async_timeout
|
|
|
25
25
|
import yaml
|
|
26
26
|
from taskcluster.client import createTemporaryCredentials
|
|
27
27
|
|
|
28
|
+
import scriptworker.version
|
|
28
29
|
from scriptworker.exceptions import Download404, DownloadError, ScriptWorkerException, ScriptWorkerRetryException, ScriptWorkerTaskException
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
@@ -37,6 +38,12 @@ else:
|
|
|
37
38
|
log = logging.getLogger(__name__)
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
# scriptworker_session {{{1
|
|
42
|
+
def scriptworker_session(*args, **kwargs):
|
|
43
|
+
kwargs.setdefault("headers", {}).setdefault("User-Agent", f"scriptworker {scriptworker.version.__version_string__}")
|
|
44
|
+
return aiohttp.ClientSession(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
40
47
|
# request {{{1
|
|
41
48
|
async def request(context, url, timeout=60, method="get", good=(200,), retry=tuple(range(500, 512)), return_type="text", **kwargs):
|
|
42
49
|
"""Async aiohttp request wrapper.
|
|
@@ -25,7 +25,7 @@ from scriptworker.cot.verify import ChainOfTrust, verify_chain_of_trust
|
|
|
25
25
|
from scriptworker.exceptions import ScriptWorkerException, WorkerShutdownDuringTask
|
|
26
26
|
from scriptworker.task import claim_work, complete_task, prepare_to_run_task, reclaim_task, run_task, worst_level
|
|
27
27
|
from scriptworker.task_process import TaskProcess
|
|
28
|
-
from scriptworker.utils import cleanup, filepaths_in_dir
|
|
28
|
+
from scriptworker.utils import cleanup, filepaths_in_dir, scriptworker_session
|
|
29
29
|
|
|
30
30
|
log = logging.getLogger(__name__)
|
|
31
31
|
|
|
@@ -210,7 +210,7 @@ async def async_main(context, credentials):
|
|
|
210
210
|
Args:
|
|
211
211
|
context (scriptworker.context.Context): the scriptworker context.
|
|
212
212
|
"""
|
|
213
|
-
async with
|
|
213
|
+
async with scriptworker_session() as session:
|
|
214
214
|
context.session = session
|
|
215
215
|
context.credentials = credentials
|
|
216
216
|
await run_tasks(context)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scriptworker
|
|
3
|
+
Version: 60.2.0
|
|
4
|
+
Summary: TaskCluster Script Worker
|
|
5
|
+
Home-page: https://github.com/mozilla-releng/scriptworker
|
|
6
|
+
Author: Mozilla Release Engineering
|
|
7
|
+
Author-email: release+python@mozilla.com
|
|
8
|
+
License: MPL 2.0
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
|
|
17
|
+
===================
|
|
18
|
+
Scriptworker Readme
|
|
19
|
+
===================
|
|
20
|
+
|
|
21
|
+
.. image:: https://travis-ci.org/mozilla-releng/scriptworker.svg?branch=master
|
|
22
|
+
:target: https://travis-ci.org/mozilla-releng/scriptworker
|
|
23
|
+
|
|
24
|
+
.. image:: https://coveralls.io/repos/github/mozilla-releng/scriptworker/badge.svg?branch=master
|
|
25
|
+
:target: https://coveralls.io/github/mozilla-releng/scriptworker?branch=master
|
|
26
|
+
|
|
27
|
+
.. image:: https://readthedocs.org/projects/scriptworker/badge/?version=latest
|
|
28
|
+
:target: http://scriptworker.readthedocs.io/en/latest/?badge=latest
|
|
29
|
+
:alt: Documentation Status
|
|
30
|
+
|
|
31
|
+
Scriptworker implements the `TaskCluster worker model`_, then launches a pre-defined script.
|
|
32
|
+
|
|
33
|
+
.. _TaskCluster worker model: https://firefox-ci-tc.services.mozilla.com/docs/reference/platform/queue/worker-interaction
|
|
34
|
+
|
|
35
|
+
This worker was designed for `Releng processes`_ that need specific, limited, and pre-defined capabilities.
|
|
36
|
+
|
|
37
|
+
.. _Releng processes: https://bugzilla.mozilla.org/show_bug.cgi?id=1245837
|
|
38
|
+
|
|
39
|
+
Free software: MPL2 License
|
|
40
|
+
|
|
41
|
+
-----
|
|
42
|
+
Usage
|
|
43
|
+
-----
|
|
44
|
+
* Create a config file. By default scriptworker will look in ``./scriptworker.yaml``, but this config path can be specified as the first and only commandline argument. There is an `example config file`_, and all config items are specified in `scriptworker.constants.DEFAULT_CONFIG`_.
|
|
45
|
+
|
|
46
|
+
.. _example config file: https://github.com/mozilla-releng/scriptworker/blob/master/scriptworker.yaml.tmpl
|
|
47
|
+
.. _scriptworker.constants.DEFAULT_CONFIG: https://github.com/mozilla-releng/scriptworker/blob/master/src/scriptworker/constants.py
|
|
48
|
+
|
|
49
|
+
Credentials can live in ``./scriptworker.yaml``, ``./secrets.json``, ``~/.scriptworker``.
|
|
50
|
+
|
|
51
|
+
* Launch: ``scriptworker [config_path]``
|
|
52
|
+
|
|
53
|
+
-------
|
|
54
|
+
Testing
|
|
55
|
+
-------
|
|
56
|
+
|
|
57
|
+
Without integration tests install tox, then
|
|
58
|
+
|
|
59
|
+
``NO_CREDENTIALS_TESTS=1 tox -e py36``
|
|
60
|
+
|
|
61
|
+
Without any tests connecting to the net, then ``NO_TESTS_OVER_WIRE=1 tox -e py36``
|
|
62
|
+
|
|
63
|
+
With integration tests, first create a client in the Taskcluster UI with the scopes::
|
|
64
|
+
|
|
65
|
+
queue:cancel-task:test-dummy-scheduler/*
|
|
66
|
+
queue:claim-work:test-dummy-provisioner/dummy-worker-*
|
|
67
|
+
queue:create-task:lowest:test-dummy-provisioner/dummy-worker-*
|
|
68
|
+
queue:define-task:test-dummy-provisioner/dummy-worker-*
|
|
69
|
+
queue:get-artifact:SampleArtifacts/_/X.txt
|
|
70
|
+
queue:scheduler-id:test-dummy-scheduler
|
|
71
|
+
queue:schedule-task:test-dummy-scheduler/*
|
|
72
|
+
queue:task-group-id:test-dummy-scheduler/*
|
|
73
|
+
queue:worker-id:test-dummy-workers/dummy-worker-*
|
|
74
|
+
|
|
75
|
+
Then generate a no priviledge personal access token in Github for the scriptworker_github_token (to avoid rate limiting) and create a ``./secrets.json`` or ``~/.scriptworker`` that looks like::
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
"integration_credentials": {
|
|
79
|
+
"clientId": "...",
|
|
80
|
+
"accessToken": "...",
|
|
81
|
+
}
|
|
82
|
+
"scriptworker_github_token": "..."
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
then to run all tests: ``tox``
|
scriptworker-60.0.0/PKG-INFO
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: scriptworker
|
|
3
|
-
Version: 60.0.0
|
|
4
|
-
Summary: TaskCluster Script Worker
|
|
5
|
-
Home-page: https://github.com/mozilla-releng/scriptworker
|
|
6
|
-
Author: Mozilla Release Engineering
|
|
7
|
-
Author-email: release+python@mozilla.com
|
|
8
|
-
License: MPL 2.0
|
|
9
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Natural Language :: English
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Requires-Python: >=3.7
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
|
|
18
|
-
UNKNOWN
|
|
19
|
-
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: scriptworker
|
|
3
|
-
Version: 60.0.0
|
|
4
|
-
Summary: TaskCluster Script Worker
|
|
5
|
-
Home-page: https://github.com/mozilla-releng/scriptworker
|
|
6
|
-
Author: Mozilla Release Engineering
|
|
7
|
-
Author-email: release+python@mozilla.com
|
|
8
|
-
License: MPL 2.0
|
|
9
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Natural Language :: English
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Requires-Python: >=3.7
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
|
|
18
|
-
UNKNOWN
|
|
19
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|