scriptworker 63.4.5__tar.gz → 63.6.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.
Files changed (30) hide show
  1. {scriptworker-63.4.5 → scriptworker-63.6.0}/HISTORY.rst +25 -0
  2. {scriptworker-63.4.5 → scriptworker-63.6.0}/PKG-INFO +2 -2
  3. {scriptworker-63.4.5 → scriptworker-63.6.0}/pyproject.toml +2 -2
  4. {scriptworker-63.4.5 → scriptworker-63.6.0}/scriptworker.yaml.tmpl +9 -3
  5. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/constants.py +56 -3
  6. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/verify.py +90 -31
  7. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/github.py +116 -21
  8. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/task.py +1 -1
  9. {scriptworker-63.4.5 → scriptworker-63.6.0}/.gitignore +0 -0
  10. {scriptworker-63.4.5 → scriptworker-63.6.0}/CONTRIBUTING.rst +0 -0
  11. {scriptworker-63.4.5 → scriptworker-63.6.0}/LICENSE +0 -0
  12. {scriptworker-63.4.5 → scriptworker-63.6.0}/README.rst +0 -0
  13. {scriptworker-63.4.5 → scriptworker-63.6.0}/renovate.json +0 -0
  14. {scriptworker-63.4.5 → scriptworker-63.6.0}/scripts/gen_ed25519_key.py +0 -0
  15. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/__init__.py +0 -0
  16. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/artifacts.py +0 -0
  17. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/client.py +0 -0
  18. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/config.py +0 -0
  19. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/context.py +0 -0
  20. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/__init__.py +0 -0
  21. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/generate.py +0 -0
  22. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/data/cot_v1_schema.json +0 -0
  23. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/data/scriptworker_task_schema.json +0 -0
  24. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/ed25519.py +0 -0
  25. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/exceptions.py +0 -0
  26. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/log.py +0 -0
  27. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/task_process.py +0 -0
  28. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/utils.py +0 -0
  29. {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/worker.py +0 -0
  30. {scriptworker-63.4.5 → scriptworker-63.6.0}/tox.ini +0 -0
@@ -4,6 +4,31 @@ 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.6.0 - 2026-10-06
8
+ -------------------
9
+
10
+ Added
11
+ ~~~~~
12
+
13
+ - Accept `github-push` decision tasks for the Firefox CoT product
14
+ - Add `comm-autoland` as a trusted project
15
+ - Obtain a repository scoped Github token from the Taskcluster Auth service
16
+
17
+ 63.5.0 - 2026-09-23
18
+ -------------------
19
+
20
+ Fixed
21
+ ~~~~~
22
+
23
+ - Add `mozilla-firefox` as a Github owner for Firefox
24
+ - Url parsing bug when Github <owner> has <repo> as a substring
25
+
26
+ Added
27
+ ~~~~~
28
+
29
+ - Allow tasks sourced from "Git mirrors" to run in graphs alongside tasks
30
+ sourced from their Mercurial equivalents
31
+
7
32
  63.4.5 - 2026-09-11
8
33
  -------------------
9
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scriptworker
3
- Version: 63.4.5
3
+ Version: 63.6.0
4
4
  Summary: Taskcluster scriptworker
5
5
  Author-email: Mozilla Release Engineering <releng@mozilla.com>
6
6
  License-File: LICENSE
@@ -21,7 +21,7 @@ Requires-Dist: json-e>=2.5.0
21
21
  Requires-Dist: jsonschema[format-nongpl]
22
22
  Requires-Dist: pyyaml
23
23
  Requires-Dist: taskcluster-taskgraph
24
- Requires-Dist: taskcluster>=40
24
+ Requires-Dist: taskcluster>=106
25
25
  Description-Content-Type: text/x-rst
26
26
 
27
27
  ===================
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scriptworker"
3
- version = "63.4.5"
3
+ version = "63.6.0"
4
4
  description = "Taskcluster scriptworker"
5
5
  authors = [
6
6
  { name = "Mozilla Release Engineering", email = "releng@mozilla.com" },
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "jsonschema[format-nongpl]",
26
26
  "json-e>=2.5.0",
27
27
  "PyYAML",
28
- "taskcluster>=40",
28
+ "taskcluster>=106",
29
29
  "taskcluster-taskgraph",
30
30
  ]
31
31
 
@@ -45,9 +45,15 @@ verify_cot_signature: false
45
45
  # Chain of Trust job type, e.g. signing
46
46
  cot_job_type: scriptworker
47
47
  cot_product: firefox
48
- # Calls to Github API are limited to 60 an hour. Using an API token allows to raise the limit to
49
- # 5000 per hour. https://developer.github.com/v3/#rate-limiting
50
- github_oauth_token: somegithubtoken
48
+
49
+ # Scriptworker first tries to obtain a repository scoped token from Taskcluster's auth service,
50
+ # using the Github App registered under this name.
51
+ # github_app_name: read
52
+ #
53
+ # `github_oauth_token` is used as a fallback if that fails (e.g. missing scopes or app not
54
+ # configured). Without either, calls to the Github API are unauthenticated and limited to 60 an
55
+ # hour. See https://developer.github.com/v3/#rate-limiting
56
+ # github_oauth_token: somegithubtoken
51
57
 
52
58
 
53
59
  #-----------------------------------------------------------------------------------------------
@@ -79,7 +79,12 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
79
79
  "max_chain_length": 20,
80
80
  # Calls to Github API are limited to 60 an hour. Using an API token allows to raise the limit to
81
81
  # 5000 per hour. https://developer.github.com/v3/#rate-limiting
82
+ # Scriptworker first tries to obtain a repository scoped token from Taskcluster's auth
83
+ # service, falling back to this token if that fails.
82
84
  "github_oauth_token": "",
85
+ # The name of the Github App registered with Taskcluster's auth service, used to obtain
86
+ # a repository scoped token via `auth.githubRepoToken`.
87
+ "github_app_name": "read",
83
88
  # ed25519 settings
84
89
  "ed25519_private_key_path": "...",
85
90
  "ed25519_public_keys": immutabledict(
@@ -142,7 +147,8 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
142
147
  "adhoc": "github",
143
148
  "app-services": "github",
144
149
  "enterprise": "github",
145
- "firefox": "hg",
150
+ # Firefox is migrating from hg.mozilla.org to Github (bug 2070846)
151
+ "firefox": ("hg", "github"),
146
152
  "glean": "github",
147
153
  "mobile": "github",
148
154
  "mozillavpn": "github",
@@ -408,6 +414,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
408
414
  "netlocs": ("hg.mozilla.org",),
409
415
  "path_regexes": (
410
416
  r"^(?P<path>/comm-central)(/|$)",
417
+ r"^(?P<path>/integration/comm-autoland)(/|$)",
411
418
  r"^(?P<path>/releases/comm-(beta|release|esr\d+))(/|$)",
412
419
  ),
413
420
  }
@@ -435,6 +442,43 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
435
442
  )
