scriptworker 63.5.0__tar.gz → 63.6.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 (30) hide show
  1. {scriptworker-63.5.0 → scriptworker-63.6.1}/HISTORY.rst +18 -0
  2. {scriptworker-63.5.0 → scriptworker-63.6.1}/PKG-INFO +2 -2
  3. {scriptworker-63.5.0 → scriptworker-63.6.1}/pyproject.toml +2 -2
  4. {scriptworker-63.5.0 → scriptworker-63.6.1}/scriptworker.yaml.tmpl +9 -3
  5. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/constants.py +18 -2
  6. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/cot/verify.py +36 -31
  7. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/github.py +104 -15
  8. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/task.py +1 -1
  9. {scriptworker-63.5.0 → scriptworker-63.6.1}/.gitignore +0 -0
  10. {scriptworker-63.5.0 → scriptworker-63.6.1}/CONTRIBUTING.rst +0 -0
  11. {scriptworker-63.5.0 → scriptworker-63.6.1}/LICENSE +0 -0
  12. {scriptworker-63.5.0 → scriptworker-63.6.1}/README.rst +0 -0
  13. {scriptworker-63.5.0 → scriptworker-63.6.1}/renovate.json +0 -0
  14. {scriptworker-63.5.0 → scriptworker-63.6.1}/scripts/gen_ed25519_key.py +0 -0
  15. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/__init__.py +0 -0
  16. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/artifacts.py +0 -0
  17. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/client.py +0 -0
  18. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/config.py +0 -0
  19. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/context.py +0 -0
  20. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/cot/__init__.py +0 -0
  21. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/cot/generate.py +0 -0
  22. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/data/cot_v1_schema.json +0 -0
  23. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/data/scriptworker_task_schema.json +0 -0
  24. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/ed25519.py +0 -0
  25. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/exceptions.py +0 -0
  26. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/log.py +0 -0
  27. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/task_process.py +0 -0
  28. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/utils.py +0 -0
  29. {scriptworker-63.5.0 → scriptworker-63.6.1}/src/scriptworker/worker.py +0 -0
  30. {scriptworker-63.5.0 → scriptworker-63.6.1}/tox.ini +0 -0
@@ -4,6 +4,24 @@ 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.1 - 2026-10-08
8
+ -------------------
9
+
10
+ Fixed
11
+ ~~~~~
12
+
13
+ - CoT verification error with Git-mirrored task triggered via action
14
+
15
+ 63.6.0 - 2026-10-06
16
+ -------------------
17
+
18
+ Added
19
+ ~~~~~
20
+
21
+ - Accept `github-push` decision tasks for the Firefox CoT product
22
+ - Add `comm-autoland` as a trusted project
23
+ - Obtain a repository scoped Github token from the Taskcluster Auth service
24
+
7
25
  63.5.0 - 2026-09-23
8
26
  -------------------
9
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scriptworker
3
- Version: 63.5.0
3
+ Version: 63.6.1
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.5.0"
3
+ version = "63.6.1"
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
  }
@@ -492,7 +499,15 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
492
499
  "github-release",
493
500
  "pr-action",
494
501
  ),
495
- "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
+ ),
496
511
  "glean": (
497
512
  "action",
498
513
  "cron",
@@ -826,6 +841,7 @@ DEFAULT_CONFIG: immutabledict[str, Any] = immutabledict(
826
841
  "/releases/comm-esr153",
827
842
  ),
828
843
  "nightly": ("/comm-central",),
844
+ "autoland": ("/integration/comm-autoland",),
829
845
  }
830
846
  ),
831
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
@@ -45,6 +44,7 @@ from scriptworker.github import (
45
44
  extract_github_repo_full_name,
46
45
  extract_github_repo_owner_and_name,
47
46
  extract_github_repo_ssh_url,
47
+ is_github_url,
48
48
  )
49
49
  from scriptworker.log import contextual_log_handler, get_chain_of_trust_log_filename
