From a17ecb624b59e7517ec7d55fa326ec73ce06b5fe Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 9 Jul 2026 16:59:13 -0700 Subject: [PATCH 01/10] Run build repair only on regressions --- .../hackbot-pulse-listener/app/consumer.py | 19 ++- .../hackbot-pulse-listener/app/regression.py | 78 ++++++++++++ .../hackbot-pulse-listener/pyproject.toml | 1 + .../tests/test_consumer.py | 73 +++++++++++ .../tests/test_regression.py | 119 ++++++++++++++++++ uv.lock | 2 + 6 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 services/hackbot-pulse-listener/app/regression.py create mode 100644 services/hackbot-pulse-listener/tests/test_regression.py diff --git a/services/hackbot-pulse-listener/app/consumer.py b/services/hackbot-pulse-listener/app/consumer.py index 427dc403bf..7c95b63ee2 100644 --- a/services/hackbot-pulse-listener/app/consumer.py +++ b/services/hackbot-pulse-listener/app/consumer.py @@ -5,7 +5,7 @@ from kombu import Connection, Exchange, Queue from kombu.mixins import ConsumerMixin -from app import client, lando, taskcluster, worker +from app import client, lando, regression, taskcluster, worker from app.config import settings from app.models import RunContext @@ -15,8 +15,11 @@ EXCHANGES = ("exchange/taskcluster-queue/v1/task-failed",) -# In-memory dedupe of hg revisions already handed to the agent. Only the -# single consumer thread touches it, so no lock is needed. +# In-memory dedupe of hg revisions already handed to the agent. A revision is +# recorded only once we actually trigger a run, so an inherited failure on one +# build label never suppresses a genuine regression on another label of the +# same push, while a revision that breaks many builds still triggers only once. +# Only the single consumer thread touches it, so no lock is needed. _seen: TTLCache = TTLCache( maxsize=settings.dedupe_max_size, ttl=settings.dedupe_ttl_seconds ) @@ -44,7 +47,15 @@ def process(body: dict, executor: Executor) -> str | None: return None if hg_revision in _seen: - logger.info("Revision %s already processed; skipping", hg_revision) + logger.info("Revision %s already triggered a run; skipping", hg_revision) + return None + + if not regression.is_new_build_failure(project, hg_revision, task_label): + logger.info( + "Build %s at %s inherited from an ancestor push; skipping", + task_label, + hg_revision, + ) return None _seen[hg_revision] = True diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py new file mode 100644 index 0000000000..93d2846bfc --- /dev/null +++ b/services/hackbot-pulse-listener/app/regression.py @@ -0,0 +1,78 @@ +import logging + +from mozci.errors import ParentPushNotFound +from mozci.push import Push + +logger = logging.getLogger(__name__) + +# Maximum number of ancestor pushes to walk back over coalescing gaps before +# giving up. Mirrors mozci's own MAX_DEPTH. +MAX_DEPTH = 20 + +# mozci results vary by data source (Taskcluster vs Treeherder), so match both +# vocabularies. Only genuine build failures count as failed; infra results +# ("exception", "canceled", "superseded", ...) are deliberately non-decisive so +# they never suppress a real regression. +_PASSED_RESULTS = ("passed", "success") +_FAILED_RESULTS = ("busted", "failed") + + +def _build_status(push: Push, label: str) -> str | None: + """Return 'passed', 'failed', or None for a build label on a push. + + None means the build produced no decisive result here: it was coalesced (not + scheduled), is still running, or only hit infra errors. Retriggers are + collapsed: any green run means 'passed'; only a genuine build failure + (never an infra exception) counts as 'failed'. + """ + results = [ + t.result for t in push.tasks if t.label == label and t.state == "completed" + ] + if any(r in _PASSED_RESULTS for r in results): + return "passed" + if any(r in _FAILED_RESULTS for r in results): + return "failed" + return None + + +def is_new_build_failure(branch: str, rev: str, label: str) -> bool: + """Return True if this push introduced the failure, False if it inherited it. + + Walks back over pushes that did not run the build (coalescing) until it + finds the nearest ancestor that did. Fails open (returns True) on any + mozci/network error or if no ancestor within MAX_DEPTH ran the build, so we + never silently drop a real regression. + """ + try: + ancestor = Push(rev, branch=branch) + for _ in range(MAX_DEPTH): + try: + ancestor = ancestor.parent + except ParentPushNotFound: + break + status = _build_status(ancestor, label) + if status is None: + continue + if status == "failed": + logger.info( + "Build %s already failing at %s; inherited failure at %s", + label, + ancestor.rev, + rev, + ) + return False + logger.info( + "Build %s passed at %s; new failure introduced at %s", + label, + ancestor.rev, + rev, + ) + return True + except Exception: + logger.exception("Regression check failed for %s@%s; running agent", label, rev) + return True + + logger.warning( + "No ancestor within %s pushes ran build %s; running agent", MAX_DEPTH, label + ) + return True diff --git a/services/hackbot-pulse-listener/pyproject.toml b/services/hackbot-pulse-listener/pyproject.toml index 821bad9511..4faba06e5e 100644 --- a/services/hackbot-pulse-listener/pyproject.toml +++ b/services/hackbot-pulse-listener/pyproject.toml @@ -5,6 +5,7 @@ description = "Listens to Taskcluster build failures and triggers the build-repa requires-python = ">=3.12" dependencies = [ "kombu>=5.6,<6", + "mozci~=2.4.8", "taskcluster>=97.1,<100.6", "httpx>=0.26.0", "pydantic-settings>=2.1.0", diff --git a/services/hackbot-pulse-listener/tests/test_consumer.py b/services/hackbot-pulse-listener/tests/test_consumer.py index 08e5981b7c..df9662b2a9 100644 --- a/services/hackbot-pulse-listener/tests/test_consumer.py +++ b/services/hackbot-pulse-listener/tests/test_consumer.py @@ -58,6 +58,7 @@ def test_build_failure_triggers_run_and_submits_poll(): with ( patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), patch.object(consumer.lando, "hg_to_git", return_value="deadbeef"), + patch.object(consumer.regression, "is_new_build_failure", return_value=True), patch.object(consumer.client, "trigger_run", return_value="run-1") as trigger, ): run_id = consumer.process(_build_msg(), executor) @@ -83,6 +84,7 @@ def test_same_revision_triggers_once(): with ( patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), patch.object(consumer.lando, "hg_to_git", return_value="deadbeef"), + patch.object(consumer.regression, "is_new_build_failure", return_value=True), patch.object(consumer.client, "trigger_run", return_value="run-1") as trigger, ): consumer.process(_build_msg(task_id="T1"), executor) @@ -91,6 +93,63 @@ def test_same_revision_triggers_once(): trigger.assert_called_once() +def test_inherited_failure_is_skipped_before_mapping(): + executor = MagicMock() + with ( + patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), + patch.object(consumer.regression, "is_new_build_failure", return_value=False), + patch.object(consumer.lando, "hg_to_git") as hg_to_git, + patch.object(consumer.client, "trigger_run") as trigger, + ): + assert consumer.process(_build_msg(), executor) is None + + hg_to_git.assert_not_called() + trigger.assert_not_called() + executor.submit.assert_not_called() + + +def test_multiple_builds_same_revision_trigger_once(): + executor = MagicMock() + with ( + patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), + patch.object(consumer.lando, "hg_to_git", return_value="deadbeef"), + patch.object(consumer.regression, "is_new_build_failure", return_value=True), + patch.object(consumer.client, "trigger_run", return_value="run-1") as trigger, + ): + consumer.process(_build_msg(task_id="T1", label="build-linux64/opt"), executor) + consumer.process(_build_msg(task_id="T2", label="build-macosx64/opt"), executor) + + trigger.assert_called_once() + + +def test_inherited_label_does_not_suppress_new_label_on_same_revision(): + executor = MagicMock() + with ( + patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), + patch.object(consumer.lando, "hg_to_git", return_value="deadbeef"), + patch.object( + consumer.regression, "is_new_build_failure", side_effect=[False, True] + ), + patch.object(consumer.client, "trigger_run", return_value="run-1") as trigger, + ): + # Inherited failure on the first label must not mark the revision seen. + assert ( + consumer.process( + _build_msg(task_id="T1", label="build-linux64/opt"), executor + ) + is None + ) + # A genuine regression on another label of the same push still runs. + assert ( + consumer.process( + _build_msg(task_id="T2", label="build-macosx64/opt"), executor + ) + == "run-1" + ) + + trigger.assert_called_once() + + def test_unwatched_project_skipped_before_api_call(): executor = MagicMock() with ( @@ -107,6 +166,7 @@ def test_unmappable_revision_skipped(): executor = MagicMock() with ( patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), + patch.object(consumer.regression, "is_new_build_failure", return_value=True), patch.object(consumer.lando, "hg_to_git", return_value=None), patch.object(consumer.client, "trigger_run") as trigger, ): @@ -121,6 +181,7 @@ def test_trigger_failure_releases_revision_for_retry(): with ( patch.object(consumer.taskcluster, "get_hg_revision", return_value="hgrev"), patch.object(consumer.lando, "hg_to_git", return_value="deadbeef"), + patch.object(consumer.regression, "is_new_build_failure", return_value=True), patch.object( consumer.client, "trigger_run", side_effect=[RuntimeError("boom"), "run-2"] ) as trigger, @@ -130,3 +191,15 @@ def test_trigger_failure_releases_revision_for_retry(): assert consumer.process(_build_msg(task_id="T2"), executor) == "run-2" assert trigger.call_count == 2 + + +def test_queue_name_includes_non_production_environment(): + with patch.object(consumer.settings, "environment", "development"): + (queue,) = consumer._build_queues("guest") + assert queue.name == "queue/guest/build-repair-development-task-failed" + + +def test_queue_name_omits_production_environment(): + with patch.object(consumer.settings, "environment", "production"): + (queue,) = consumer._build_queues("guest") + assert queue.name == "queue/guest/build-repair-task-failed" diff --git a/services/hackbot-pulse-listener/tests/test_regression.py b/services/hackbot-pulse-listener/tests/test_regression.py new file mode 100644 index 0000000000..75c81d9450 --- /dev/null +++ b/services/hackbot-pulse-listener/tests/test_regression.py @@ -0,0 +1,119 @@ +from unittest.mock import patch + +from app import regression +from mozci.errors import ParentPushNotFound + +LABEL = "build-linux64/opt" + + +class FakeTask: + def __init__(self, label=LABEL, state="completed", result="passed"): + self.label = label + self.state = state + self.result = result + + +class FakePush: + def __init__(self, rev, tasks, parent=None): + self.rev = rev + self.tasks = tasks + self._parent = parent + + @property + def parent(self): + if self._parent is None: + raise ParentPushNotFound("no parent", rev=self.rev, branch="autoland") + return self._parent + + +def _passed(label=LABEL): + return [FakeTask(label=label, result="passed")] + + +def _failed(label=LABEL): + return [FakeTask(label=label, result="failed")] + + +def _infra(label=LABEL): + return [FakeTask(label=label, state="exception", result="exception")] + + +def _running(label=LABEL): + return [FakeTask(label=label, state="running", result=None)] + + +def _chain(*task_lists): + """Build a parent chain: task_lists[0] is the observed push, [1] its parent, ...""" + push = None + for i, tasks in enumerate(reversed(task_lists)): + push = FakePush(rev=f"rev{len(task_lists) - 1 - i}", tasks=tasks, parent=push) + return push + + +def _run(observed): + with patch.object(regression, "Push", return_value=observed): + return regression.is_new_build_failure("autoland", observed.rev, LABEL) + + +def test_parent_passed_is_new_failure(): + assert _run(_chain(_failed(), _passed())) is True + + +def test_parent_failed_is_inherited(): + assert _run(_chain(_failed(), _failed())) is False + + +def test_parent_intermittent_is_new_failure(): + parent_tasks = [ + FakeTask(result="failed"), + FakeTask(result="passed"), + ] + assert _run(_chain(_failed(), parent_tasks)) is True + + +def test_coalesced_parent_then_green_grandparent_is_new_failure(): + assert _run(_chain(_failed(), [], _passed())) is True + + +def test_coalesced_parent_then_failed_grandparent_is_inherited(): + assert _run(_chain(_failed(), [], _failed())) is False + + +def test_infra_only_parent_is_skipped_then_green(): + assert _run(_chain(_failed(), _infra(), _passed())) is True + + +def test_running_parent_is_skipped_then_failed(): + assert _run(_chain(_failed(), _running(), _failed())) is False + + +def test_no_parent_runs_agent(): + assert _run(_chain(_failed())) is True + + +def test_no_decisive_ancestor_runs_agent(): + empties = [_failed()] + [[] for _ in range(regression.MAX_DEPTH + 2)] + assert _run(_chain(*empties)) is True + + +def test_mozci_error_runs_agent(): + with patch.object(regression, "Push", side_effect=RuntimeError("boom")): + assert regression.is_new_build_failure("autoland", "rev", LABEL) is True + + +def test_other_label_on_parent_ignored(): + parent_tasks = _failed(label="build-macosx64/opt") # different build, not ours + assert _run(_chain(_failed(), parent_tasks)) is True + + +def test_completed_exception_parent_is_not_treated_as_failed(): + # A completed task with an infra result must not suppress a real regression: + # it is non-decisive, so we walk past it to the green grandparent. + exception_parent = [FakeTask(state="completed", result="exception")] + assert _run(_chain(_failed(), exception_parent, _passed())) is True + + +def test_treeherder_result_vocabulary(): + # success/busted are the Treeherder-source spellings of passed/failed. + assert _run(_chain(_failed(), [FakeTask(result="success")])) is True + assert _run(_chain(_failed(), [FakeTask(result="busted")])) is False diff --git a/uv.lock b/uv.lock index 43fd43cdd1..5342e71c10 100644 --- a/uv.lock +++ b/uv.lock @@ -2666,6 +2666,7 @@ dependencies = [ { name = "httpx" }, { name = "kombu" }, { name = "markdown2" }, + { name = "mozci" }, { name = "pydantic-settings" }, { name = "sendgrid" }, { name = "sentry-sdk" }, @@ -2683,6 +2684,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.26.0" }, { name = "kombu", specifier = ">=5.6,<6" }, { name = "markdown2", specifier = ">=2.4.0" }, + { name = "mozci", specifier = "~=2.4.8" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "sendgrid", specifier = ">=6.12.5" }, From 5d4672c9599cf204fe73ca6a67ebc847c6dc6a9a Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 9 Jul 2026 17:03:35 -0700 Subject: [PATCH 02/10] Use a separate queue for testing --- services/hackbot-pulse-listener/app/consumer.py | 8 +++++++- services/hackbot-pulse-listener/deploy.sh | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/hackbot-pulse-listener/app/consumer.py b/services/hackbot-pulse-listener/app/consumer.py index 7c95b63ee2..d8aceea9e1 100644 --- a/services/hackbot-pulse-listener/app/consumer.py +++ b/services/hackbot-pulse-listener/app/consumer.py @@ -120,12 +120,18 @@ def on_message(body, message): def _build_queues(user: str) -> list[Queue]: + # Both local and prod authenticate as the same pulse user, so the queue name + # must also vary by environment; otherwise both consumers bind to the same + # durable queue and steal each other's messages. Production keeps the plain + # name for continuity. + env = settings.environment + env_segment = "" if env == "production" else f"{env}-" queues = [] for exchange in EXCHANGES: suffix = exchange.rsplit("/", 1)[-1] queues.append( Queue( - name=f"queue/{user}/build-repair-{suffix}", + name=f"queue/{user}/build-repair-{env_segment}{suffix}", exchange=Exchange(exchange, type="topic", no_declare=True), routing_key="#", durable=True, diff --git a/services/hackbot-pulse-listener/deploy.sh b/services/hackbot-pulse-listener/deploy.sh index 5f52c373b6..81ef49d95e 100755 --- a/services/hackbot-pulse-listener/deploy.sh +++ b/services/hackbot-pulse-listener/deploy.sh @@ -100,6 +100,7 @@ gcloud builds submit "${ROOT_DIR}" \ echo "==> Deploying worker pool" ENV_VARS="HACKBOT_API_URL=${HACKBOT_API_URL},HACKBOT_UI_URL=${HACKBOT_UI_URL}" +ENV_VARS="${ENV_VARS},ENVIRONMENT=production" ENV_VARS="${ENV_VARS},PULSE_USER=${PULSE_USER},WATCHED_REPOS=${WATCHED_REPOS}" ENV_VARS="${ENV_VARS},NOTIFICATION_SENDER=${NOTIFICATION_SENDER}" ENV_VARS="${ENV_VARS},NOTIFICATION_TEAM_EMAIL=${NOTIFICATION_TEAM_EMAIL}" From 377880b2190eee97beddfed0cb4f8206c0397887 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 9 Jul 2026 17:06:04 -0700 Subject: [PATCH 03/10] Fix dependencies --- .../hackbot-pulse-listener/pyproject.toml | 3 + uv.lock | 161 ++++-------------- 2 files changed, 32 insertions(+), 132 deletions(-) diff --git a/services/hackbot-pulse-listener/pyproject.toml b/services/hackbot-pulse-listener/pyproject.toml index 4faba06e5e..a9b9dae4be 100644 --- a/services/hackbot-pulse-listener/pyproject.toml +++ b/services/hackbot-pulse-listener/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ "markdown2>=2.4.0", "cachetools>=5.3.0", "sentry-sdk>=2.51.0", + # mozci imports zstandard eagerly (default cache serializer) but only + # declares it under its optional `cache` extra, so pull it in explicitly. + "zstandard~=0.25.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 5342e71c10..34b26c363c 100644 --- a/uv.lock +++ b/uv.lock @@ -725,12 +725,12 @@ requires-dist = [ { name = "shap", extras = ["plots"], specifier = ">=0.51,<0.53" }, { name = "spacy", marker = "extra == 'nlp'", specifier = "==3.8.14" }, { name = "tabulate", specifier = "~=0.10.0" }, - { name = "taskcluster", specifier = ">=97.1,<100.6" }, + { name = "taskcluster", specifier = ">=97.1,<100.4" }, { name = "tenacity", specifier = "~=9.1.4" }, { name = "tqdm", specifier = ">=4.67.3,<4.69.0" }, { name = "unidiff", specifier = "~=0.7.5" }, { name = "weave", specifier = ">=0.50.0" }, - { name = "xgboost", specifier = ">=3.2,<3.4" }, + { name = "xgboost", specifier = "~=3.2.0" }, { name = "zstandard", specifier = "~=0.25.0" }, ] provides-extras = ["nlp", "nn"] @@ -740,7 +740,7 @@ spawn-pipeline = [ { name = "json-e", specifier = "==4.8.2" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "requests", specifier = "==2.34.2" }, - { name = "taskcluster", specifier = "==100.5.0" }, + { name = "taskcluster", specifier = "==100.3.0" }, ] test = [ { name = "coverage", specifier = "==7.14.1" }, @@ -1507,15 +1507,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "diskcache-weave" version = "5.6.3.post1" @@ -1990,43 +1981,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] -[[package]] -name = "glean-parser" -version = "17.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "diskcache" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/c3/24387435f0bf3def52922a70c3f5aab81c82ab6120ac4db719109f0c8b23/glean_parser-17.3.0.tar.gz", hash = "sha256:f70fb4496436068f81ef786029a1b369f61ae2f9327eeb2fa50335dba59ee214", size = 125596, upload-time = "2025-07-09T09:03:02.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/31/8176e4e77144bae2493d9da708c15da01e403befbf3c9490db001a64da53/glean_parser-17.3.0-py3-none-any.whl", hash = "sha256:84d975d40333e97d9f6e0ce0f22db4e1e3d8e088b2da678e34cc7ab3361f071d", size = 115439, upload-time = "2025-07-09T09:03:00.784Z" }, -] - -[[package]] -name = "glean-sdk" -version = "65.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "glean-parser" }, - { name = "semver" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/8a/a45f4ee0a98901cd474204587ab6ecf18f141803a11d3c091c1f035fbfed/glean_sdk-65.2.3.tar.gz", hash = "sha256:e8d17d1851b688b242c9de51c9125b41cd87a30a788a2a26be6cdf00e628d1be", size = 260945, upload-time = "2025-11-06T11:15:08.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/43/fabb546efe0925ba11c6a1d776507f75c267cfa8401729391e14e2fc3fc6/glean_sdk-65.2.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fdb39d22e2a6c095ec8990b71b2eb833cbde4a807e8032a4ae218a2983eb23bd", size = 1686649, upload-time = "2025-11-06T11:12:57.204Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/cd8d6b2f31e96c4fd4e3fe54ad3f3c2ed51bbba18b2a5ff4adeb94970af3/glean_sdk-65.2.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:263d1a91f1048261013f11a78073d5e1b9d17f29065f9ef7e296dc995f2f846a", size = 898508, upload-time = "2025-11-06T11:12:59.447Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ce/76677fa87e58abeb4b016b1870fe1b6a9b616ea857099dc6a95f504e8b99/glean_sdk-65.2.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:38b90604ae9f55fc522e67b47989ed547ccd35d7dbb871b5f78caffedc9cbae0", size = 866137, upload-time = "2025-11-06T11:13:00.771Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7e/51f1c15418f7e173bde4d73c06ea153e8447fc10ec05d62fb052098b08cd/glean_sdk-65.2.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182eaa29eba7096c751353eac8faec1916ea6b81445ea688874b732d45727b7a", size = 821146, upload-time = "2025-11-06T11:14:59.985Z" }, - { url = "https://files.pythonhosted.org/packages/93/65/ed4c48130e3423de9386011796452a81ba552429a6b6f0d6ec14e5aaa14b/glean_sdk-65.2.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8346bd5938218906ad572ab82c5fd4468481dda6a2ba41e5fbe67c9383bbcf9", size = 934818, upload-time = "2025-11-06T11:14:31.725Z" }, - { url = "https://files.pythonhosted.org/packages/1b/24/ee4285df4a19726be7941bad99daa6d1ef3c581fb1ca54f2fdd68d3ddc2d/glean_sdk-65.2.3-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:45d61656d4eead892cdc2cac7d8b2ba1185a9520a0ca50e8326e8a6c2566519e", size = 934533, upload-time = "2025-11-06T11:15:06.777Z" }, - { url = "https://files.pythonhosted.org/packages/68/d0/78b6e7d156aa0267b4d65dfc67746254a039d485f6943cce86b2091f9443/glean_sdk-65.2.3-py3-none-win_amd64.whl", hash = "sha256:9e603dc7fca2b6cd739a3a31195d4d564db542de5571d7afbd33a7ca8fb8fdde", size = 878415, upload-time = "2025-11-06T11:14:06.474Z" }, - { url = "https://files.pythonhosted.org/packages/37/a2/f8bf156045be612d9dbf44c8c63e889050f94da3205ecdb4a836b79948bb/glean_sdk-65.2.3-py3-none-win_arm64.whl", hash = "sha256:70920c20bbf393da807933056e85575598078f8124c44c68257df195b5d8d5f4", size = 741441, upload-time = "2025-11-06T11:15:20.785Z" }, -] - [[package]] name = "google-api-core" version = "2.30.3" @@ -2051,15 +2005,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.55.2" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/b9/e370d86fea3da13ec0256df30323dd26c0cb9c8c85f0c6ec42ac9df0106b/google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae", size = 361414, upload-time = "2026-07-07T18:43:21.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/f6/494e18317546d7def90c957b71d68b025d24f0e22e486c2606bc57765c48/google_auth-2.54.0.tar.gz", hash = "sha256:130f6fd5e3f497fdad897a23ed9489973437edf561238c4b92a4d02c435f8af9", size = 343161, upload-time = "2026-06-12T18:03:17.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/c6/02eb5a337ac316a4c30c012e747bad5cea36e1a876efecdf80865541f7d8/google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b", size = 256778, upload-time = "2026-07-07T18:43:19.52Z" }, + { url = "https://files.pythonhosted.org/packages/70/c5/d53bddd2c0949833fcb4ea06f9d5dd1c40575a1a4214cd1021eff57ba301/google_auth-2.54.0-py3-none-any.whl", hash = "sha256:784e9837f92244141250470d47c893df50cbab485ce491aca5e9deb558ad2b48", size = 249878, upload-time = "2026-06-12T18:02:57.58Z" }, ] [package.optional-dependencies] @@ -2080,26 +2034,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] -[[package]] -name = "google-cloud-pubsub" -version = "2.39.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "grpcio-status" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/2b/4bf2c17e319ff65340389565b0e1b4d72696d87802b2f5f94390fbefa73c/google_cloud_pubsub-2.39.0.tar.gz", hash = "sha256:eed65e25f57f95bf3e02d96d7ee171688b23922471f9f21b5a91ed90e1282c0f", size = 402096, upload-time = "2026-06-03T15:28:26.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/20/dd0b27d4ad4577c062e77ff968ca3e2d404186cd78c8a2a53a0ef5fe5389/google_cloud_pubsub-2.39.0-py3-none-any.whl", hash = "sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19", size = 324665, upload-time = "2026-06-03T15:27:41.119Z" }, -] - [[package]] name = "google-cloud-run" version = "0.16.0" @@ -2506,7 +2440,7 @@ dependencies = [ { name = "agent-tools", extra = ["bugzilla", "firefox"] }, { name = "bugsy" }, { name = "claude-agent-sdk" }, - { name = "hackbot-runtime", extra = ["claude-sdk", "phabricator"] }, + { name = "hackbot-runtime", extra = ["claude-sdk"] }, { name = "mcp" }, { name = "starlette" }, { name = "uvicorn" }, @@ -2517,7 +2451,7 @@ requires-dist = [ { name = "agent-tools", extras = ["bugzilla", "firefox"], editable = "libs/agent-tools" }, { name = "bugsy" }, { name = "claude-agent-sdk", specifier = ">=0.1.30" }, - { name = "hackbot-runtime", extras = ["claude-sdk", "phabricator"], editable = "libs/hackbot-runtime" }, + { name = "hackbot-runtime", extras = ["claude-sdk"], editable = "libs/hackbot-runtime" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "starlette", specifier = ">=0.36.0" }, { name = "uvicorn", specifier = ">=0.27.0" }, @@ -2616,11 +2550,8 @@ dependencies = [ { name = "asyncpg" }, { name = "cloud-sql-python-connector", extra = ["asyncpg"] }, { name = "fastapi" }, - { name = "google-auth" }, - { name = "google-cloud-pubsub" }, { name = "google-cloud-run" }, { name = "google-cloud-storage" }, - { name = "hackbot-runtime" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "sentry-sdk" }, @@ -2641,11 +2572,8 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.29.0" }, { name = "cloud-sql-python-connector", extras = ["asyncpg"], specifier = ">=1.5.0" }, { name = "fastapi", specifier = ">=0.109.0" }, - { name = "google-auth", specifier = ">=2.29.0" }, - { name = "google-cloud-pubsub", specifier = ">=2.21.0" }, { name = "google-cloud-run", specifier = ">=0.10.0" }, { name = "google-cloud-storage", specifier = ">=2.16.0" }, - { name = "hackbot-runtime", editable = "libs/hackbot-runtime" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" }, { name = "pydantic", specifier = ">=2.6.0" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, @@ -2671,6 +2599,7 @@ dependencies = [ { name = "sendgrid" }, { name = "sentry-sdk" }, { name = "taskcluster" }, + { name = "zstandard" }, ] [package.optional-dependencies] @@ -2689,7 +2618,8 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "sendgrid", specifier = ">=6.12.5" }, { name = "sentry-sdk", specifier = ">=2.51.0" }, - { name = "taskcluster", specifier = ">=97.1,<100.6" }, + { name = "taskcluster", specifier = ">=97.1,<100.4" }, + { name = "zstandard", specifier = "~=0.25.0" }, ] provides-extras = ["dev"] @@ -2702,7 +2632,6 @@ dependencies = [ { name = "google-auth" }, { name = "pydantic-settings" }, { name = "requests" }, - { name = "weave" }, ] [package.optional-dependencies] @@ -2710,9 +2639,6 @@ claude-sdk = [ { name = "agent-tools", extra = ["claude-sdk"] }, { name = "claude-agent-sdk" }, ] -phabricator = [ - { name = "mozphab" }, -] [package.metadata] requires-dist = [ @@ -2720,12 +2646,10 @@ requires-dist = [ { name = "agent-tools", extras = ["claude-sdk"], marker = "extra == 'claude-sdk'", editable = "libs/agent-tools" }, { name = "claude-agent-sdk", marker = "extra == 'claude-sdk'", specifier = ">=0.1.30" }, { name = "google-auth", specifier = ">=2.0.0" }, - { name = "mozphab", marker = "extra == 'phabricator'", specifier = "==2.15.3" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, { name = "requests", specifier = ">=2.32.0" }, - { name = "weave", specifier = ">=0.53.1" }, ] -provides-extras = ["claude-sdk", "phabricator"] +provides-extras = ["claude-sdk"] [[package]] name = "hf-xet" @@ -4098,7 +4022,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4116,9 +4040,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, ] [[package]] @@ -4300,25 +4224,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/2d/5cd9787f930f2a0cf4ab3d105ce15edc39733328e7606d97124877b70c4e/mozInstall-2.1.0-py2.py3-none-any.whl", hash = "sha256:8abc26e37b1976eb3d815ee2a42bb6bbb10926e16620eabb6f8f1ed764b0c0fe", size = 6687, upload-time = "2023-07-28T13:43:23.953Z" }, ] -[[package]] -name = "mozphab" -version = "2.15.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "distro" }, - { name = "glean-sdk" }, - { name = "packaging" }, - { name = "python-hglib" }, - { name = "sentry-sdk" }, - { name = "setuptools" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/f8/af97c0db47ef85ea801d782fcc6a3f6cbf2ee7c06ef75946fdf40ed28e3c/mozphab-2.15.3.tar.gz", hash = "sha256:2eb9e3fb4b936acc8d340b49739905b4d8f2f5d7450e519cfdd2cebf13bb8226", size = 278633, upload-time = "2026-06-25T16:31:57.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/32/512ae30c4ef7fa2c606a24fc1f9a331a215988f4819328233d427f3269de/mozphab-2.15.3-py3-none-any.whl", hash = "sha256:3265eb9ddd03e08c44c8ea784912790f60a8ec0a1f104fdc64eea410f4dff23e", size = 124401, upload-time = "2026-06-25T16:31:55.998Z" }, -] - [[package]] name = "multidict" version = "6.7.1" @@ -6545,15 +6450,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] -[[package]] -name = "semver" -version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, -] - [[package]] name = "sendgrid" version = "6.12.5" @@ -6946,7 +6842,7 @@ wheels = [ [[package]] name = "taskcluster" -version = "100.5.0" +version = "100.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -6957,9 +6853,9 @@ dependencies = [ { name = "slugid" }, { name = "taskcluster-urls" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/c5/823ca59868de80f05f6992482dfb1b0106bd106b9eb410e1c3dd2990f7a3/taskcluster-100.5.0.tar.gz", hash = "sha256:5b2dc2624e7e1a1ca8352b63cf9c4734e9edfab4452a4b3adaa45ce09b19f444", size = 254859, upload-time = "2026-06-25T08:25:48.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/89/b809400a31d06b8a323289db0ad747118d923c77edcedc22cbb208eeca9c/taskcluster-100.3.0.tar.gz", hash = "sha256:72c1d1a27be49f70732711f16d7555669399d5fd5ab4c3f6cc3ba63fb62c79dc", size = 254833, upload-time = "2026-06-09T10:42:04.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/e6/459aeffc43c6d7266d34388ec167c34ccf02dab20970365ab499602134d2/taskcluster-100.5.0-py3-none-any.whl", hash = "sha256:b56b385da136024cb0f7326a545156bc0d9827194930636bd2d4a9dde2e7d59f", size = 150887, upload-time = "2026-06-25T08:25:47.427Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/11c44cc059c8edd87a788b5734265b8964eb56042a975085d3c115fc0279/taskcluster-100.3.0-py3-none-any.whl", hash = "sha256:127893d98e5b81ad937480fe7ae5d8b97f3c3b84b6fec09eb5ab1bac8e01d9d1", size = 150897, upload-time = "2026-06-09T10:42:03.374Z" }, ] [[package]] @@ -7567,7 +7463,7 @@ wheels = [ [[package]] name = "weave" -version = "0.53.1" +version = "0.52.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -7578,6 +7474,7 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "polyfile-weave" }, { name = "pydantic" }, @@ -7585,9 +7482,9 @@ dependencies = [ { name = "tenacity" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/34/c9ddb250e27708b581f52fd75f94ca04edbe7ed287560fe13995e042521d/weave-0.53.1.tar.gz", hash = "sha256:623c1918394e8493a3344fb89ffc71b3e1e96c3bd247a77309aa2b6e78475cb9", size = 1113042, upload-time = "2026-07-02T21:17:45.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/df/dcb32bf366eca5e11d345a8e15238256396a9ff7346a29b3290a358b9cd8/weave-0.52.43.tar.gz", hash = "sha256:3c4abcb4da33e0db8a03bd52739503d99c90bdb1922b704ca2b2df8a0f6d561e", size = 977397, upload-time = "2026-06-15T21:29:23.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/87/99d77ddb9f1008a92e36e6d72b0ff63e17fbe22abe6b383bbfb80001d70b/weave-0.53.1-py3-none-any.whl", hash = "sha256:95c45e22fe38cf241f4ff26d120c86bcff3652e4c813e0bcf5950ec26e7f5db8", size = 1349841, upload-time = "2026-07-02T21:17:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a0/e823f714e337c6342a831307d21d2a12c87bc6a6015fc0ac62be9dd8e01f/weave-0.52.43-py3-none-any.whl", hash = "sha256:d7fb10e893c9da57c7663c8c51a1a67eb59216f9da0e4056a244a41de63faf19", size = 1199217, upload-time = "2026-06-15T21:29:21.267Z" }, ] [[package]] @@ -7744,20 +7641,20 @@ wheels = [ [[package]] name = "xgboost" -version = "3.3.0" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/41/846d4de2b8fc694073fd3ac5052caf68caa1ea11cb7fa32d7ad9c049b232/xgboost-3.3.0.tar.gz", hash = "sha256:58bcb8a4cace648cdab7b94fa4f16d2c9ff26d90dd4d26907168106fa06d8746", size = 1224702, upload-time = "2026-06-17T21:26:50.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/bb/1eb0242409d22db725d7a88088e6cfd6556829fb0736f9ff69aa9f1e9455/xgboost-3.2.0.tar.gz", hash = "sha256:99b0e9a2a64896cdaf509c5e46372d336c692406646d20f2af505003c0c5d70d", size = 1263936, upload-time = "2026-02-10T11:03:05.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/72/3b68983c0215ef65d48e9eeb1f168c3c6e3d62a61ece605de3209c79cae1/xgboost-3.3.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:07688a377046b8640897b62421150bf73c6cc7101823474ec6ad08b93290f587", size = 2553505, upload-time = "2026-06-17T21:21:32.146Z" }, - { url = "https://files.pythonhosted.org/packages/c9/62/b49e756822b29909d0c95ed334662dc6c7c81a99ec6bc10dc18e69f3d6e7/xgboost-3.3.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:af7cea10f418b7c251ddc8da440f57bdab2990b5fc9f74a35a92b0f150ea287d", size = 2376040, upload-time = "2026-06-17T21:22:01.981Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/a0adcd1ee28f525bd5c9dc3ebe78a7599bf97c22866d6449f967b829e338/xgboost-3.3.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:624a83aeb1e7ba081719795db179f4ce6fff12e79de05cd9baf15ee48fd22f0e", size = 98180629, upload-time = "2026-06-17T21:24:00.804Z" }, - { url = "https://files.pythonhosted.org/packages/47/1f/8b3e578cfd8e3bcdb4374e2bbe0b40b4e5320accb5cbdcf535ecc512eb5c/xgboost-3.3.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f59edaf28eccd1c519788607c72ed907ee6cedfa933d706620bc1612d24b354e", size = 98716607, upload-time = "2026-06-17T21:26:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/07/6b/087fd5d28fdbb90d385c50ee9308a820241b82feebdf42e72e19a48e4b32/xgboost-3.3.0-py3-none-win_amd64.whl", hash = "sha256:b06057f6a018fc04e6b3e0c15568ca636b8151a5b5f333478e500fcaf4fc7594", size = 69522696, upload-time = "2026-06-17T21:20:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/6e4cdd877c24adf56cb3586bc96d93d4dcd780b5ea1efb32e1ee0de08bae/xgboost-3.2.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2f661966d3e322536d9c448090a870fcba1e32ee5760c10b7c46bac7a342079a", size = 2507014, upload-time = "2026-02-10T10:50:57.44Z" }, + { url = "https://files.pythonhosted.org/packages/93/f1/c09ef1add609453aa3ba5bafcd0d1c1a805c1263c0b60138ec968f8ec296/xgboost-3.2.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:eabbd40d474b8dbf6cb3536325f9150b9e6f0db32d18de9914fb3227d0bef5b7", size = 2328527, upload-time = "2026-02-10T10:51:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/96/9f/d9914a7b8df842832850b1a18e5f47aaa071c217cdd1da2ae9deb291018b/xgboost-3.2.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:852eabc6d3b3702a59bf78dbfdcd1cb9c4d3a3b6e5ed1f8781d8b9512354fdd2", size = 131100954, upload-time = "2026-02-10T11:02:42.704Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/679de17c2caa4fd3b0b4386ecf7377301702cb0afb22930a07c142fcb1d8/xgboost-3.2.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:99b4a6bbcb47212fec5cf5fbe12347215f073c08967431b0122cfbd1ee70312c", size = 131748579, upload-time = "2026-02-10T10:54:40.424Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1661dd114a914a67e3f7ab66fa1382e7599c2a8c340f314ad30a3e2b4d08/xgboost-3.2.0-py3-none-win_amd64.whl", hash = "sha256:0d169736fd836fc13646c7ab787167b3a8110351c2c6bc770c755ee1618f0442", size = 101681668, upload-time = "2026-02-10T10:59:31.202Z" }, ] [[package]] From ec27d7a1e610147dd43fde8ca2fb1a84494afb17 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Tue, 14 Jul 2026 09:52:16 -0700 Subject: [PATCH 04/10] Add retries when parent builds are still in progress --- .../hackbot-pulse-listener/app/__main__.py | 2 +- services/hackbot-pulse-listener/app/config.py | 7 +- .../hackbot-pulse-listener/app/consumer.py | 32 ++++- .../hackbot-pulse-listener/app/regression.py | 132 +++++++++++++----- 4 files changed, 132 insertions(+), 41 deletions(-) diff --git a/services/hackbot-pulse-listener/app/__main__.py b/services/hackbot-pulse-listener/app/__main__.py index 80c99d76e7..76d736f138 100644 --- a/services/hackbot-pulse-listener/app/__main__.py +++ b/services/hackbot-pulse-listener/app/__main__.py @@ -19,7 +19,7 @@ def main() -> None: logger.warning("PULSE_USER/PULSE_PASSWORD not set; listener will not start") return - executor = ThreadPoolExecutor(max_workers=settings.poll_max_workers) + executor = ThreadPoolExecutor(max_workers=settings.max_workers) consumer_obj = consumer.build_consumer(executor) def shutdown(signum, _frame): diff --git a/services/hackbot-pulse-listener/app/config.py b/services/hackbot-pulse-listener/app/config.py index 969cff3e8c..2525501525 100644 --- a/services/hackbot-pulse-listener/app/config.py +++ b/services/hackbot-pulse-listener/app/config.py @@ -32,7 +32,12 @@ class Settings(BaseSettings): # Polling the API for run completion poll_interval_seconds: int = 60 run_max_age_minutes: int = 12 * 60 - poll_max_workers: int = 8 + # Shared worker pool for message processing and run polling. A + # regression check may block for a few minutes waiting for a parent + # build to settle, so the pool is sized well above the number of + # builds/runs in flight at once. Threads are cheap and mostly idle + # while waiting. + max_workers: int = 256 # Email notifications (SendGrid) sendgrid_api_key: str | None = None diff --git a/services/hackbot-pulse-listener/app/consumer.py b/services/hackbot-pulse-listener/app/consumer.py index d8aceea9e1..3e8e85d93d 100644 --- a/services/hackbot-pulse-listener/app/consumer.py +++ b/services/hackbot-pulse-listener/app/consumer.py @@ -1,4 +1,5 @@ import logging +import threading from concurrent.futures import Executor from cachetools import TTLCache @@ -19,10 +20,12 @@ # recorded only once we actually trigger a run, so an inherited failure on one # build label never suppresses a genuine regression on another label of the # same push, while a revision that breaks many builds still triggers only once. -# Only the single consumer thread touches it, so no lock is needed. +# Messages are handled on worker threads, so the check-and-record is done under +# a lock. _seen: TTLCache = TTLCache( maxsize=settings.dedupe_max_size, ttl=settings.dedupe_ttl_seconds ) +_seen_lock = threading.Lock() def process(body: dict, executor: Executor) -> str | None: @@ -46,7 +49,9 @@ def process(body: dict, executor: Executor) -> str | None: logger.warning("No GECKO_HEAD_REV for task %s; skipping", task_id) return None - if hg_revision in _seen: + with _seen_lock: + already_seen = hg_revision in _seen + if already_seen: logger.info("Revision %s already triggered a run; skipping", hg_revision) return None @@ -57,7 +62,12 @@ def process(body: dict, executor: Executor) -> str | None: hg_revision, ) return None - _seen[hg_revision] = True + + with _seen_lock: + if hg_revision in _seen: + logger.info("Revision %s already triggered a run; skipping", hg_revision) + return None + _seen[hg_revision] = True git_commit = lando.hg_to_git(hg_revision) if not git_commit: @@ -67,7 +77,8 @@ def process(body: dict, executor: Executor) -> str | None: task_id, project, ) - _seen.pop(hg_revision, None) + with _seen_lock: + _seen.pop(hg_revision, None) return None inputs: dict = { @@ -84,7 +95,8 @@ def process(body: dict, executor: Executor) -> str | None: run_id = client.trigger_run(inputs) except Exception: logger.exception("Failed to trigger build-repair run for %s", hg_revision) - _seen.pop(hg_revision, None) + with _seen_lock: + _seen.pop(hg_revision, None) return None logger.info( @@ -108,11 +120,19 @@ def process(body: dict, executor: Executor) -> str | None: def make_handler(executor: Executor): - def on_message(body, message): + def run(body: dict) -> None: try: process(body, executor) except Exception: logger.exception("Error handling pulse message") + + def on_message(body, message): + # Process on a worker thread so a regression check that blocks waiting + # for a parent build to settle never stalls the consumer thread. + try: + executor.submit(run, body) + except Exception: + logger.exception("Failed to dispatch pulse message") finally: message.ack() diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py index 93d2846bfc..299783c8d7 100644 --- a/services/hackbot-pulse-listener/app/regression.py +++ b/services/hackbot-pulse-listener/app/regression.py @@ -1,4 +1,5 @@ import logging +import time from mozci.errors import ParentPushNotFound from mozci.push import Push @@ -9,6 +10,16 @@ # giving up. Mirrors mozci's own MAX_DEPTH. MAX_DEPTH = 20 +# When the nearest ancestor that ran the build has not settled yet (still +# running/pending, or its result has not propagated into mozci's data sources), +# retry the whole check in-process after a delay instead of racing ahead. On +# autoland a stack lands and its pushes build near-simultaneously, so a parent +# build of the same label often resolves seconds to a couple of minutes after +# the tip's; skipping it as if coalesced misreports an inherited failure as new. +# The budget is small so the (single) consumer thread is never blocked for long. +RETRY_DELAY_SECONDS = 60 +MAX_RETRIES = 3 + # mozci results vary by data source (Taskcluster vs Treeherder), so match both # vocabularies. Only genuine build failures count as failed; infra results # ("exception", "canceled", "superseded", ...) are deliberately non-decisive so @@ -16,63 +27,118 @@ _PASSED_RESULTS = ("passed", "success") _FAILED_RESULTS = ("busted", "failed") +# States in which a task will no longer change (mirrors mozci's TASK_FINAL_STATES). +# A build in any other state (running, pending, unscheduled) is still in flight, +# so its result is not knowable yet and the check should wait for it. +_TERMINAL_STATES = ("completed", "failed", "exception") + +# Sentinel meaning the decision cannot be made yet because an ancestor build is +# still in flight; the caller retries after a delay. +_PENDING = object() -def _build_status(push: Push, label: str) -> str | None: - """Return 'passed', 'failed', or None for a build label on a push. - None means the build produced no decisive result here: it was coalesced (not - scheduled), is still running, or only hit infra errors. Retriggers are - collapsed: any green run means 'passed'; only a genuine build failure - (never an infra exception) counts as 'failed'. +def _build_status(push: Push, label: str): + """Return 'passed', 'failed', _PENDING, or None for a build label on a push. + + 'passed'/'failed' come from a completed run with a decisive result. + _PENDING means a task exists but has not settled yet (still running/pending, + or its result has not been ingested), so waiting may change the answer. + None means no decisive result and nothing in flight: the build was coalesced + (never scheduled) or only hit infra errors (exception, ...), both of which + are deliberately non-decisive so they never suppress a real regression. + Retriggers are collapsed: any green run means 'passed'; only a genuine build + failure counts as 'failed'. """ - results = [ - t.result for t in push.tasks if t.label == label and t.state == "completed" - ] + label_tasks = [t for t in push.tasks if t.label == label] + results = [t.result for t in label_tasks if t.state == "completed"] if any(r in _PASSED_RESULTS for r in results): return "passed" if any(r in _FAILED_RESULTS for r in results): return "failed" + if any(t.state not in _TERMINAL_STATES for t in label_tasks): + return _PENDING return None +def _classify(branch: str, rev: str, label: str): + """Walk ancestors once. Returns True (new), False (inherited), or _PENDING. + + A fresh Push is built each call so retries re-fetch live data (recent pushes + are not finalized in mozci, so their tasks are never served from cache). + """ + ancestor = Push(rev, branch=branch) + for _ in range(MAX_DEPTH): + try: + ancestor = ancestor.parent + except ParentPushNotFound: + break + status = _build_status(ancestor, label) + if status is None: + continue + if status is _PENDING: + logger.info( + "Build %s not settled yet at %s; deferring decision for %s", + label, + ancestor.rev, + rev, + ) + return _PENDING + if status == "failed": + logger.info( + "Build %s already failing at %s; inherited failure at %s", + label, + ancestor.rev, + rev, + ) + return False + logger.info( + "Build %s passed at %s; new failure introduced at %s", + label, + ancestor.rev, + rev, + ) + return True + + logger.warning( + "No ancestor within %s pushes ran build %s; running agent", MAX_DEPTH, label + ) + return True + + def is_new_build_failure(branch: str, rev: str, label: str) -> bool: """Return True if this push introduced the failure, False if it inherited it. Walks back over pushes that did not run the build (coalescing) until it - finds the nearest ancestor that did. Fails open (returns True) on any - mozci/network error or if no ancestor within MAX_DEPTH ran the build, so we - never silently drop a real regression. + finds the nearest ancestor that did. When that ancestor's build is still in + flight, retries in-process after a delay rather than racing ahead and + misreporting an inherited failure as new. Fails open (returns True) on any + mozci/network error, if the build stays unsettled after MAX_RETRIES, or if + no ancestor within MAX_DEPTH ran the build, so we never silently drop a real + regression. """ try: - ancestor = Push(rev, branch=branch) - for _ in range(MAX_DEPTH): - try: - ancestor = ancestor.parent - except ParentPushNotFound: - break - status = _build_status(ancestor, label) - if status is None: - continue - if status == "failed": + for attempt in range(MAX_RETRIES + 1): + result = _classify(branch, rev, label) + if result is not _PENDING: + return result + if attempt < MAX_RETRIES: logger.info( - "Build %s already failing at %s; inherited failure at %s", + "Build %s unsettled on an ancestor of %s; retrying in %ss (%s/%s)", label, - ancestor.rev, rev, + RETRY_DELAY_SECONDS, + attempt + 1, + MAX_RETRIES, ) - return False - logger.info( - "Build %s passed at %s; new failure introduced at %s", - label, - ancestor.rev, - rev, - ) - return True + time.sleep(RETRY_DELAY_SECONDS) except Exception: logger.exception("Regression check failed for %s@%s; running agent", label, rev) return True logger.warning( - "No ancestor within %s pushes ran build %s; running agent", MAX_DEPTH, label + "Build %s still unsettled after %s retries at %s; running agent", + label, + MAX_RETRIES, + rev, ) return True From 998477ac4b3b94ac7cd7d1c1cfbc6e91084af654 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Tue, 14 Jul 2026 14:25:01 -0700 Subject: [PATCH 05/10] Increase wait time and handled unscheduled builds --- .../hackbot-pulse-listener/app/regression.py | 124 ++++++++++-------- 1 file changed, 72 insertions(+), 52 deletions(-) diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py index 299783c8d7..880f5a1a14 100644 --- a/services/hackbot-pulse-listener/app/regression.py +++ b/services/hackbot-pulse-listener/app/regression.py @@ -10,61 +10,80 @@ # giving up. Mirrors mozci's own MAX_DEPTH. MAX_DEPTH = 20 -# When the nearest ancestor that ran the build has not settled yet (still -# running/pending, or its result has not propagated into mozci's data sources), -# retry the whole check in-process after a delay instead of racing ahead. On -# autoland a stack lands and its pushes build near-simultaneously, so a parent -# build of the same label often resolves seconds to a couple of minutes after -# the tip's; skipping it as if coalesced misreports an inherited failure as new. -# The budget is small so the (single) consumer thread is never blocked for long. -RETRY_DELAY_SECONDS = 60 -MAX_RETRIES = 3 +# When the nearest ancestor that ran the build has not produced a decisive +# result yet, wait for it in-process instead of racing ahead and misreporting an +# inherited failure as new. A build can take tens of minutes and may hit an +# infra exception and be auto-retried, so we poll until it settles or the +# deadline elapses (well above a normal build + one retry). Fails open after the +# deadline so a real regression is never silently dropped. +POLL_INTERVAL_SECONDS = 120 +MAX_WAIT_SECONDS = 60 * 60 # mozci results vary by data source (Taskcluster vs Treeherder), so match both -# vocabularies. Only genuine build failures count as failed; infra results -# ("exception", "canceled", "superseded", ...) are deliberately non-decisive so -# they never suppress a real regression. +# vocabularies. Only genuine build failures count as failed; results like +# "canceled"/"superseded" are non-decisive so they never suppress a regression. _PASSED_RESULTS = ("passed", "success") _FAILED_RESULTS = ("busted", "failed") -# States in which a task will no longer change (mirrors mozci's TASK_FINAL_STATES). -# A build in any other state (running, pending, unscheduled) is still in flight, -# so its result is not knowable yet and the check should wait for it. -_TERMINAL_STATES = ("completed", "failed", "exception") +# A build in one of these states, or with one of these (infra) results, has not +# settled: it is still running or was retried after an exception, so its outcome +# is not knowable yet and we wait for it. Anything else that is not a decisive +# pass/fail (unscheduled, canceled, superseded, ...) or a build that never ran +# at all (coalesced) is treated as non-decisive and skipped. +_UNSETTLED_STATES = ("pending", "running", "exception") +_UNSETTLED_RESULTS = ("exception", "retry") -# Sentinel meaning the decision cannot be made yet because an ancestor build is -# still in flight; the caller retries after a delay. +# Sentinel meaning the decision cannot be made yet because an ancestor build has +# not settled; the caller waits and re-checks. _PENDING = object() def _build_status(push: Push, label: str): """Return 'passed', 'failed', _PENDING, or None for a build label on a push. - 'passed'/'failed' come from a completed run with a decisive result. - _PENDING means a task exists but has not settled yet (still running/pending, - or its result has not been ingested), so waiting may change the answer. - None means no decisive result and nothing in flight: the build was coalesced - (never scheduled) or only hit infra errors (exception, ...), both of which - are deliberately non-decisive so they never suppress a real regression. - Retriggers are collapsed: any green run means 'passed'; only a genuine build - failure counts as 'failed'. + 'passed'/'failed' come from a run with a decisive result. _PENDING means the + result is not knowable yet: a task exists but has not settled (still running, + or exceptioned and awaiting a retry), or no task is visible yet but the build + was scheduled to run on this push (its result has not propagated). None means + the build produced no decisive result and none is coming: it was coalesced / + never scheduled, or it only reached a non-decisive terminal state. None is + deliberately non-decisive so coalescing gaps are skipped and never suppress a + real regression. Retriggers are collapsed: any green run means 'passed'; only + a genuine build failure counts as 'failed'. """ label_tasks = [t for t in push.tasks if t.label == label] - results = [t.result for t in label_tasks if t.state == "completed"] - if any(r in _PASSED_RESULTS for r in results): - return "passed" - if any(r in _FAILED_RESULTS for r in results): - return "failed" - if any(t.state not in _TERMINAL_STATES for t in label_tasks): - return _PENDING - return None + if label_tasks: + results = [t.result for t in label_tasks] + if any(r in _PASSED_RESULTS for r in results): + return "passed" + if any(r in _FAILED_RESULTS for r in results): + return "failed" + if any( + t.state in _UNSETTLED_STATES or t.result in _UNSETTLED_RESULTS + for t in label_tasks + ): + return _PENDING + # Ran but reached a non-decisive terminal state (canceled, ...): skip. + return None + + # No task for this label on the push. Distinguish a build that was scheduled + # to run here but whose result is not visible yet (wait) from one that was + # never scheduled / coalesced away (skip), using the decision task's + # scheduled set (available well before the builds themselves finish). + try: + scheduled = label in push.scheduled_task_labels + except Exception: + logger.debug("Could not read scheduled task labels for %s", push.rev) + scheduled = False + return _PENDING if scheduled else None def _classify(branch: str, rev: str, label: str): """Walk ancestors once. Returns True (new), False (inherited), or _PENDING. - A fresh Push is built each call so retries re-fetch live data (recent pushes - are not finalized in mozci, so their tasks are never served from cache). + A fresh Push is built each call so re-checks re-fetch live data (recent + pushes are not finalized in mozci, so their tasks are never served from + cache). """ ancestor = Push(rev, branch=branch) for _ in range(MAX_DEPTH): @@ -109,36 +128,37 @@ def is_new_build_failure(branch: str, rev: str, label: str) -> bool: """Return True if this push introduced the failure, False if it inherited it. Walks back over pushes that did not run the build (coalescing) until it - finds the nearest ancestor that did. When that ancestor's build is still in - flight, retries in-process after a delay rather than racing ahead and + finds the nearest ancestor that did. When that ancestor's build has not + settled yet, waits in-process and re-checks until it produces a decisive + result or MAX_WAIT_SECONDS elapses, rather than racing ahead and misreporting an inherited failure as new. Fails open (returns True) on any - mozci/network error, if the build stays unsettled after MAX_RETRIES, or if + mozci/network error, if the build stays unsettled past the deadline, or if no ancestor within MAX_DEPTH ran the build, so we never silently drop a real regression. """ try: - for attempt in range(MAX_RETRIES + 1): + deadline = time.monotonic() + MAX_WAIT_SECONDS + while True: result = _classify(branch, rev, label) if result is not _PENDING: return result - if attempt < MAX_RETRIES: - logger.info( - "Build %s unsettled on an ancestor of %s; retrying in %ss (%s/%s)", - label, - rev, - RETRY_DELAY_SECONDS, - attempt + 1, - MAX_RETRIES, - ) - time.sleep(RETRY_DELAY_SECONDS) + if time.monotonic() >= deadline: + break + logger.info( + "Waiting %ss for an unsettled ancestor build of %s (%s)", + POLL_INTERVAL_SECONDS, + rev, + label, + ) + time.sleep(POLL_INTERVAL_SECONDS) except Exception: logger.exception("Regression check failed for %s@%s; running agent", label, rev) return True logger.warning( - "Build %s still unsettled after %s retries at %s; running agent", + "Build %s still unsettled after %ss at %s; running agent", label, - MAX_RETRIES, + MAX_WAIT_SECONDS, rev, ) return True From 4686aaf5fa671269fcaf4e3d25201e699f24a544 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Tue, 14 Jul 2026 16:25:53 -0700 Subject: [PATCH 06/10] Reuse mozci failed field --- services/hackbot-pulse-listener/app/regression.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py index 880f5a1a14..da99558782 100644 --- a/services/hackbot-pulse-listener/app/regression.py +++ b/services/hackbot-pulse-listener/app/regression.py @@ -19,11 +19,10 @@ POLL_INTERVAL_SECONDS = 120 MAX_WAIT_SECONDS = 60 * 60 -# mozci results vary by data source (Taskcluster vs Treeherder), so match both -# vocabularies. Only genuine build failures count as failed; results like -# "canceled"/"superseded" are non-decisive so they never suppress a regression. +# A green build; mozci reports it as "passed" (Taskcluster) or "success" +# (Treeherder). Failures are read from mozci's Task.failed attribute instead of +# reimplementing the vocabulary. _PASSED_RESULTS = ("passed", "success") -_FAILED_RESULTS = ("busted", "failed") # A build in one of these states, or with one of these (infra) results, has not # settled: it is still running or was retried after an exception, so its outcome @@ -53,10 +52,12 @@ def _build_status(push: Push, label: str): """ label_tasks = [t for t in push.tasks if t.label == label] if label_tasks: - results = [t.result for t in label_tasks] - if any(r in _PASSED_RESULTS for r in results): + if any(t.result in _PASSED_RESULTS for t in label_tasks): return "passed" - if any(r in _FAILED_RESULTS for r in results): + # mozci's Task.failed also counts `exception` as a failure, but we treat + # an exceptioned build as unsettled (infra, likely auto-retried) and wait + # for the retry rather than declaring the parent failed. + if any(t.failed and t.result not in _UNSETTLED_RESULTS for t in label_tasks): return "failed" if any( t.state in _UNSETTLED_STATES or t.result in _UNSETTLED_RESULTS From 24ffa4fe440e35c824fff647ebeb6def5c5138b1 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 16 Jul 2026 14:21:16 -0700 Subject: [PATCH 07/10] Remove comment --- services/hackbot-pulse-listener/app/consumer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/hackbot-pulse-listener/app/consumer.py b/services/hackbot-pulse-listener/app/consumer.py index 3e8e85d93d..7a53471438 100644 --- a/services/hackbot-pulse-listener/app/consumer.py +++ b/services/hackbot-pulse-listener/app/consumer.py @@ -142,8 +142,7 @@ def on_message(body, message): def _build_queues(user: str) -> list[Queue]: # Both local and prod authenticate as the same pulse user, so the queue name # must also vary by environment; otherwise both consumers bind to the same - # durable queue and steal each other's messages. Production keeps the plain - # name for continuity. + # durable queue and steal each other's messages. env = settings.environment env_segment = "" if env == "production" else f"{env}-" queues = [] From fcc5c1b10c4bc95c6e10cd8e6e51b4e171990979 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 16 Jul 2026 14:22:35 -0700 Subject: [PATCH 08/10] Pull max depth from mozci --- services/hackbot-pulse-listener/app/regression.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py index da99558782..d893f3b64d 100644 --- a/services/hackbot-pulse-listener/app/regression.py +++ b/services/hackbot-pulse-listener/app/regression.py @@ -2,13 +2,10 @@ import time from mozci.errors import ParentPushNotFound -from mozci.push import Push +from mozci.push import MAX_DEPTH, Push logger = logging.getLogger(__name__) -# Maximum number of ancestor pushes to walk back over coalescing gaps before -# giving up. Mirrors mozci's own MAX_DEPTH. -MAX_DEPTH = 20 # When the nearest ancestor that ran the build has not produced a decisive # result yet, wait for it in-process instead of racing ahead and misreporting an From 8b58d723d9405baa439def97a9ebf1a1cdee1b08 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 16 Jul 2026 14:44:32 -0700 Subject: [PATCH 09/10] Handle pending case --- .../hackbot-pulse-listener/app/regression.py | 14 +++-- .../tests/test_regression.py | 62 ++++++++++++++++--- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/services/hackbot-pulse-listener/app/regression.py b/services/hackbot-pulse-listener/app/regression.py index d893f3b64d..6445a912e1 100644 --- a/services/hackbot-pulse-listener/app/regression.py +++ b/services/hackbot-pulse-listener/app/regression.py @@ -51,16 +51,20 @@ def _build_status(push: Push, label: str): if label_tasks: if any(t.result in _PASSED_RESULTS for t in label_tasks): return "passed" - # mozci's Task.failed also counts `exception` as a failure, but we treat - # an exceptioned build as unsettled (infra, likely auto-retried) and wait - # for the retry rather than declaring the parent failed. - if any(t.failed and t.result not in _UNSETTLED_RESULTS for t in label_tasks): - return "failed" + # Checked before failure: a retrigger that is still running or was + # exceptioned and auto-retried may yet turn green, and any green run wins, + # so we wait for it rather than prematurely inheriting a failure. if any( t.state in _UNSETTLED_STATES or t.result in _UNSETTLED_RESULTS for t in label_tasks ): return _PENDING + # Not "not t.failed": a run can be neither passed nor failed (canceled, + # superseded, ...). mozci's Task.failed also counts `exception`, but those + # are unsettled and already returned above, so only genuine build failures + # reach here. + if any(t.failed for t in label_tasks): + return "failed" # Ran but reached a non-decisive terminal state (canceled, ...): skip. return None diff --git a/services/hackbot-pulse-listener/tests/test_regression.py b/services/hackbot-pulse-listener/tests/test_regression.py index 75c81d9450..520fcd37a9 100644 --- a/services/hackbot-pulse-listener/tests/test_regression.py +++ b/services/hackbot-pulse-listener/tests/test_regression.py @@ -12,6 +12,10 @@ def __init__(self, label=LABEL, state="completed", result="passed"): self.state = state self.result = result + @property + def failed(self): + return self.result in ("busted", "failed", "exception") + class FakePush: def __init__(self, rev, tasks, parent=None): @@ -51,10 +55,24 @@ def _chain(*task_lists): def _run(observed): - with patch.object(regression, "Push", return_value=observed): + with ( + patch.object(regression, "Push", return_value=observed), + patch.object(regression.time, "sleep"), + ): return regression.is_new_build_failure("autoland", observed.rev, LABEL) +def _run_polling(observed_sequence): + """Feed a fresh observed-push chain on each poll, simulating builds settling.""" + with ( + patch.object(regression, "Push", side_effect=observed_sequence), + patch.object(regression.time, "sleep"), + ): + return regression.is_new_build_failure( + "autoland", observed_sequence[0].rev, LABEL + ) + + def test_parent_passed_is_new_failure(): assert _run(_chain(_failed(), _passed())) is True @@ -79,12 +97,37 @@ def test_coalesced_parent_then_failed_grandparent_is_inherited(): assert _run(_chain(_failed(), [], _failed())) is False -def test_infra_only_parent_is_skipped_then_green(): - assert _run(_chain(_failed(), _infra(), _passed())) is True +def test_infra_parent_is_waited_then_new_failure(): + # An exceptioned parent build is polled, not skipped; once its retry lands + # green the observed push introduced the failure. + assert ( + _run_polling([_chain(_failed(), _infra()), _chain(_failed(), _passed())]) + is True + ) + + +def test_running_parent_is_waited_then_inherited(): + # A still-running parent is polled until it settles; a failure there means + # the observed push inherited it. + assert ( + _run_polling([_chain(_failed(), _running()), _chain(_failed(), _failed())]) + is False + ) -def test_running_parent_is_skipped_then_failed(): - assert _run(_chain(_failed(), _running(), _failed())) is False +def test_unsettled_parent_past_deadline_runs_agent(): + # An ancestor build that never settles fails open once the deadline passes. + observed = _chain(_failed(), _running()) + with ( + patch.object(regression, "Push", return_value=observed), + patch.object(regression.time, "sleep"), + patch.object( + regression.time, + "monotonic", + side_effect=[0.0, regression.MAX_WAIT_SECONDS + 1], + ), + ): + assert regression.is_new_build_failure("autoland", observed.rev, LABEL) is True def test_no_parent_runs_agent(): @@ -106,11 +149,12 @@ def test_other_label_on_parent_ignored(): assert _run(_chain(_failed(), parent_tasks)) is True -def test_completed_exception_parent_is_not_treated_as_failed(): - # A completed task with an infra result must not suppress a real regression: - # it is non-decisive, so we walk past it to the green grandparent. +def test_exception_parent_is_waited_not_failed(): + # A completed task with an infra `exception` result is unsettled, not a + # failure: we wait for the retry rather than declaring the parent failed. exception_parent = [FakeTask(state="completed", result="exception")] - assert _run(_chain(_failed(), exception_parent, _passed())) is True + settled = _chain(_failed(), _passed()) + assert _run_polling([_chain(_failed(), exception_parent), settled]) is True def test_treeherder_result_vocabulary(): From f815d9d0df8911cbdbeb71390f0b26816f655256 Mon Sep 17 00:00:00 2001 From: Evgeny Pavlov Date: Thu, 16 Jul 2026 14:49:29 -0700 Subject: [PATCH 10/10] Relock deps --- uv.lock | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/uv.lock b/uv.lock index 34b26c363c..8c3466b240 100644 --- a/uv.lock +++ b/uv.lock @@ -725,12 +725,12 @@ requires-dist = [ { name = "shap", extras = ["plots"], specifier = ">=0.51,<0.53" }, { name = "spacy", marker = "extra == 'nlp'", specifier = "==3.8.14" }, { name = "tabulate", specifier = "~=0.10.0" }, - { name = "taskcluster", specifier = ">=97.1,<100.4" }, + { name = "taskcluster", specifier = ">=97.1,<100.6" }, { name = "tenacity", specifier = "~=9.1.4" }, { name = "tqdm", specifier = ">=4.67.3,<4.69.0" }, { name = "unidiff", specifier = "~=0.7.5" }, { name = "weave", specifier = ">=0.50.0" }, - { name = "xgboost", specifier = "~=3.2.0" }, + { name = "xgboost", specifier = ">=3.2,<3.4" }, { name = "zstandard", specifier = "~=0.25.0" }, ] provides-extras = ["nlp", "nn"] @@ -740,7 +740,7 @@ spawn-pipeline = [ { name = "json-e", specifier = "==4.8.2" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "requests", specifier = "==2.34.2" }, - { name = "taskcluster", specifier = "==100.3.0" }, + { name = "taskcluster", specifier = "==100.5.0" }, ] test = [ { name = "coverage", specifier = "==7.14.1" }, @@ -1507,6 +1507,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + [[package]] name = "diskcache-weave" version = "5.6.3.post1" @@ -1981,6 +1990,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] +[[package]] +name = "glean-parser" +version = "17.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "diskcache" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/c3/24387435f0bf3def52922a70c3f5aab81c82ab6120ac4db719109f0c8b23/glean_parser-17.3.0.tar.gz", hash = "sha256:f70fb4496436068f81ef786029a1b369f61ae2f9327eeb2fa50335dba59ee214", size = 125596, upload-time = "2025-07-09T09:03:02.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/31/8176e4e77144bae2493d9da708c15da01e403befbf3c9490db001a64da53/glean_parser-17.3.0-py3-none-any.whl", hash = "sha256:84d975d40333e97d9f6e0ce0f22db4e1e3d8e088b2da678e34cc7ab3361f071d", size = 115439, upload-time = "2025-07-09T09:03:00.784Z" }, +] + +[[package]] +name = "glean-sdk" +version = "65.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "glean-parser" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/8a/a45f4ee0a98901cd474204587ab6ecf18f141803a11d3c091c1f035fbfed/glean_sdk-65.2.3.tar.gz", hash = "sha256:e8d17d1851b688b242c9de51c9125b41cd87a30a788a2a26be6cdf00e628d1be", size = 260945, upload-time = "2025-11-06T11:15:08.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/43/fabb546efe0925ba11c6a1d776507f75c267cfa8401729391e14e2fc3fc6/glean_sdk-65.2.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fdb39d22e2a6c095ec8990b71b2eb833cbde4a807e8032a4ae218a2983eb23bd", size = 1686649, upload-time = "2025-11-06T11:12:57.204Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/cd8d6b2f31e96c4fd4e3fe54ad3f3c2ed51bbba18b2a5ff4adeb94970af3/glean_sdk-65.2.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:263d1a91f1048261013f11a78073d5e1b9d17f29065f9ef7e296dc995f2f846a", size = 898508, upload-time = "2025-11-06T11:12:59.447Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ce/76677fa87e58abeb4b016b1870fe1b6a9b616ea857099dc6a95f504e8b99/glean_sdk-65.2.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:38b90604ae9f55fc522e67b47989ed547ccd35d7dbb871b5f78caffedc9cbae0", size = 866137, upload-time = "2025-11-06T11:13:00.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7e/51f1c15418f7e173bde4d73c06ea153e8447fc10ec05d62fb052098b08cd/glean_sdk-65.2.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182eaa29eba7096c751353eac8faec1916ea6b81445ea688874b732d45727b7a", size = 821146, upload-time = "2025-11-06T11:14:59.985Z" }, + { url = "https://files.pythonhosted.org/packages/93/65/ed4c48130e3423de9386011796452a81ba552429a6b6f0d6ec14e5aaa14b/glean_sdk-65.2.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8346bd5938218906ad572ab82c5fd4468481dda6a2ba41e5fbe67c9383bbcf9", size = 934818, upload-time = "2025-11-06T11:14:31.725Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/ee4285df4a19726be7941bad99daa6d1ef3c581fb1ca54f2fdd68d3ddc2d/glean_sdk-65.2.3-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:45d61656d4eead892cdc2cac7d8b2ba1185a9520a0ca50e8326e8a6c2566519e", size = 934533, upload-time = "2025-11-06T11:15:06.777Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/78b6e7d156aa0267b4d65dfc67746254a039d485f6943cce86b2091f9443/glean_sdk-65.2.3-py3-none-win_amd64.whl", hash = "sha256:9e603dc7fca2b6cd739a3a31195d4d564db542de5571d7afbd33a7ca8fb8fdde", size = 878415, upload-time = "2025-11-06T11:14:06.474Z" }, + { url = "https://files.pythonhosted.org/packages/37/a2/f8bf156045be612d9dbf44c8c63e889050f94da3205ecdb4a836b79948bb/glean_sdk-65.2.3-py3-none-win_arm64.whl", hash = "sha256:70920c20bbf393da807933056e85575598078f8124c44c68257df195b5d8d5f4", size = 741441, upload-time = "2025-11-06T11:15:20.785Z" }, +] + [[package]] name = "google-api-core" version = "2.30.3" @@ -2034,6 +2080,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] +[[package]] +name = "google-cloud-pubsub" +version = "2.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "grpcio-status" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/2b/4bf2c17e319ff65340389565b0e1b4d72696d87802b2f5f94390fbefa73c/google_cloud_pubsub-2.39.0.tar.gz", hash = "sha256:eed65e25f57f95bf3e02d96d7ee171688b23922471f9f21b5a91ed90e1282c0f", size = 402096, upload-time = "2026-06-03T15:28:26.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/20/dd0b27d4ad4577c062e77ff968ca3e2d404186cd78c8a2a53a0ef5fe5389/google_cloud_pubsub-2.39.0-py3-none-any.whl", hash = "sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19", size = 324665, upload-time = "2026-06-03T15:27:41.119Z" }, +] + [[package]] name = "google-cloud-run" version = "0.16.0" @@ -2440,7 +2506,7 @@ dependencies = [ { name = "agent-tools", extra = ["bugzilla", "firefox"] }, { name = "bugsy" }, { name = "claude-agent-sdk" }, - { name = "hackbot-runtime", extra = ["claude-sdk"] }, + { name = "hackbot-runtime", extra = ["claude-sdk", "phabricator"] }, { name = "mcp" }, { name = "starlette" }, { name = "uvicorn" }, @@ -2451,7 +2517,7 @@ requires-dist = [ { name = "agent-tools", extras = ["bugzilla", "firefox"], editable = "libs/agent-tools" }, { name = "bugsy" }, { name = "claude-agent-sdk", specifier = ">=0.1.30" }, - { name = "hackbot-runtime", extras = ["claude-sdk"], editable = "libs/hackbot-runtime" }, + { name = "hackbot-runtime", extras = ["claude-sdk", "phabricator"], editable = "libs/hackbot-runtime" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "starlette", specifier = ">=0.36.0" }, { name = "uvicorn", specifier = ">=0.27.0" }, @@ -2550,8 +2616,11 @@ dependencies = [ { name = "asyncpg" }, { name = "cloud-sql-python-connector", extra = ["asyncpg"] }, { name = "fastapi" }, + { name = "google-auth" }, + { name = "google-cloud-pubsub" }, { name = "google-cloud-run" }, { name = "google-cloud-storage" }, + { name = "hackbot-runtime" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "sentry-sdk" }, @@ -2572,8 +2641,11 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.29.0" }, { name = "cloud-sql-python-connector", extras = ["asyncpg"], specifier = ">=1.5.0" }, { name = "fastapi", specifier = ">=0.109.0" }, + { name = "google-auth", specifier = ">=2.29.0" }, + { name = "google-cloud-pubsub", specifier = ">=2.21.0" }, { name = "google-cloud-run", specifier = ">=0.10.0" }, { name = "google-cloud-storage", specifier = ">=2.16.0" }, + { name = "hackbot-runtime", editable = "libs/hackbot-runtime" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" }, { name = "pydantic", specifier = ">=2.6.0" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, @@ -2618,7 +2690,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "sendgrid", specifier = ">=6.12.5" }, { name = "sentry-sdk", specifier = ">=2.51.0" }, - { name = "taskcluster", specifier = ">=97.1,<100.4" }, + { name = "taskcluster", specifier = ">=97.1,<100.6" }, { name = "zstandard", specifier = "~=0.25.0" }, ] provides-extras = ["dev"] @@ -2632,6 +2704,7 @@ dependencies = [ { name = "google-auth" }, { name = "pydantic-settings" }, { name = "requests" }, + { name = "weave" }, ] [package.optional-dependencies] @@ -2639,6 +2712,9 @@ claude-sdk = [ { name = "agent-tools", extra = ["claude-sdk"] }, { name = "claude-agent-sdk" }, ] +phabricator = [ + { name = "mozphab" }, +] [package.metadata] requires-dist = [ @@ -2646,10 +2722,12 @@ requires-dist = [ { name = "agent-tools", extras = ["claude-sdk"], marker = "extra == 'claude-sdk'", editable = "libs/agent-tools" }, { name = "claude-agent-sdk", marker = "extra == 'claude-sdk'", specifier = ">=0.1.30" }, { name = "google-auth", specifier = ">=2.0.0" }, + { name = "mozphab", marker = "extra == 'phabricator'", specifier = "==2.15.3" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, { name = "requests", specifier = ">=2.32.0" }, + { name = "weave", specifier = ">=0.53.1" }, ] -provides-extras = ["claude-sdk"] +provides-extras = ["claude-sdk", "phabricator"] [[package]] name = "hf-xet" @@ -4224,6 +4302,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/2d/5cd9787f930f2a0cf4ab3d105ce15edc39733328e7606d97124877b70c4e/mozInstall-2.1.0-py2.py3-none-any.whl", hash = "sha256:8abc26e37b1976eb3d815ee2a42bb6bbb10926e16620eabb6f8f1ed764b0c0fe", size = 6687, upload-time = "2023-07-28T13:43:23.953Z" }, ] +[[package]] +name = "mozphab" +version = "2.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "distro" }, + { name = "glean-sdk" }, + { name = "packaging" }, + { name = "python-hglib" }, + { name = "sentry-sdk" }, + { name = "setuptools" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/f8/af97c0db47ef85ea801d782fcc6a3f6cbf2ee7c06ef75946fdf40ed28e3c/mozphab-2.15.3.tar.gz", hash = "sha256:2eb9e3fb4b936acc8d340b49739905b4d8f2f5d7450e519cfdd2cebf13bb8226", size = 278633, upload-time = "2026-06-25T16:31:57.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/32/512ae30c4ef7fa2c606a24fc1f9a331a215988f4819328233d427f3269de/mozphab-2.15.3-py3-none-any.whl", hash = "sha256:3265eb9ddd03e08c44c8ea784912790f60a8ec0a1f104fdc64eea410f4dff23e", size = 124401, upload-time = "2026-06-25T16:31:55.998Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -6450,6 +6547,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + [[package]] name = "sendgrid" version = "6.12.5" @@ -6842,7 +6948,7 @@ wheels = [ [[package]] name = "taskcluster" -version = "100.3.0" +version = "100.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -6853,9 +6959,9 @@ dependencies = [ { name = "slugid" }, { name = "taskcluster-urls" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/89/b809400a31d06b8a323289db0ad747118d923c77edcedc22cbb208eeca9c/taskcluster-100.3.0.tar.gz", hash = "sha256:72c1d1a27be49f70732711f16d7555669399d5fd5ab4c3f6cc3ba63fb62c79dc", size = 254833, upload-time = "2026-06-09T10:42:04.689Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/c5/823ca59868de80f05f6992482dfb1b0106bd106b9eb410e1c3dd2990f7a3/taskcluster-100.5.0.tar.gz", hash = "sha256:5b2dc2624e7e1a1ca8352b63cf9c4734e9edfab4452a4b3adaa45ce09b19f444", size = 254859, upload-time = "2026-06-25T08:25:48.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/02/11c44cc059c8edd87a788b5734265b8964eb56042a975085d3c115fc0279/taskcluster-100.3.0-py3-none-any.whl", hash = "sha256:127893d98e5b81ad937480fe7ae5d8b97f3c3b84b6fec09eb5ab1bac8e01d9d1", size = 150897, upload-time = "2026-06-09T10:42:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e6/459aeffc43c6d7266d34388ec167c34ccf02dab20970365ab499602134d2/taskcluster-100.5.0-py3-none-any.whl", hash = "sha256:b56b385da136024cb0f7326a545156bc0d9827194930636bd2d4a9dde2e7d59f", size = 150887, upload-time = "2026-06-25T08:25:47.427Z" }, ] [[package]] @@ -7463,7 +7569,7 @@ wheels = [ [[package]] name = "weave" -version = "0.52.43" +version = "0.53.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -7474,7 +7580,6 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "polyfile-weave" }, { name = "pydantic" }, @@ -7482,9 +7587,9 @@ dependencies = [ { name = "tenacity" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/df/dcb32bf366eca5e11d345a8e15238256396a9ff7346a29b3290a358b9cd8/weave-0.52.43.tar.gz", hash = "sha256:3c4abcb4da33e0db8a03bd52739503d99c90bdb1922b704ca2b2df8a0f6d561e", size = 977397, upload-time = "2026-06-15T21:29:23.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/34/c9ddb250e27708b581f52fd75f94ca04edbe7ed287560fe13995e042521d/weave-0.53.1.tar.gz", hash = "sha256:623c1918394e8493a3344fb89ffc71b3e1e96c3bd247a77309aa2b6e78475cb9", size = 1113042, upload-time = "2026-07-02T21:17:45.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/a0/e823f714e337c6342a831307d21d2a12c87bc6a6015fc0ac62be9dd8e01f/weave-0.52.43-py3-none-any.whl", hash = "sha256:d7fb10e893c9da57c7663c8c51a1a67eb59216f9da0e4056a244a41de63faf19", size = 1199217, upload-time = "2026-06-15T21:29:21.267Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/99d77ddb9f1008a92e36e6d72b0ff63e17fbe22abe6b383bbfb80001d70b/weave-0.53.1-py3-none-any.whl", hash = "sha256:95c45e22fe38cf241f4ff26d120c86bcff3652e4c813e0bcf5950ec26e7f5db8", size = 1349841, upload-time = "2026-07-02T21:17:42.969Z" }, ] [[package]]