scriptworker 63.2.0__tar.gz → 63.4.1__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.
Files changed (31) hide show
  1. {scriptworker-63.2.0 → scriptworker-63.4.1}/HISTORY.rst +37 -0
  2. {scriptworker-63.2.0 → scriptworker-63.4.1}/PKG-INFO +2 -2
  3. {scriptworker-63.2.0 → scriptworker-63.4.1}/pyproject.toml +1 -1
  4. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/artifacts.py +34 -4
  5. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/config.py +13 -2
  6. scriptworker-63.4.1/src/scriptworker/constants.py +840 -0
  7. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/context.py +11 -3
  8. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/cot/verify.py +19 -5
  9. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/utils.py +65 -14
  10. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/worker.py +4 -2
  11. scriptworker-63.2.0/src/scriptworker/constants.py +0 -822
  12. {scriptworker-63.2.0 → scriptworker-63.4.1}/.gitignore +0 -0
  13. {scriptworker-63.2.0 → scriptworker-63.4.1}/CONTRIBUTING.rst +0 -0
  14. {scriptworker-63.2.0 → scriptworker-63.4.1}/LICENSE +0 -0
  15. {scriptworker-63.2.0 → scriptworker-63.4.1}/README.rst +0 -0
  16. {scriptworker-63.2.0 → scriptworker-63.4.1}/renovate.json +0 -0
  17. {scriptworker-63.2.0 → scriptworker-63.4.1}/scripts/gen_ed25519_key.py +0 -0
  18. {scriptworker-63.2.0 → scriptworker-63.4.1}/scriptworker.yaml.tmpl +0 -0
  19. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/__init__.py +0 -0
  20. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/client.py +0 -0
  21. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/cot/__init__.py +0 -0
  22. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/cot/generate.py +0 -0
  23. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/data/cot_v1_schema.json +0 -0
  24. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/data/scriptworker_task_schema.json +0 -0
  25. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/ed25519.py +0 -0
  26. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/exceptions.py +0 -0
  27. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/github.py +0 -0
  28. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/log.py +0 -0
  29. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/task.py +0 -0
  30. {scriptworker-63.2.0 → scriptworker-63.4.1}/src/scriptworker/task_process.py +0 -0
  31. {scriptworker-63.2.0 → scriptworker-63.4.1}/tox.ini +0 -0