50
50
  from scriptworker.task import (
@@ -67,6 +67,7 @@ from scriptworker.task import (
67
67
  get_triggered_by,
68
68
  get_worker_pool_id,
69
69
  is_action,
70
+ is_github_task,
70
71
  is_try_or_pull_request,
71
72
  retry_get_task_definition,
72
73
  )
@@ -1126,7 +1127,7 @@ async def _get_additional_github_releases_jsone_context(decision_link):
1126
1127
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1127
1128
  tag_name = get_revision(task, source_env_prefix)
1128
1129
 
1129
- github_repo = GitHubRepository(repo_owner, repo_name, context.config["github_oauth_token"])
1130
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1130
1131
  release_data = await github_repo.get_release(tag_name)
1131
1132
 
1132
1133
  # The release data expose by the API[1] is not the same as the original event[2]. That's why
@@ -1208,17 +1209,16 @@ async def _get_additional_github_pull_request_jsone_context(decision_link):
1208
1209
  repo_url = repo_url.replace("git@github.com:", "ssh://github.com/", 1)
1209
1210
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1210
1211
  pull_request_number = get_pull_request_number(task, source_env_prefix)
1211
- token = context.config["github_oauth_token"]
1212
1212
 
1213
- github_repo = GitHubRepository(repo_owner, repo_name, token)
1214
- repo_definition = github_repo.definition
1213
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1214
+ repo_definition = await github_repo.get_definition()
1215
1215
 
1216
1216
  # We need to query the repository where the pull request was made to extract
1217
1217
  # pull request data. The pull request could be created on the same repo as
1218
1218
  # the commit, or an upstream repo. We can compare the base and head repo URLs
1219
1219
  # to infer where the pull request lives.
1220
1220
  if repo_definition["fork"] and base_repo_url != repo_url:
1221
- 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"])
1222
1222
 
1223
1223
  pull_request_data = await github_repo.get_pull_request(pull_request_number)
1224
1224
  # Even though pull_request_data['head']['repo']['pushed_at'] does exist,
@@ -1253,7 +1253,7 @@ async def _get_additional_github_push_jsone_context(decision_link):
1253
1253
  repo_owner, repo_name = extract_github_repo_owner_and_name(repo_url)
1254
1254
  commit_hash = get_revision(task, source_env_prefix)
1255
1255
 
1256
- github_repo = GitHubRepository(repo_owner, repo_name, context.config["github_oauth_token"])
1256
+ github_repo = GitHubRepository(context, repo_owner, repo_name)
1257
1257
  commit_data = await github_repo.get_commit(commit_hash)
1258
1258
 
1259
1259
  committer = commit_data["committer"] or {}
@@ -1351,7 +1351,18 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1351
1351
  "taskId": None,
1352
1352
  }
1353
1353
 
1354
- 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":
1355
1366
  if tasks_for == "github-release":
1356
1367
  jsone_context.update(await _get_additional_github_releases_jsone_context(decision_link))
1357
1368
  elif tasks_for == "cron":
@@ -1364,7 +1375,7 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1364
1375
  jsone_context.update(await _get_additional_github_push_jsone_context(decision_link))
1365
1376
  else:
1366
1377
  raise CoTError('Unknown tasks_for "{}" for github cot_product "{}"!'.format(tasks_for, chain.context.config["cot_product"]))
1367
- elif chain.context.config["cot_product_type"] == "hg":
1378
+ elif cot_product_type == "hg":
1368
1379
  source_url = get_source_url(decision_link)
1369
1380
  project = await get_project(chain.context, source_url)
1370
1381
  jsone_context["repository"] = {
@@ -1384,9 +1395,7 @@ async def populate_jsone_context(chain, parent_link, decision_link, tasks_for):
1384
1395
  else:
1385
1396
  raise CoTError('Unknown tasks_for "{}" for hg cot_product "{}"!'.format(tasks_for, chain.context.config["cot_product"]))
1386
1397
  else:
1387
- raise CoTError(
1388
- 'Unknown cot_product_type "{}" for cot_product "{}"!'.format(chain.context.config["cot_product_type"], chain.context.config["cot_product"])
1389
- )
1398
+ raise CoTError('Unknown cot_product_type "{}" for cot_product "{}"!'.format(cot_product_type, chain.context.config["cot_product"]))
1390
1399
 
1391
1400
  log.debug("{} json-e context:".format(parent_link.name))
1392
1401
  # format_json() breaks on lambda values; use pprint.pformat here.
@@ -1407,16 +1416,11 @@ def build_taskcluster_yml_url(link):
1407
1416
  """
1408
1417
  source_env_prefix = link.context.config["source_env_prefix"]
1409
1418
  repo_url = get_repo(link.task, source_env_prefix)
1410
- repo_url = repo_url.replace("git@github.com:", "ssh://github.com/", 1)
1411
1419
  revision = get_revision(link.task, source_env_prefix)
1412
1420
  repo_parts = urlparse(repo_url)
1413
- if repo_parts.netloc == "github.com":
1414
- user, repo_name = extract_github_repo_owner_and_name(repo_url)
1415
- url = f"https://raw.githubusercontent.com/{user}/{repo_name}/{revision}/.taskcluster.yml"
1416
- elif repo_parts.netloc == "hg.mozilla.org":
1417
- url = f"{repo_parts.scheme}://{repo_parts.netloc}{repo_parts.path}/raw-file/{revision}/.taskcluster.yml"
1418
- else:
1421
+ if repo_parts.netloc != "hg.mozilla.org":
1419
1422
  raise CoTError("Unsupported VCS server!")
1423
+ url = f"{repo_parts.scheme}://{repo_parts.netloc}{repo_parts.path}/raw-file/{revision}/.taskcluster.yml"
1420
1424
  log.debug(f"{link.name} .taskcluster.yml is at {url}")
1421
1425
  return url
1422
1426
 
@@ -1438,19 +1442,20 @@ async def get_in_tree_template(link):
1438
1442
 
1439
1443
  """
1440
1444
  context = link.context
1441
- source_url = build_taskcluster_yml_url(link)
1442
- 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)
1443
1448
 