436
443
  }
437
444
  ),
445
+ # XXX While migrating Firefox from hg.mozilla.org to GitHub, graphs are
446
+ # still generated by decision tasks from hg, but some tasks in them
447
+ # clone from the GitHub mirror instead. This maps an hg repo to its
448
+ # GitHub mirror. A task whose source is the mirror of its parent task's
449
+ # repo is treated as coming from the parent's repo; its definition has
450
+ # already been verified against the parent task's graph. Parent
451
+ # (decision/action) tasks themselves are never mapped.
452
+ # Remove this once the migration is complete.
453
+ "hg_git_mirrors": immutabledict(
454
+ {
455
+ "by-cot-product": immutabledict(
456
+ {
457
+ "adhoc": immutabledict({}),
458
+ "app-services": immutabledict({}),
459
+ "enterprise": immutabledict({}),
460
+ "firefox": immutabledict(
461
+ {
462
+ "/mozilla-central": "/mozilla-firefox/firefox",
463
+ "/integration/autoland": "/mozilla-firefox/firefox",
464
+ "/releases/mozilla-beta": "/mozilla-firefox/firefox",
465
+ "/releases/mozilla-release": "/mozilla-firefox/firefox",
466
+ "/releases/mozilla-esr115": "/mozilla-firefox/firefox",
467
+ "/releases/mozilla-esr140": "/mozilla-firefox/firefox",
468
+ "/releases/mozilla-esr153": "/mozilla-firefox/firefox",
469
+ }
470
+ ),
471
+ "glean": immutabledict({}),
472
+ "mobile": immutabledict({}),
473
+ "mozillavpn": immutabledict({}),
474
+ "scriptworker": immutabledict({}),
475
+ "thunderbird": immutabledict({}),
476
+ "translations": immutabledict({}),
477
+ "xpi": immutabledict({}),
478
+ }
479
+ )
480
+ }
481
+ ),
438
482
  "valid_tasks_for": immutabledict(
439
483
  {
440
484
  "by-cot-product": immutabledict(
@@ -455,7 +499,15 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
455
499
  "github-release",
456
500
  "pr-action",
457
501
  ),
458
- "firefox": ("hg-push", "cron", "action"),
502
+ "firefox": (
503
+ "action",
504
+ "cron",
505
+ "github-pull-request",
506
+ "github-pull-request-untrusted",
507
+ "github-push",
508
+ "hg-push",
509
+ "pr-action",
510
+ ),
459
511
  "glean": (
460
512
  "action",
461
513
  "cron",
@@ -514,7 +566,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
514
566
  "mozilla",
515
567
  "thunderbird",
516
568
  ),
517
- "firefox": "",
569
+ "firefox": "mozilla-firefox",
518
570
  "glean": "mozilla",
519
571
  "mobile": "mozilla-mobile",
520
572
  "mozillavpn": "mozilla-mobile",
@@ -789,6 +841,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
789
841
  "/releases/comm-esr153",
790
842
  ),
791
843
  "nightly": ("/comm-central",),
844
+ "autoland": ("/integration/comm-autoland",),
792
845
  }
