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.
- {scriptworker-63.4.5 → scriptworker-63.6.0}/HISTORY.rst +25 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/PKG-INFO +2 -2
- {scriptworker-63.4.5 → scriptworker-63.6.0}/pyproject.toml +2 -2
- {scriptworker-63.4.5 → scriptworker-63.6.0}/scriptworker.yaml.tmpl +9 -3
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/constants.py +56 -3
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/verify.py +90 -31
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/github.py +116 -21
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/task.py +1 -1
- {scriptworker-63.4.5 → scriptworker-63.6.0}/.gitignore +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/CONTRIBUTING.rst +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/LICENSE +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/README.rst +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/renovate.json +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/scripts/gen_ed25519_key.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/__init__.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/artifacts.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/client.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/config.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/context.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/__init__.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/cot/generate.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/data/cot_v1_schema.json +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/data/scriptworker_task_schema.json +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/ed25519.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/exceptions.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/log.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/task_process.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/utils.py +0 -0
- {scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/worker.py +0 -0
- {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.
|
|
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>=
|
|
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.
|
|
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>=
|
|
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
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
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
|
-
|
|
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": (
|
|
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
|
|
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
|
|
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
|
|
1209
|
-
repo_definition = github_repo.
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1437
|
-
repo_url = get_repo(link.task,
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
github3.repos.repo.Repository: the github3 repository object
|
|
36
53
|
|
|
37
54
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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: {}"
|
|
361
|
+
raise ValueError(f"Revision cannot be extracted from url: {url}")
|
|
266
362
|
|
|
267
|
-
|
|
268
|
-
repo_url = url[:end_index]
|
|
363
|
+
repo_url = f"{parsed.scheme}://{parsed.netloc}/{repo_owner}/{repo_name}"
|
|
269
364
|
|
|
270
|
-
return
|
|
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
|
|
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
|
|
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
|
{scriptworker-63.4.5 → scriptworker-63.6.0}/src/scriptworker/data/scriptworker_task_schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|