1444
- auth = None
1445
- if (
1446
- (repo_url.startswith(("ssh://", "git@github.com")) or any(vcs_rule.get("require_secret") for vcs_rule in context.config["trusted_vcs_rules"]))
1447
- and "github.com" in repo_url
1448
- and context.config.get("github_oauth_token")
1449
- ):
1450
- 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)
1451
1457
  url_hash = hashlib.sha1(source_url.encode("ascii")).hexdigest()
1452
- 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)
1453
- 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)))
1454
1459
 
1455
1460
 
1456
1461
  def _get_action_from_actions_json(all_actions, callback_name):
@@ -2004,7 +2009,7 @@ def _get_parent_repo(chain, repos):
2004
2009
  found, ``chain``'s repo; any mismatch is reported by the sibling check.
2005
2010
 
2006
2011
  """
2007
- 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}
2012
+ parent_repos = {repo for obj, repo in repos.items() if obj.task_id == chain.decision_task_id and obj.task_type in PARENT_TASK_TYPES}
2008
2013
  if len(parent_repos) == 1:
2009
2014
  return parent_repos.pop()
2010
2015
  return repos[chain]
@@ -1,12 +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
6
8
  from urllib.parse import unquote, urlparse
7
9
 
8
10
  from github3 import GitHub
9
11
  from github3.exceptions import GitHubException
12
+ from taskcluster.aio import Auth
13
+ from taskcluster.exceptions import TaskclusterFailure
10
14
 
11
15
  from scriptworker.exceptions import ConfigError
12
16
  from scriptworker.utils import get_parts_of_url_path, get_single_item_from_sequence, retry_async_decorator, retry_request, retry_sync
@@ -24,30 +28,110 @@ log = logging.getLogger(__name__)
24
28
  class GitHubRepository:
25
29
  """Wrapper around GitHub API. Used to access public data."""
26
30
 
27
- def __init__(self, owner, repo_name, token=""):
28
- """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.
29
35
 
30
36
  Args:
31
- owner (str): the owner's GitHub username
37
+ context (scriptworker.context.Context): the scriptworker context
38
+ owner (str): the owner of the repository
32
39
  repo_name (str): the name of the repository
33
- 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.
34
50
 
35
51
  Returns:
36
- dict: a representation of the repo definition
52
+ github3.repos.repo.Repository: the github3 repository object
37
53
 
38
54
  """
39
- github = retry_sync(GitHub, kwargs={"token": token}, sleeptime_kwargs=_GITHUB_LIBRARY_SLEEP_TIME_KWARGS)
40
- 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
41
73
 
42
- @property
43
- def definition(self):
74
+ Returns:
75
+ str: the scoped GitHub token, or the fallback token
76
+
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):
44
96
  """Fetch the definition of the repository, exposed by the GitHub API.
45
97
 
46
98
  Returns:
47
99
  dict: a representation of the repo definition
48
100
 
49
101
  """
50
- 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
51
135
 
52
136
  @retry_async_decorator(retry_exceptions=GitHubException)
53
137
  async def get_commit(self, commit_hash):
@@ -60,7 +144,8 @@ class GitHubRepository:
60
144
  dict: a representation of the commit
61
145
 
62
146
  """
63
- return self._github_repository.commit(commit_hash).as_dict()
147
+ repository = await self._get_repository()
148
+ return repository.commit(commit_hash).as_dict()
64
149
 
65
150
  @retry_async_decorator(retry_exceptions=GitHubException)
66
151
  async def get_pull_request(self, pull_request_number):
@@ -73,7 +158,8 @@ class GitHubRepository:
73
158
  dict: a representation of the pull request
74
159
 
75
160
  """
76
- 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()
77
163
 
78
164
  @retry_async_decorator(retry_exceptions=GitHubException)
79
165
  async def get_release(self, tag_name):
@@ -86,7 +172,8 @@ class GitHubRepository:
86
172
  dict: a representation of the tag
87
173
 
88
174
  """
89
- 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()
90
177
 
91
178
  @retry_async_decorator(retry_exceptions=GitHubException)
92
179
  async def get_tag_hash(self, tag_name):
@@ -99,8 +186,9 @@ class GitHubRepository:
99
186
  str: the commit hash linked by the tag
100
187
 
101
188
  """
189
+ repository = await self._get_repository()
102
190
  tag_object = get_single_item_from_sequence(
103
- sequence=self._github_repository.tags(),
191
+ sequence=repository.tags(),
104
192
  condition=lambda tag: tag.name == tag_name,
105
193
  no_item_error_message='No tag "{}" exist'.format(tag_name),
106
194
  too_many_item_error_message='Too many tags "{}" found'.format(tag_name),
@@ -129,7 +217,8 @@ class GitHubRepository:
129
217
  if not _is_git_full_hash(revision):
130
218
  revision = await self.get_tag_hash(tag_name=revision)
131
219
 
132
- 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)
133
222
 
134
223
  # https://github.com/{repo_owner}/{repo_name}/branch_commits/{revision} just returns some \n
135
224
  # when the commit hasn't landed on the origin repo. Otherwise, some HTML data is returned - it
@@ -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