793
846
  ),
794
847
  "translations": immutabledict(
@@ -20,7 +20,6 @@ import tempfile
20
20
  from copy import deepcopy
21
21
  from urllib.parse import urlparse
22
22
 
23
- import aiohttp
24
23
  import dictdiffer
25
24
  import jsone
26
25
  from immutabledict import immutabledict
@@ -40,7 +39,13 @@ from scriptworker.constants import DEFAULT_CONFIG
40
39
  from scriptworker.context import Context
41
40
  from scriptworker.ed25519 import ed25519_public_key_from_string, verify_ed25519_signature
42
41
  from scriptworker.exceptions import BaseDownloadError, CoTError, ScriptWorkerEd25519Error
43
- from scriptworker.github import GitHubRepository, extract_github_repo_full_name, extract_github_repo_owner_and_name, extract_github_repo_ssh_url
42
+ from scriptworker.github import (
43
+ GitHubRepository,
44
+ extract_github_repo_full_name,
45
+ extract_github_repo_owner_and_name,
46
+ extract_github_repo_ssh_url,
47
+ is_github_url,
48
+ )
44
49
  from scriptworker.log import contextual_log_handler, get_chain_of_trust_log_filename
45
50
  from scriptworker.task import (
46
51
  get_action_callback_name,
@@ -62,6 +67,7 @@ from scriptworker.task import (
62
67
  get_triggered_by,
63
68
  get_worker_pool_id,
64
69
  is_action,
70
+ is_github_task,
65
71
  is_try_or_pull_request,
66
72
  retry_get_task_definition,
67
73
  )
@@ -1121,7 +1127,7 @@ async def _get_additional_github_releases_jsone_context(decision_link):
1121
1127
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1122
1128
  tag_name = get_revision(task, source_env_prefix)
1123
1129
 
1124
- github_repo = GitHubRepository(repo_owner, repo_name, context.config["github_oauth_token"])
1130
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1125
1131
  release_data = await github_repo.get_release(tag_name)
1126
1132
 
1127
1133
  # The release data expose by the API[1] is not the same as the original event[2]. That's why
@@ -1203,17 +1209,16 @@ async def _get_additional_github_pull_request_jsone_context(decision_link):
1203
1209
  repo_url = repo_url.replace("git@github.com:", "ssh://github.com/", 1)
1204
1210
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1205
1211
  pull_request_number = get_pull_request_number(task, source_env_prefix)
1206
- token = context.config["github_oauth_token"]
1207
1212
 
1208
- github_repo = GitHubRepository(repo_owner, repo_name, token)
1209
- repo_definition = github_repo.definition
1213
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1214
+ repo_definition = await github_repo.get_definition()
1210
1215
 
1211
1216
  # We need to query the repository where the pull request was made to extract
1212
1217
  # pull request data. The pull request could be created on the same repo as
1213
1218
  # the commit, or an upstream repo. We can compare the base and head repo URLs
1214
1219
  # to infer where the pull request lives.
1215
1220
  if repo_definition["fork"] and base_repo_url != repo_url:
1216
- github_repo = GitHubRepository(owner=repo_definition["parent"]["owner"]["login"], repo_name=repo_definition["parent"]["name"], token=token)
1221
+ github_repo = GitHubRepository(context, repo_definition["parent"]["owner"]["login"], repo_definition["parent"]["name"])
1217
1222
 
1218
1223
  pull_request_data = await github_repo.get_pull_request(pull_request_number)
1219
1224
  # Even though pull_request_data['head']['repo']['pushed_at'] does exist,
@@ -1248,7 +1253,7 @@ async def _get_additional_github_push_jsone_context(decision_link):
1248
1253
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1249
1254
  commit_hash = get_revision(task, source_env_prefix)
1250
1255
 
1251
- github_repo = GitHubRepository(repo_owner, repo_name, context.config["github_oauth_token"])
1256
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1252
1257
  commit_data = await github_repo.get_commit(commit_hash)
1253
1258
 
1254
1259
  committer = commit_data["committer"] or {}
@@ -1346,7 +1351,18 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1346
1351
  "taskId": None,
1347
1352
  }
1348
1353
 
1349
- if chain.context.config["cot_product_type"] == "github":
1354
+ cot_product_type = chain.context.config["cot_product_type"]
1355
+ # A tuple becomes a list when the config round-trips through json/yaml.
1356
+ if isinstance(cot_product_type, (tuple, list)):
1357
+ # Products migrating between VCSes support several types (bug 2070846).
1358
+ # tasks_for like `action` and `cron` exist on both hg and github, so
1359
+ # determine the type from the decision task itself.
1360
+ detected_type = "github" if is_github_task(decision_link.task) else "hg"
1361
+ if detected_type not in cot_product_type:
1362
+ raise CoTError('cot_product_type "{}" is not enabled for cot_product "{}"!'.format(detected_type, chain.context.config["cot_product"]))
1363
+ cot_product_type = detected_type
1364
+
1365
+ if cot_product_type == "github":
1350
1366
  if tasks_for == "github-release":
1351
1367
  jsone_context.update(await _get_additional_github_releases_jsone_context(decision_link))
1352
1368
  elif tasks_for == "cron":
@@ -1359,7 +1375,7 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1359
1375
  jsone_context.update(await _get_additional_github_push_jsone_context(decision_link))
1360
1376
  else:
1361
1377
  raise CoTError('Unknown tasks_for "{}" for github cot_product "{}"!'.format(tasks_for, chain.context.config["cot_product"]))
1362
- elif chain.context.config["cot_product_type"] == "hg":
1378
+ elif cot_product_type == "hg":
1363
1379
  source_url = get_source_url(decision_link)
1364
1380
  project = await get_project(chain.context, source_url)
1365
1381
  jsone_context["repository"] = {
@@ -1379,9 +1395,7 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1379
1395
  else:
1380
1396
  raise CoTError('Unknown tasks_for "{}" for hg cot_product "{}"!'.format(tasks_for, chain.context.config["cot_product"]))
1381
1397
  else:
1382
- raise CoTError(
1383
- 'Unknown cot_product_type "{}" for cot_product "{}"!'.format(chain.context.config["cot_product_type"], chain.context.config["cot_product"])
1384
- )
1398
+ raise CoTError('Unknown cot_product_type "{}" for cot_product "{}"!'.format(cot_product_type, chain.context.config["cot_product"]))
1385
1399
 
1386
1400
  log.debug("{} json-e context:".format(parent_link.name))
1387
1401
  # format_json() breaks on lambda values; use pprint.pformat here.
@@ -1402,16 +1416,11 @@ def build_taskcluster_yml_url(link):
1402
1416
  """
1403
1417
  source_env_prefix = link.context.config["source_env_prefix"]
1404
1418
  repo_url = get_repo(link.task, source_env_prefix)
1405
- repo_url = repo_url.replace("git@github.com:", "ssh://github.com/", 1)
1406
1419
  revision = get_revision(link.task, source_env_prefix)
1407
1420
  repo_parts = urlparse(repo_url)
1408
- if repo_parts.netloc == "github.com":
1409
- user, repo_name = extract_github_repo_owner_and_name(repo_url)
1410
- url = f"https://raw.githubusercontent.com/{user}/{repo_name}/{revision}/.taskcluster.yml"
1411
- elif repo_parts.netloc == "hg.mozilla.org":
1412
- url = f"{repo_parts.scheme}://{repo_parts.netloc}{repo_parts.path}/raw-file/{revision}/.taskcluster.yml"
1413
- else:
1421
+ if repo_parts.netloc != "hg.mozilla.org":
1414
1422
  raise CoTError("Unsupported VCS server!")
1423
+ url = f"{repo_parts.scheme}://{repo_parts.netloc}{repo_parts.path}/raw-file/{revision}/.taskcluster.yml"
1415
1424
  log.debug(f"{link.name} .taskcluster.yml is at {url}")
1416
1425
  return url
1417
1426
 
@@ -1433,19 +1442,20 @@ async def get_in_tree_template(link):
1433
1442
 
1434
1443
  """
1435
1444
  context = link.context
1436
- source_url = build_taskcluster_yml_url(link)
1437
- repo_url = get_repo(link.task, link.context.config["source_env_prefix"])
1445
+ source_env_prefix = context.config["source_env_prefix"]
1446
+ repo_url = get_repo(link.task, source_env_prefix)
1447
+ repo_url = repo_url.replace("git@github.com:", "ssh://github.com/", 1)
1438
1448
 
1439
- auth = None
1440
- if (
1441
- (repo_url.startswith(("ssh://", "git@github.com")) or any(vcs_rule.get("require_secret") for vcs_rule in context.config["trusted_vcs_rules"]))
1442
- and "github.com" in repo_url
1443
- and context.config.get("github_oauth_token")
1444
- ):
1445
- auth = aiohttp.BasicAuth(context.config["github_oauth_token"])
1449
+ if is_github_url(repo_url):
1450
+ revision = get_revision(link.task, source_env_prefix)
1451
+ repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1452
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1453
+ content = await github_repo.get_file_contents(".taskcluster.yml", ref=revision)
1454
+ return load_json_or_yaml(content, file_type="yaml")
1455
+
1456
+ source_url = build_taskcluster_yml_url(link)
1446
1457
  url_hash = hashlib.sha1(source_url.encode("ascii")).hexdigest()
1447
- tmpl = await load_json_or_yaml_from_url(context, source_url, os.path.join(context.config["work_dir"], "{}_taskcluster.yml".format(url_hash)), auth=auth)
1448
- return tmpl
1458
+ return await load_json_or_yaml_from_url(context, source_url, os.path.join(context.config["work_dir"], "{}_taskcluster.yml".format(url_hash)))
1449
1459
 
1450
1460
 
1451
1461
  def _get_action_from_actions_json(all_actions, callback_name):
@@ -1983,6 +1993,44 @@ def get_source_url(obj):
1983
1993
  return source
1984
1994
 
1985
1995
 
1996
+ # hg_git_mirrors {{{1
1997
+ def _get_parent_repo(chain, repos):
1998
+ """Get the repo of ``chain``'s parent (decision/action) tasks.
1999
+
2000
+ These are the tasks whose definitions are rebuilt from the tree, and that
2001
+ the other tasks in the graph derive their trust from.
2002
+
2003
+ Args:
2004
+ chain (ChainOfTrust): the chain we're operating on
2005
+ repos (dict): the repo path of each object in the chain
2006
+
2007
+ Returns:
2008
+ str: the parent tasks' repo path. If they don't agree, or can't be
2009
+ found, ``chain``'s repo; any mismatch is reported by the sibling check.
2010
+
2011
+ """
2012
+ parent_repos = {repo for obj, repo in repos.items() if obj.decision_task_id == chain.decision_task_id and obj.task_type in PARENT_TASK_TYPES}
2013
+ if len(parent_repos) == 1:
2014
+ return parent_repos.pop()
2015
+ return repos[chain]
2016
+
2017
+
2018
+ def _is_git_mirror(chain, repo, parent_repo):
2019
+ """Determine whether ``repo`` is the git mirror of ``parent_repo``.
2020
+
2021
+ Args:
2022
+ chain (ChainOfTrust): the chain we're operating on
2023
+ repo (str): a non-parent task's repo path
2024
+ parent_repo (str): the parent tasks' repo path
2025
+
2026
+ Returns:
2027
+ bool: whether ``repo`` is the git mirror of ``parent_repo``.
2028
+
2029
+ """
2030
+ mirrors = chain.context.config["hg_git_mirrors"] or {}
2031
+ return repo is not None and mirrors.get(parent_repo) == repo
2032
+
2033
+
1986
2034
  # trace_back_to_tree {{{1
1987
2035
  async def trace_back_to_tree(chain):
1988
2036
  """Trace the chain back to the tree.
@@ -2011,6 +2059,17 @@ async def trace_back_to_tree(chain):
2011
2059
  source_url = get_source_url(obj)
2012
2060
  repo_path = match_url_regex(chain.context.config["trusted_vcs_rules"], source_url, match_url_path_callback)
2013
2061
  repos[obj] = repo_path
2062
+ # XXX During the hg -> git migration, graphs generated from hg may contain
2063
+ # tasks sourced from the git mirror. Treat those as coming from the parent
2064
+ # tasks' repo. Parent tasks are never mapped: their source is what their
2065
+ # definition gets rebuilt from, so it must be trusted in its own right.
2066
+ parent_repo = _get_parent_repo(chain, repos)
2067
+ for obj, repo in repos.items():
2068
+ if obj.decision_task_id != chain.decision_task_id or obj.task_type in PARENT_TASK_TYPES:
2069
+ continue
2070
+ if _is_git_mirror(chain, repo, parent_repo):
2071
+ log.info(f"{obj.name} {obj.task_id}: {repo} is the git mirror of {parent_repo}")
2072
+ repos[obj] = parent_repo
2014
2073
  # check for restricted scopes.
2015
2074
  my_repo = repos[chain]
2016
2075
  for scope in chain.task["scopes"]:
@@ -1,11 +1,16 @@
1
1
  """GitHub helper functions."""
2
2
 
3
3
  import asyncio
4
+ import hashlib
4
5
  import logging
5
6
  import re
7
+ from pathlib import Path
8
+ from urllib.parse import unquote, urlparse
6
9
 
7
10
  from github3 import GitHub
8
11
  from github3.exceptions import GitHubException
12
+ from taskcluster.aio import Auth
13
+ from taskcluster.exceptions import TaskclusterFailure
9
14
 
10
15
  from scriptworker.exceptions import ConfigError
11
16
  from scriptworker.utils import get_parts_of_url_path, get_single_item_from_sequence, retry_async_decorator, retry_request, retry_sync
@@ -23,30 +28,110 @@ log = logging.getLogger(__name__)
23
28
  class GitHubRepository:
24
29
  """Wrapper around GitHub API. Used to access public data."""
25
30
 
26
- def __init__(self, owner, repo_name, token=""):
27
- """Build the GitHub API URL which points to the definition of the repository.
31
+ GITHUB_PERMISSIONS = {"contents": "read", "metadata": "read", "pull_requests": "read"}
32
+
33
+ def __init__(self, context, owner, repo_name):
34
+ """Store the repository coordinates. The github3 repository object is built lazily.
28
35
 
29
36
  Args:
30
- owner (str): the owner's GitHub username
37
+ context (scriptworker.context.Context): the scriptworker context
38
+ owner (str): the owner of the repository
31
39
  repo_name (str): the name of the repository
32
- token (str): the GitHub API token
40
+
41
+ """
42
+ self._context = context
43
+ self._owner = owner
44
+ self._repo_name = repo_name
45
+ self._repository_cache = None
46
+ self._repository_lock = asyncio.Lock()
47
+
48
+ async def _get_repository(self):
49
+ """Build and cache the github3 repository object.
33
50
 
34
51
  Returns:
35
- dict: a representation of the repo definition
52
+ github3.repos.repo.Repository: the github3 repository object
36
53
 
37
54
  """
38
- github = retry_sync(GitHub, kwargs={"token": token}, sleeptime_kwargs=_GITHUB_LIBRARY_SLEEP_TIME_KWARGS)
39
- self._github_repository = retry_sync(github.repository, args=(owner, repo_name), sleeptime_kwargs=_GITHUB_LIBRARY_SLEEP_TIME_KWARGS)
55
+ async with self._repository_lock:
56
+ if self._repository_cache is None:
57
+ token = await self._get_token(self._context, self._owner, self._repo_name)
58
+ github = retry_sync(GitHub, kwargs={"token": token}, sleeptime_kwargs=_GITHUB_LIBRARY_SLEEP_TIME_KWARGS)
59
+ self._repository_cache = retry_sync(github.repository, args=(self._owner, self._repo_name), sleeptime_kwargs=_GITHUB_LIBRARY_SLEEP_TIME_KWARGS)
60
+
61
+ return self._repository_cache
62
+
63
+ async def _get_token(self, context, owner, repo_name):
64
+ """Get a repository scoped GitHub token from Taskcluster's auth service.
65
+
66
+ Falls back to ``context.config["github_oauth_token"]`` if the auth service call
67
+ fails, e.g. because of missing scopes.
68
+
69
+ Args:
70
+ context (scriptworker.context.Context): the scriptworker context
71
+ owner (str): the owner of the repository
72
+ repo_name (str): the name of the repository
73
+
74
+ Returns:
75
+ str: the scoped GitHub token, or the fallback token
40
76
 
41
- @property
42
- def definition(self):
77
+ """
78
+ if not context.credentials:
79
+ return context.config.get("github_oauth_token", "")
80
+
81
+ try:
82
+ auth = Auth(options={"rootUrl": context.config["taskcluster_root_url"], "credentials": context.credentials})
83
+ response = await auth.githubRepoToken(
84
+ context.config["github_app_name"], owner, payload={"repositories": [repo_name], "permissions": self.GITHUB_PERMISSIONS}
85
+ )
86
+ return response["token"]
87
+ except TaskclusterFailure as e:
88
+ # TODO When opening a PR from a fork, we're guaranteed to hit this
89
+ # fallback as the task won't have auth service scopes for the repo
90
+ # fork. We'll need to improve this before we can stop depending on
91
+ # `github_oauth_token`.
92
+ log.warning(f"Could not obtain Github token from Taskcluster for {owner}/{repo_name}, falling back to `github_oauth_token`: {e}")
93
+ return context.config.get("github_oauth_token", "")
94
+
95
+ async def get_definition(self):
43
96
  """Fetch the definition of the repository, exposed by the GitHub API.
44
97
 
45
98
  Returns:
46
99
  dict: a representation of the repo definition
47
100
 
48
101
  """
49
- return self._github_repository.as_dict()
102
+ repository = await self._get_repository()
103
+ return repository.as_dict()
104
+
105
+ @retry_async_decorator(retry_exceptions=GitHubException)
106
+ async def get_file_contents(self, path, ref=None):
107
+ """Fetch the decoded contents of a file in the repository.
108
+
109
+ Args:
110
+ path (str): the path to the file, relative to the repository root
111
+ ref (str, optional): the commit/branch/tag to read the file from.
112
+ Defaults to the repository's default branch. Results for full
113
+ commit hashes are cached on disk in `work_dir`.
114
+
115
+ Returns:
116
+ str: the decoded contents of the file
117
+
118
+ """
119
+ # Files at a full commit hash are immutable, so they're safe to cache on disk.
120
+ cache_path = None
121
+ if ref and _is_git_full_hash(ref):
122
+ cache_key = hashlib.sha1(f"{self._owner}/{self._repo_name}@{ref}:{path}".encode("utf-8")).hexdigest()
123
+ cache_path = Path(self._context.config["work_dir"]) / ".cache" / "github_files" / cache_key
124
+ if cache_path.exists():
125
+ return cache_path.read_text(encoding="utf-8")
126
+
127
+ repository = await self._get_repository()
128
+ contents = repository.file_contents(path, ref=ref)
129
+ decoded = contents.decoded.decode("utf-8")
130
+
131
+ if cache_path:
132
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
133
+ cache_path.write_text(decoded, encoding="utf-8")
134
+ return decoded
50
135
 
51
136
  @retry_async_decorator(retry_exceptions=GitHubException)
52
137
  async def get_commit(self, commit_hash):
@@ -59,7 +144,8 @@ class GitHubRepository:
59
144
  dict: a representation of the commit
60
145
 
61
146
  """
62
- return self._github_repository.commit(commit_hash).as_dict()
147
+ repository = await self._get_repository()
148
+ return repository.commit(commit_hash).as_dict()
63
149
 
64
150
  @retry_async_decorator(retry_exceptions=GitHubException)
65
151
  async def get_pull_request(self, pull_request_number):
@@ -72,7 +158,8 @@ class GitHubRepository:
72
158
  dict: a representation of the pull request
73
159
 
74
160
  """
75
- return self._github_repository.pull_request(pull_request_number).as_dict()
161
+ repository = await self._get_repository()
162
+ return repository.pull_request(pull_request_number).as_dict()
76
163
 
77
164
  @retry_async_decorator(retry_exceptions=GitHubException)
78
165
  async def get_release(self, tag_name):
@@ -85,7 +172,8 @@ class GitHubRepository:
85
172
  dict: a representation of the tag
86
173
 
87
174
  """
88
- return self._github_repository.release_from_tag(tag_name).as_dict()
175
+ repository = await self._get_repository()
176
+ return repository.release_from_tag(tag_name).as_dict()
89
177
 
90
178
  @retry_async_decorator(retry_exceptions=GitHubException)
91
179
  async def get_tag_hash(self, tag_name):
@@ -98,8 +186,9 @@ class GitHubRepository:
98
186
  str: the commit hash linked by the tag
99
187
 
100
188
  """
189
+ repository = await self._get_repository()
101
190
  tag_object = get_single_item_from_sequence(
102
- sequence=self._github_repository.tags(),
191
+ sequence=repository.tags(),
103
192
  condition=lambda tag: tag.name == tag_name,
104
193
  no_item_error_message='No tag "{}" exist'.format(tag_name),
105
194
  too_many_item_error_message='Too many tags "{}" found'.format(tag_name),
@@ -128,7 +217,8 @@ class GitHubRepository:
128
217
  if not _is_git_full_hash(revision):
129
218
  revision = await self.get_tag_hash(tag_name=revision)
130
219
 
131
- html_text = await _fetch_github_branch_commits_data(context, self._github_repository.html_url, revision)
220
+ repository = await self._get_repository()
221
+ html_text = await _fetch_github_branch_commits_data(context, repository.html_url, revision)
132
222
 
133
223
  # https://github.com/{repo_owner}/{repo_name}/branch_commits/{revision} just returns some \n
134
224
  # when the commit hasn't landed on the origin repo. Otherwise, some HTML data is returned - it
@@ -202,6 +292,8 @@ def extract_github_repo_owner_and_name(url):
202
292
  _check_github_url_is_supported(url)
203
293
 
204
294
  parts = get_parts_of_url_path(url)
295
+ if len(parts) < 2:
296
+ raise ValueError(f"Could not extract repo owner and name from GitHub URL: {url}")
205
297
  repo_owner = parts[0]
206
298
  repo_name = parts[1]
207
299
 
@@ -257,17 +349,20 @@ def extract_github_repo_and_revision_from_source_url(url):
257
349
  """
258
350
  _check_github_url_is_supported(url)
259
351
 
260
- parts = get_parts_of_url_path(url)
261
- repo_name = parts[1]
352
+ parsed = urlparse(url)
353
+ parts = unquote(parsed.path).lstrip("/").split("/")
354
+ if len(parts) < 2:
355
+ raise ValueError(f"Could not extract repo owner and name from GitHub URL: {url}")
356
+ repo_owner = parts[0]
357
+ repo_name = _strip_trailing_dot_git(parts[1])
262
358
  try:
263
359
  revision = parts[3]
264
360
  except IndexError:
265
- raise ValueError("Revision cannot be extracted from url: {}".format(url))
361
+ raise ValueError(f"Revision cannot be extracted from url: {url}")
266
362
 
267
- end_index = url.index(repo_name) + len(repo_name)
268
- repo_url = url[:end_index]
363
+ repo_url = f"{parsed.scheme}://{parsed.netloc}/{repo_owner}/{repo_name}"
269
364
 
270
- return _strip_trailing_dot_git(repo_url), revision
365
+ return repo_url, revision
271
366
 
272
367
 
273
368
  def _strip_trailing_dot_git(url):
@@ -550,7 +550,7 @@ async def is_pull_request(context, task):
550
550
  if not revision and can_skip:
551
551
  continue
552
552
 
553
- github_repository = GitHubRepository(repo_owner, repo_name, context.config["github_oauth_token"])
553
+ github_repository = GitHubRepository(context, repo_owner, repo_name)
554
554
  conditions.append(not await github_repository.has_commit_landed_on_repository(context, revision))
555
555
 
556
556
  return any(conditions)
File without changes
File without changes
File without changes
File without changes