@@ -4,6 +4,43 @@ 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
+ 63.4.1 - 2026-08-11
8
+ -------------------
9
+
10
+ Fixed
11
+ ~~~~~
12
+
13
+ - Allow enterprise tasks to have comm source repositories
14
+
15
+ 63.4.0 - 2026-08-11
16
+ -------------------
17
+
18
+ Added
19
+ ~~~~~
20
+ - set Content-MD5 header to avoid silent data corruption uploading artifacts (#815)
21
+
22
+ 63.3.0 - 2026-08-05
23
+ -------------------
24
+
25
+ Added
26
+ ~~~~~
27
+
28
+ - `cot_product` is now optional. When unset, `by-cot-product` mappings resolve to
29
+ `None`, and `check_config` requires `verify_chain_of_trust` to be off (#810)
30
+ - `download_file` accepts an optional `expected_content_type` kwarg that rejects
31
+ `text/html` responses when another type was requested (#791)
32
+
33
+ Fixed
34
+ ~~~~~
35
+
36
+ - Event loop creation in a fresh process, which raised a `RuntimeError` on
37
+ Python 3.14 since `asyncio.get_event_loop()` no longer creates a loop (#811)
38
+ - JSON/YAML load failures now include the file path and, for downloaded
39
+ artifacts, the source URL in the error message (#791)
40
+ - `load_json_or_yaml_from_url` retries the download when the cached file fails
41
+ to parse, turning transient artifact-server glitches into self-healing
42
+ retries instead of hard failures (#791)
43
+
7
44
  63.2.0 - 2026-06-18
8
45
  -------------------
9
46
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: scriptworker
3
- Version: 63.2.0
3
+ Version: 63.4.1
4
4
  Summary: Taskcluster scriptworker
5
5
  Author-email: Mozilla Release Engineering <releng@mozilla.com>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scriptworker"
3
- version = "63.2.0"
3
+ version = "63.4.1"
4
4
  description = "Taskcluster scriptworker"
5
5
  authors = [
6
6
  { name = "Mozilla Release Engineering", email = "releng@mozilla.com" },
@@ -6,6 +6,7 @@ in S3.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import base64
9
10
  import fnmatch
10
11
  import gzip
11
12
  import logging
@@ -21,7 +22,15 @@ from taskcluster.exceptions import TaskclusterFailure
21
22
  from scriptworker.client import validate_artifact_url
22
23
  from scriptworker.exceptions import DownloadError, ScriptWorkerRetryException, ScriptWorkerTaskException
23
24
  from scriptworker.task import get_decision_task_id, get_run_id, get_task_id
24
- from scriptworker.utils import add_enumerable_item_to_dict, download_file, get_loggable_url, raise_future_exceptions, retry_async, semaphore_wrapper
25
+ from scriptworker.utils import (
26
+ add_enumerable_item_to_dict,
27
+ download_file,
28
+ get_hash,
29
+ get_loggable_url,
30
+ raise_future_exceptions,
31
+ retry_async,
32
+ semaphore_wrapper,
33
+ )
25
34
 
26
35
  log = logging.getLogger(__name__)
27
36
 
@@ -163,6 +172,8 @@ async def create_artifact(context, path, target_path, content_type, content_enco
163
172
  payload = {"storageType": storage_type, "expires": expires or get_expiration_arrow(context).isoformat(), "contentType": content_type}
164
173
  args = [get_task_id(context.claim_task), get_run_id(context.claim_task), target_path, payload]
165
174
 
175
+ content_md5 = await asyncio.to_thread(get_content_md5, path)
176
+
166
177
  tc_response = await context.temp_queue.createArtifact(*args)
167
178
  skip_auto_headers = [aiohttp.hdrs.CONTENT_TYPE]
168
179
  loggable_url = get_loggable_url(tc_response["putUrl"])
@@ -172,7 +183,7 @@ async def create_artifact(context, path, target_path, content_type, content_enco
172
183
  async with context.session.put(
173
184
  tc_response["putUrl"],
174
185
  data=fh,
175
- headers=_craft_artifact_put_headers(content_type, content_encoding),
186
+ headers=_craft_artifact_put_headers(content_type, content_md5, content_encoding),
176
187
  skip_auto_headers=skip_auto_headers,
177
188
  compress=False,
178
189
  ) as resp:
@@ -180,6 +191,12 @@ async def create_artifact(context, path, target_path, content_type, content_enco
180
191
  response_text = await resp.text()
181
192
  log.info(response_text)
182
193
  if resp.status not in (200, 204):
194
+ if content_md5 is not None and "BadDigest" in (response_text or ""):
195
+ log.error(
196
+ "{} was corrupted in transit: the storage backend rejected the body as not matching Content-MD5 {}. Retrying.".format(
197
+ target_path, content_md5
198
+ )
199
+ )
183
200
  raise ScriptWorkerRetryException("Bad status {}".format(resp.status))
184
201
 
185
202
 
@@ -206,9 +223,22 @@ async def create_link_artifact(context, target_path, link_to, content_type, expi
206
223
  await context.temp_queue.createArtifact(*args)
207
224
 
208
225
 
209
- def _craft_artifact_put_headers(content_type, encoding=None):
226
+ def get_content_md5(path):
227
+ """Get the base64-encoded md5 digest of a file, formatted for ``Content-MD5``.
228
+
229
+ Args:
230
+ path (str): the path to the file to digest.
231
+
232
+ Returns:
233
+ str: the base64-encoded md5 digest.
234
+
235
+ """
236
+ return base64.b64encode(bytes.fromhex(get_hash(path, hash_alg="md5"))).decode("ascii")
237
+
238
+
239
+ def _craft_artifact_put_headers(content_type, content_md5, encoding=None):
210
240
  log.debug("{} {}".format(content_type, encoding))
211
- headers = {aiohttp.hdrs.CONTENT_TYPE: content_type}
241
+ headers = {aiohttp.hdrs.CONTENT_TYPE: content_type, aiohttp.hdrs.CONTENT_MD5: content_md5}
212
242
 
213
243
  if encoding is not None:
214
244
  headers[aiohttp.hdrs.CONTENT_ENCODING] = encoding
@@ -125,11 +125,17 @@ def check_config(config, path):
125
125
  if key not in DEFAULT_CONFIG:
126
126
  messages.append("Unknown key {} in {}!".format(key, path))
127
127
  continue
128
+ is_by_cot_product = isinstance(DEFAULT_CONFIG[key], Mapping) and "by-cot-product" in DEFAULT_CONFIG[key]
128
129
  if value is None:
129
- messages.append(_VALUE_UNDEFINED_MESSAGE.format(path=path, key=key))
130
+ if key != "cot_product" and not is_by_cot_product:
131
+ messages.append(_VALUE_UNDEFINED_MESSAGE.format(path=path, key=key))
130
132
  else:
131
133
  value_type = type(value)
132
- if isinstance(DEFAULT_CONFIG[key], Mapping) and "by-cot-product" in DEFAULT_CONFIG[key]:
134
+ if key == "cot_product":
135
+ # `cot_product`'s own default is `None`, so its type can't be
136
+ # inferred from `DEFAULT_CONFIG` the way other keys' can.
137
+ default_type = str
138
+ elif is_by_cot_product:
133
139
  default_type = type(DEFAULT_CONFIG[key]["by-cot-product"][config["cot_product"]])
134
140
  else:
135
141
  default_type = type(DEFAULT_CONFIG[key])
@@ -139,6 +145,8 @@ def check_config(config, path):
139
145
  messages.append(_VALUE_UNDEFINED_MESSAGE.format(path=path, key=key))
140
146
  if key in ("provisioner_id", "worker_group", "worker_type", "worker_id") and not _is_id_valid(value):
141
147
  messages.append('{} doesn\'t match "{}" (required by Taskcluster)'.format(key, _GENERIC_ID_REGEX.pattern))
148
+ if config.get("cot_product") is None and config.get("verify_chain_of_trust"):
149
+ messages.append("{} verify_chain_of_trust can't be True when cot_product is not set!".format(path))
142
150
  return messages
143
151
 
144
152
 
@@ -161,6 +169,9 @@ def apply_product_config(config):
161
169
 
162
170
  for key in config:
163
171
  if isinstance(config[key], Mapping) and "by-cot-product" in config[key]:
172
+ if cot_product is None:
173
+ config[key] = None
174
+ continue
164
175
  try:
165
176
  config[key] = config[key]["by-cot-product"][cot_product]
166
177
  except KeyError: