From 10471ef7fc30839176a104618a2e56dfa1d691a3 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Mon, 7 Sep 2026 20:19:04 +0200 Subject: [PATCH 1/5] Move action handlers to hackbot-api --- .../app/action_handlers}/__init__.py | 6 +++--- .../hackbot-api/app/action_handlers}/base.py | 0 .../app/action_handlers}/bugzilla_handler.py | 2 +- .../app/action_handlers/contract.py | 16 ++++++++++++++++ .../app/action_handlers}/email_handler.py | 5 ++--- .../action_handlers}/phabricator_handler.py | 2 +- .../app/action_handlers}/registry.py | 16 ++++++++-------- .../app/action_handlers}/slack_handler.py | 4 ++-- .../app/action_handlers}/testrail_handler.py | 2 +- .../action_handlers}/try_server_handler.py | 2 +- .../tests/test_bugzilla_handler.py | 2 +- .../hackbot-api}/tests/test_email_handler.py | 6 +++--- .../tests/test_phabricator_handler.py | 6 +++--- .../hackbot-api}/tests/test_slack_handler.py | 2 +- .../tests/test_testrail_handler.py | 2 +- .../tests/test_try_server_handler.py | 19 +++++++++++++++---- 16 files changed, 59 insertions(+), 33 deletions(-) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/__init__.py (62%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/base.py (100%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/bugzilla_handler.py (99%) create mode 100644 services/hackbot-api/app/action_handlers/contract.py rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/email_handler.py (96%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/phabricator_handler.py (99%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/registry.py (65%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/slack_handler.py (94%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/testrail_handler.py (99%) rename {libs/hackbot-runtime/hackbot_runtime/actions/handlers => services/hackbot-api/app/action_handlers}/try_server_handler.py (99%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_bugzilla_handler.py (99%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_email_handler.py (97%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_phabricator_handler.py (98%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_slack_handler.py (97%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_testrail_handler.py (99%) rename {libs/hackbot-runtime => services/hackbot-api}/tests/test_try_server_handler.py (96%) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/__init__.py b/services/hackbot-api/app/action_handlers/__init__.py similarity index 62% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/__init__.py rename to services/hackbot-api/app/action_handlers/__init__.py index fb4dacffb7..d35b089527 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/__init__.py +++ b/services/hackbot-api/app/action_handlers/__init__.py @@ -1,15 +1,15 @@ """Apply-side handlers for recorded actions.""" -from hackbot_runtime.actions.handlers.base import ( +from app.action_handlers.base import ( ActionHandler, ActionResult, ApplyContext, ) -from hackbot_runtime.actions.handlers.bugzilla_handler import ( +from app.action_handlers.bugzilla_handler import ( merge_resolved, plan_coalesced_groups, ) -from hackbot_runtime.actions.handlers.registry import HANDLERS, get_handler +from app.action_handlers.registry import HANDLERS, get_handler __all__ = [ "ActionHandler", diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/base.py b/services/hackbot-api/app/action_handlers/base.py similarity index 100% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/base.py rename to services/hackbot-api/app/action_handlers/base.py diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/bugzilla_handler.py b/services/hackbot-api/app/action_handlers/bugzilla_handler.py similarity index 99% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/bugzilla_handler.py rename to services/hackbot-api/app/action_handlers/bugzilla_handler.py index 1cd875730f..ab79148c64 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/bugzilla_handler.py +++ b/services/hackbot-api/app/action_handlers/bugzilla_handler.py @@ -17,7 +17,7 @@ import requests -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext +from app.action_handlers.base import ActionResult, ApplyContext log = logging.getLogger(__name__) diff --git a/services/hackbot-api/app/action_handlers/contract.py b/services/hackbot-api/app/action_handlers/contract.py new file mode 100644 index 0000000000..465b45b541 --- /dev/null +++ b/services/hackbot-api/app/action_handlers/contract.py @@ -0,0 +1,16 @@ +"""Values used when applying recorded Hackbot actions.""" + +# Artifact containing a run's source-code patch. +PATCH_ARTIFACT = "changes/changes.patch" + +# Email-body token replaced with the source-code patch. +PATCH_PLACEHOLDER = "{patch}" + +# Public URL of the Hackbot UI. +HACKBOT_UI_URL = "https://hackbot.moz.tools" + +# Actions that submit source changes to Phabricator. +PATCH_ACTION_TYPES = frozenset({"phabricator.submit_patch", "phabricator.update_patch"}) + +# Actions that submit source changes to the Try server. +TRY_ACTION_TYPES = frozenset({"try_server.push"}) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/email_handler.py b/services/hackbot-api/app/action_handlers/email_handler.py similarity index 96% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/email_handler.py rename to services/hackbot-api/app/action_handlers/email_handler.py index 098f7c4c00..0a3217ea90 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/email_handler.py +++ b/services/hackbot-api/app/action_handlers/email_handler.py @@ -26,9 +26,8 @@ import os from typing import Any -from hackbot_runtime.actions.email import PATCH_PLACEHOLDER -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext -from hackbot_runtime.changes import PATCH_ARTIFACT +from app.action_handlers.base import ActionResult, ApplyContext +from app.action_handlers.contract import PATCH_ARTIFACT, PATCH_PLACEHOLDER log = logging.getLogger(__name__) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/phabricator_handler.py b/services/hackbot-api/app/action_handlers/phabricator_handler.py similarity index 99% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/phabricator_handler.py rename to services/hackbot-api/app/action_handlers/phabricator_handler.py index e67110e4b8..872acc6281 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/phabricator_handler.py +++ b/services/hackbot-api/app/action_handlers/phabricator_handler.py @@ -27,7 +27,7 @@ from async_lru import alru_cache from phabricator_client import PhabricatorClient -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext +from app.action_handlers.base import ActionResult, ApplyContext log = logging.getLogger(__name__) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/registry.py b/services/hackbot-api/app/action_handlers/registry.py similarity index 65% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/registry.py rename to services/hackbot-api/app/action_handlers/registry.py index 70e4e2c2b3..93d5313d9b 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/registry.py +++ b/services/hackbot-api/app/action_handlers/registry.py @@ -1,21 +1,21 @@ -from hackbot_runtime.actions.handlers.base import ActionHandler -from hackbot_runtime.actions.handlers.bugzilla_handler import ( +from app.action_handlers.base import ActionHandler +from app.action_handlers.bugzilla_handler import ( AddAttachmentHandler, AddCommentHandler, CreateBugHandler, UpdateBugHandler, ) -from hackbot_runtime.actions.handlers.email_handler import SendEmailHandler -from hackbot_runtime.actions.handlers.phabricator_handler import ( +from app.action_handlers.email_handler import SendEmailHandler +from app.action_handlers.phabricator_handler import ( AddCommentHandler as PhabricatorAddCommentHandler, ) -from hackbot_runtime.actions.handlers.phabricator_handler import ( +from app.action_handlers.phabricator_handler import ( SubmitPatchHandler, UpdatePatchHandler, ) -from hackbot_runtime.actions.handlers.slack_handler import PostMessageHandler -from hackbot_runtime.actions.handlers.testrail_handler import SubmitTestPlanHandler -from hackbot_runtime.actions.handlers.try_server_handler import PushHandler +from app.action_handlers.slack_handler import PostMessageHandler +from app.action_handlers.testrail_handler import SubmitTestPlanHandler +from app.action_handlers.try_server_handler import PushHandler # Maps a recorded action's dotted `type` to the handler that applies it. # Adding a new action type later is a one-line addition here — the dispatch diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/slack_handler.py b/services/hackbot-api/app/action_handlers/slack_handler.py similarity index 94% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/slack_handler.py rename to services/hackbot-api/app/action_handlers/slack_handler.py index 04807595c6..5b8db74a86 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/slack_handler.py +++ b/services/hackbot-api/app/action_handlers/slack_handler.py @@ -13,8 +13,8 @@ from slack_sdk import WebClient -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext -from hackbot_runtime.actions.slack import HACKBOT_UI_URL +from app.action_handlers.base import ActionResult, ApplyContext +from app.action_handlers.contract import HACKBOT_UI_URL log = logging.getLogger(__name__) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/testrail_handler.py b/services/hackbot-api/app/action_handlers/testrail_handler.py similarity index 99% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/testrail_handler.py rename to services/hackbot-api/app/action_handlers/testrail_handler.py index bbdd361ce5..2245ead899 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/testrail_handler.py +++ b/services/hackbot-api/app/action_handlers/testrail_handler.py @@ -9,7 +9,7 @@ from testrail_client import TestRailClient -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext +from app.action_handlers.base import ActionResult, ApplyContext log = logging.getLogger(__name__) diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/try_server_handler.py b/services/hackbot-api/app/action_handlers/try_server_handler.py similarity index 99% rename from libs/hackbot-runtime/hackbot_runtime/actions/handlers/try_server_handler.py rename to services/hackbot-api/app/action_handlers/try_server_handler.py index 92fff1de47..ebfe5d30d3 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/handlers/try_server_handler.py +++ b/services/hackbot-api/app/action_handlers/try_server_handler.py @@ -11,7 +11,7 @@ from lando_client import LandoClient, encode_patch -from hackbot_runtime.actions.handlers.base import ActionResult, ApplyContext +from app.action_handlers.base import ActionResult, ApplyContext log = logging.getLogger(__name__) diff --git a/libs/hackbot-runtime/tests/test_bugzilla_handler.py b/services/hackbot-api/tests/test_bugzilla_handler.py similarity index 99% rename from libs/hackbot-runtime/tests/test_bugzilla_handler.py rename to services/hackbot-api/tests/test_bugzilla_handler.py index cb8ad2dd85..de9f753653 100644 --- a/libs/hackbot-runtime/tests/test_bugzilla_handler.py +++ b/services/hackbot-api/tests/test_bugzilla_handler.py @@ -7,7 +7,7 @@ import base64 -from hackbot_runtime.actions.handlers import ApplyContext, bugzilla_handler +from app.action_handlers import ApplyContext, bugzilla_handler def _ctx(attachments=None, artifacts=None, agent="test-agent"): diff --git a/libs/hackbot-runtime/tests/test_email_handler.py b/services/hackbot-api/tests/test_email_handler.py similarity index 97% rename from libs/hackbot-runtime/tests/test_email_handler.py rename to services/hackbot-api/tests/test_email_handler.py index 459bf9d1bf..429b54ddb0 100644 --- a/libs/hackbot-runtime/tests/test_email_handler.py +++ b/services/hackbot-api/tests/test_email_handler.py @@ -8,7 +8,7 @@ import json import pytest -from hackbot_runtime.actions.handlers import email_handler +from app.action_handlers import email_handler def _ctx(artifacts=None): @@ -17,7 +17,7 @@ async def download(key): raise FileNotFoundError(key) return artifacts[key] - from hackbot_runtime.actions.handlers import ApplyContext + from app.action_handlers import ApplyContext return ApplyContext( run_id="run-1", agent="build-repair", download_artifact=download @@ -121,7 +121,7 @@ async def download(key): reads.append(key) return b"+fix\n" - from hackbot_runtime.actions.handlers import ApplyContext + from app.action_handlers import ApplyContext ctx = ApplyContext(run_id="r", agent="a", download_artifact=download) await email_handler.SendEmailHandler().apply( diff --git a/libs/hackbot-runtime/tests/test_phabricator_handler.py b/services/hackbot-api/tests/test_phabricator_handler.py similarity index 98% rename from libs/hackbot-runtime/tests/test_phabricator_handler.py rename to services/hackbot-api/tests/test_phabricator_handler.py index 416f3b2295..5318489185 100644 --- a/libs/hackbot-runtime/tests/test_phabricator_handler.py +++ b/services/hackbot-api/tests/test_phabricator_handler.py @@ -11,9 +11,9 @@ from unittest.mock import AsyncMock import pytest -from hackbot_runtime.actions.handlers import ApplyContext, phabricator_handler -from hackbot_runtime.actions.handlers.registry import get_handler -from hackbot_runtime.actions.phabricator import PATCH_ACTION_TYPES +from app.action_handlers import ApplyContext, phabricator_handler +from app.action_handlers.contract import PATCH_ACTION_TYPES +from app.action_handlers.registry import get_handler @pytest.fixture(autouse=True) diff --git a/libs/hackbot-runtime/tests/test_slack_handler.py b/services/hackbot-api/tests/test_slack_handler.py similarity index 97% rename from libs/hackbot-runtime/tests/test_slack_handler.py rename to services/hackbot-api/tests/test_slack_handler.py index 4da8fa3e28..24a87d2ba9 100644 --- a/libs/hackbot-runtime/tests/test_slack_handler.py +++ b/services/hackbot-api/tests/test_slack_handler.py @@ -5,7 +5,7 @@ """ import pytest -from hackbot_runtime.actions.handlers import ApplyContext, slack_handler +from app.action_handlers import ApplyContext, slack_handler from slack_sdk.errors import SlackApiError diff --git a/libs/hackbot-runtime/tests/test_testrail_handler.py b/services/hackbot-api/tests/test_testrail_handler.py similarity index 99% rename from libs/hackbot-runtime/tests/test_testrail_handler.py rename to services/hackbot-api/tests/test_testrail_handler.py index c7be486867..b69c212f96 100644 --- a/libs/hackbot-runtime/tests/test_testrail_handler.py +++ b/services/hackbot-api/tests/test_testrail_handler.py @@ -1,6 +1,6 @@ """Tests for the apply-side TestRail action handler.""" -from hackbot_runtime.actions.handlers import ApplyContext, testrail_handler +from app.action_handlers import ApplyContext, testrail_handler def _ctx(): diff --git a/libs/hackbot-runtime/tests/test_try_server_handler.py b/services/hackbot-api/tests/test_try_server_handler.py similarity index 96% rename from libs/hackbot-runtime/tests/test_try_server_handler.py rename to services/hackbot-api/tests/test_try_server_handler.py index 517414c647..cb5a1b55c1 100644 --- a/libs/hackbot-runtime/tests/test_try_server_handler.py +++ b/services/hackbot-api/tests/test_try_server_handler.py @@ -9,16 +9,27 @@ """ import json +import subprocess from base64 import b64decode from datetime import datetime, timezone import pytest -from hackbot_runtime.actions.handlers import ApplyContext, try_server_handler -from hackbot_runtime.actions.handlers.registry import get_handler -from hackbot_runtime.actions.try_server import TRY_ACTION_TYPES -from hackbot_runtime.changes import _git +from app.action_handlers import ApplyContext, try_server_handler +from app.action_handlers.contract import TRY_ACTION_TYPES +from app.action_handlers.registry import get_handler from lando_client import LandoClient + +def _git(repo, *args: str) -> str: + """Run a git command in ``repo`` and return its stdout.""" + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + capture_output=True, + text=True, + ).stdout + + _SUBMISSION = { "base_commit": "a" * 40, "base_commit_vcs": "git", From a9672184109cd0c5b5ff2d8b5a635cd78bcd4b7c Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Mon, 7 Sep 2026 20:25:42 +0200 Subject: [PATCH 2/5] Move TestRail handler registration test to hackbot-api --- libs/hackbot-runtime/tests/test_testrail_action.py | 6 ------ services/hackbot-api/tests/test_testrail_handler.py | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/hackbot-runtime/tests/test_testrail_action.py b/libs/hackbot-runtime/tests/test_testrail_action.py index 205bfaa803..c5af4dc60f 100644 --- a/libs/hackbot-runtime/tests/test_testrail_action.py +++ b/libs/hackbot-runtime/tests/test_testrail_action.py @@ -1,8 +1,6 @@ import pytest from agent_tools.registry import ToolError from hackbot_runtime.actions import ActionsRecorder, testrail -from hackbot_runtime.actions.handlers import get_handler -from hackbot_runtime.actions.handlers.testrail_handler import SubmitTestPlanHandler from hackbot_runtime.actions.testrail import ACTION_TYPE @@ -215,7 +213,3 @@ async def test_submit_test_plan_tool_rejects_not_run_results(): assert "invalid TestRail submission" in str(exc.value) assert recorder.actions == [] - - -def test_submit_test_plan_handler_is_registered(): - assert isinstance(get_handler(ACTION_TYPE), SubmitTestPlanHandler) diff --git a/services/hackbot-api/tests/test_testrail_handler.py b/services/hackbot-api/tests/test_testrail_handler.py index b69c212f96..a7ae0a1f91 100644 --- a/services/hackbot-api/tests/test_testrail_handler.py +++ b/services/hackbot-api/tests/test_testrail_handler.py @@ -1,6 +1,7 @@ """Tests for the apply-side TestRail action handler.""" -from app.action_handlers import ApplyContext, testrail_handler +from app.action_handlers import ApplyContext, get_handler, testrail_handler +from app.action_handlers.testrail_handler import SubmitTestPlanHandler def _ctx(): @@ -340,3 +341,7 @@ async def test_resolve_template_id_requires_steps_template(): assert str(exc) == 'TestRail has no template named "Test Case (Steps)"' else: raise AssertionError("missing Test Case (Steps) template did not fail") + + +def test_submit_test_plan_handler_is_registered(): + assert isinstance(get_handler("testrail.submit_test_plan"), SubmitTestPlanHandler) From fa8c68ecfe88eb2da587e99a85e71c380cb7ac5f Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Mon, 7 Sep 2026 20:30:51 +0200 Subject: [PATCH 3/5] Use moved action handlers in hackbot-api --- services/hackbot-api/app/actions_applier.py | 12 ++++++------ services/hackbot-api/app/routers/runs.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/hackbot-api/app/actions_applier.py b/services/hackbot-api/app/actions_applier.py index bdb95ca17a..b150082bb9 100644 --- a/services/hackbot-api/app/actions_applier.py +++ b/services/hackbot-api/app/actions_applier.py @@ -5,7 +5,7 @@ manageable in the UI. Whether they're then applied *automatically* is decided by `_auto_apply_blocker` (see `app/agents.py`); either way they can be applied on demand (manual apply-all from the UI). Application runs each pending -row through the handler registry in `hackbot_runtime.actions.handlers` and is +row through the handler registry in `app.action_handlers` and is idempotent per action — an already-`applied` row is never re-applied, so Pub/Sub retries and repeated manual applies are safe. """ @@ -17,17 +17,17 @@ from datetime import datetime, timezone from typing import Any -from hackbot_runtime.actions.handlers import ( +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app import gcs +from app.action_handlers import ( ActionResult, ApplyContext, get_handler, merge_resolved, plan_coalesced_groups, ) -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app import gcs from app.agents import AGENT_REGISTRY, AgentSpec from app.database.models import Run, RunAction from app.schemas import RunStatus diff --git a/services/hackbot-api/app/routers/runs.py b/services/hackbot-api/app/routers/runs.py index 50350959e6..a35a0e48f1 100644 --- a/services/hackbot-api/app/routers/runs.py +++ b/services/hackbot-api/app/routers/runs.py @@ -5,12 +5,12 @@ from typing import Annotated from fastapi import APIRouter, Depends, Header, HTTPException, Query, status -from hackbot_runtime.actions.phabricator import PATCH_ACTION_TYPES from pydantic import BeforeValidator from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app import gcs, jobs, pubsub +from app.action_handlers.contract import PATCH_ACTION_TYPES from app.actions_applier import apply_all_pending from app.agents import AGENT_REGISTRY, AgentSpec, model_to_env from app.auth import require_api_key From e039535cc2bb7f471a730d514ecb4dab3d5610d6 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Mon, 7 Sep 2026 20:31:48 +0200 Subject: [PATCH 4/5] Move action handler dependencies to hackbot-api --- libs/hackbot-runtime/pyproject.toml | 10 ---------- services/hackbot-api/pyproject.toml | 10 ++++++++-- uv.lock | 28 ++++++++++++---------------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/libs/hackbot-runtime/pyproject.toml b/libs/hackbot-runtime/pyproject.toml index bfd797f86a..bf048bda89 100644 --- a/libs/hackbot-runtime/pyproject.toml +++ b/libs/hackbot-runtime/pyproject.toml @@ -8,14 +8,7 @@ dependencies = [ "httpx>=0.26.0", "pydantic-settings>=2.1.0", "google-auth>=2.0.0", - "async-lru>=2.0.0", "agent-tools", - "lando-client", - "phabricator-client", - "testrail-client", - "slack-sdk>=3.27.0", - "sendgrid>=6.12.5", - "markdown2>=2.4.0", "weave>=0.53.4" ] @@ -28,9 +21,6 @@ phabricator = ["MozPhab==2.15.3"] [tool.uv.sources] agent-tools = { workspace = true } -lando-client = { workspace = true } -phabricator-client = { workspace = true } -testrail-client = { workspace = true } [build-system] requires = ["hatchling"] diff --git a/services/hackbot-api/pyproject.toml b/services/hackbot-api/pyproject.toml index 9558779e0b..1c88aaaaa7 100644 --- a/services/hackbot-api/pyproject.toml +++ b/services/hackbot-api/pyproject.toml @@ -18,11 +18,16 @@ dependencies = [ "google-auth>=2.29.0", "sentry-sdk>=2.51.0", "cachetools>=5.3.0", + "requests>=2.32.0", + "async-lru>=2.0.0", "slack-sdk>=3.27.0", + "sendgrid>=6.12.5", + "markdown2>=2.4.0", "python-multipart>=0.0.9", "hackbot-client", - "hackbot-runtime", + "lando-client", "phabricator-client", + "testrail-client", ] [project.optional-dependencies] @@ -30,8 +35,9 @@ dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0", "httpx>=0.26.0"] [tool.uv.sources] hackbot-client = { workspace = true } -hackbot-runtime = { workspace = true } +lando-client = { workspace = true } phabricator-client = { workspace = true } +testrail-client = { workspace = true } [build-system] requires = ["hatchling"] diff --git a/uv.lock b/uv.lock index c4b7e1f0a7..b0a97e13e8 100644 --- a/uv.lock +++ b/uv.lock @@ -2707,6 +2707,7 @@ version = "0.1.0" source = { editable = "services/hackbot-api" } dependencies = [ { name = "alembic" }, + { name = "async-lru" }, { name = "asyncpg" }, { name = "cachetools" }, { name = "cloud-sql-python-connector", extra = ["asyncpg"] }, @@ -2716,14 +2717,18 @@ dependencies = [ { name = "google-cloud-run" }, { name = "google-cloud-storage" }, { name = "hackbot-client" }, - { name = "hackbot-runtime" }, + { name = "lando-client" }, + { name = "markdown2" }, { name = "phabricator-client" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, + { name = "requests" }, + { name = "sendgrid" }, { name = "sentry-sdk" }, { name = "slack-sdk" }, { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "testrail-client" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -2737,6 +2742,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.13.1" }, + { name = "async-lru", specifier = ">=2.0.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "cachetools", specifier = ">=5.3.0" }, { name = "cloud-sql-python-connector", extras = ["asyncpg"], specifier = ">=1.5.0" }, @@ -2746,17 +2752,21 @@ requires-dist = [ { name = "google-cloud-run", specifier = ">=0.10.0" }, { name = "google-cloud-storage", specifier = ">=2.16.0" }, { name = "hackbot-client", editable = "libs/hackbot-client" }, - { name = "hackbot-runtime", editable = "libs/hackbot-runtime" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" }, + { name = "lando-client", editable = "libs/lando-client" }, + { name = "markdown2", specifier = ">=2.4.0" }, { name = "phabricator-client", editable = "libs/phabricator-client" }, { name = "pydantic", specifier = ">=2.6.0" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "requests", specifier = ">=2.32.0" }, + { name = "sendgrid", specifier = ">=6.12.5" }, { name = "sentry-sdk", specifier = ">=2.51.0" }, { name = "slack-sdk", specifier = ">=3.27.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.25" }, + { name = "testrail-client", editable = "libs/testrail-client" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" }, ] provides-extras = ["dev"] @@ -2827,17 +2837,10 @@ version = "0.1.0" source = { editable = "libs/hackbot-runtime" } dependencies = [ { name = "agent-tools" }, - { name = "async-lru" }, { name = "google-auth" }, { name = "httpx" }, - { name = "lando-client" }, - { name = "markdown2" }, - { name = "phabricator-client" }, { name = "pydantic-settings" }, { name = "requests" }, - { name = "sendgrid" }, - { name = "slack-sdk" }, - { name = "testrail-client" }, { name = "weave" }, ] @@ -2854,19 +2857,12 @@ phabricator = [ requires-dist = [ { name = "agent-tools", editable = "libs/agent-tools" }, { name = "agent-tools", extras = ["claude-sdk"], marker = "extra == 'claude-sdk'", editable = "libs/agent-tools" }, - { name = "async-lru", specifier = ">=2.0.0" }, { name = "claude-agent-sdk", marker = "extra == 'claude-sdk'", specifier = ">=0.1.30" }, { name = "google-auth", specifier = ">=2.0.0" }, { name = "httpx", specifier = ">=0.26.0" }, - { name = "lando-client", editable = "libs/lando-client" }, - { name = "markdown2", specifier = ">=2.4.0" }, { name = "mozphab", marker = "extra == 'phabricator'", specifier = "==2.15.3" }, - { name = "phabricator-client", editable = "libs/phabricator-client" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, { name = "requests", specifier = ">=2.32.0" }, - { name = "sendgrid", specifier = ">=6.12.5" }, - { name = "slack-sdk", specifier = ">=3.27.0" }, - { name = "testrail-client", editable = "libs/testrail-client" }, { name = "weave", specifier = ">=0.53.4" }, ] provides-extras = ["claude-sdk", "phabricator"] From 735703ef3441643aad0463d4c311690dadb4c836 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Mon, 7 Sep 2026 22:07:45 +0200 Subject: [PATCH 5/5] Keep single-use action handler constants local --- .../hackbot-api/app/action_handlers/contract.py | 16 ---------------- .../app/action_handlers/email_handler.py | 11 ++++++++--- .../hackbot-api/app/action_handlers/registry.py | 3 +++ .../app/action_handlers/slack_handler.py | 5 +++-- services/hackbot-api/app/routers/runs.py | 2 +- .../tests/test_phabricator_handler.py | 3 +-- .../hackbot-api/tests/test_try_server_handler.py | 4 +++- 7 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 services/hackbot-api/app/action_handlers/contract.py diff --git a/services/hackbot-api/app/action_handlers/contract.py b/services/hackbot-api/app/action_handlers/contract.py deleted file mode 100644 index 465b45b541..0000000000 --- a/services/hackbot-api/app/action_handlers/contract.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Values used when applying recorded Hackbot actions.""" - -# Artifact containing a run's source-code patch. -PATCH_ARTIFACT = "changes/changes.patch" - -# Email-body token replaced with the source-code patch. -PATCH_PLACEHOLDER = "{patch}" - -# Public URL of the Hackbot UI. -HACKBOT_UI_URL = "https://hackbot.moz.tools" - -# Actions that submit source changes to Phabricator. -PATCH_ACTION_TYPES = frozenset({"phabricator.submit_patch", "phabricator.update_patch"}) - -# Actions that submit source changes to the Try server. -TRY_ACTION_TYPES = frozenset({"try_server.push"}) diff --git a/services/hackbot-api/app/action_handlers/email_handler.py b/services/hackbot-api/app/action_handlers/email_handler.py index 0a3217ea90..696a88e7ea 100644 --- a/services/hackbot-api/app/action_handlers/email_handler.py +++ b/services/hackbot-api/app/action_handlers/email_handler.py @@ -27,10 +27,15 @@ from typing import Any from app.action_handlers.base import ActionResult, ApplyContext -from app.action_handlers.contract import PATCH_ARTIFACT, PATCH_PLACEHOLDER log = logging.getLogger(__name__) +# Artifact containing a run's source-code patch. +_PATCH_ARTIFACT = "changes/changes.patch" + +# Email-body token replaced with the source-code patch. +PATCH_PLACEHOLDER = "{patch}" + # A diff long enough to bury the rest of the mail is cut off; the attachment, # when the caller asked for one, still carries every line. _MAX_PATCH_LINES = 400 @@ -55,7 +60,7 @@ async def _patch(ctx: ApplyContext) -> bytes | None: recipient the patch, not the whole notification. """ try: - return await ctx.download_artifact(PATCH_ARTIFACT) + return await ctx.download_artifact(_PATCH_ARTIFACT) except Exception: log.exception("Could not read the patch of run %s", ctx.run_id) return None @@ -126,7 +131,7 @@ async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult message.add_attachment( Attachment( FileContent(base64.b64encode(patch).decode()), - FileName(PATCH_ARTIFACT.rsplit("/", 1)[-1]), + FileName(_PATCH_ARTIFACT.rsplit("/", 1)[-1]), disposition=Disposition("attachment"), ) ) diff --git a/services/hackbot-api/app/action_handlers/registry.py b/services/hackbot-api/app/action_handlers/registry.py index 93d5313d9b..20169569a8 100644 --- a/services/hackbot-api/app/action_handlers/registry.py +++ b/services/hackbot-api/app/action_handlers/registry.py @@ -17,6 +17,9 @@ from app.action_handlers.testrail_handler import SubmitTestPlanHandler from app.action_handlers.try_server_handler import PushHandler +# Actions that submit source changes to Phabricator. +PATCH_ACTION_TYPES = frozenset({"phabricator.submit_patch", "phabricator.update_patch"}) + # Maps a recorded action's dotted `type` to the handler that applies it. # Adding a new action type later is a one-line addition here — the dispatch # loop (see the apply-run-actions route) never changes. diff --git a/services/hackbot-api/app/action_handlers/slack_handler.py b/services/hackbot-api/app/action_handlers/slack_handler.py index 5b8db74a86..a0ba1c6b7c 100644 --- a/services/hackbot-api/app/action_handlers/slack_handler.py +++ b/services/hackbot-api/app/action_handlers/slack_handler.py @@ -14,10 +14,11 @@ from slack_sdk import WebClient from app.action_handlers.base import ActionResult, ApplyContext -from app.action_handlers.contract import HACKBOT_UI_URL log = logging.getLogger(__name__) +_HACKBOT_UI_URL = "https://hackbot.moz.tools" + _TIMEOUT_SECONDS = 30 @@ -40,7 +41,7 @@ async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult "notification_type": "info", "source": { "ref_id": ctx.run_id, - "ref_url": f"{HACKBOT_UI_URL}/runs/{ctx.run_id}", + "ref_url": f"{_HACKBOT_UI_URL}/runs/{ctx.run_id}", }, "context": { "agent": ctx.agent, diff --git a/services/hackbot-api/app/routers/runs.py b/services/hackbot-api/app/routers/runs.py index a35a0e48f1..b9547972c8 100644 --- a/services/hackbot-api/app/routers/runs.py +++ b/services/hackbot-api/app/routers/runs.py @@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app import gcs, jobs, pubsub -from app.action_handlers.contract import PATCH_ACTION_TYPES +from app.action_handlers.registry import PATCH_ACTION_TYPES from app.actions_applier import apply_all_pending from app.agents import AGENT_REGISTRY, AgentSpec, model_to_env from app.auth import require_api_key diff --git a/services/hackbot-api/tests/test_phabricator_handler.py b/services/hackbot-api/tests/test_phabricator_handler.py index 5318489185..d1970203bd 100644 --- a/services/hackbot-api/tests/test_phabricator_handler.py +++ b/services/hackbot-api/tests/test_phabricator_handler.py @@ -12,8 +12,7 @@ import pytest from app.action_handlers import ApplyContext, phabricator_handler -from app.action_handlers.contract import PATCH_ACTION_TYPES -from app.action_handlers.registry import get_handler +from app.action_handlers.registry import PATCH_ACTION_TYPES, get_handler @pytest.fixture(autouse=True) diff --git a/services/hackbot-api/tests/test_try_server_handler.py b/services/hackbot-api/tests/test_try_server_handler.py index cb5a1b55c1..cb38681940 100644 --- a/services/hackbot-api/tests/test_try_server_handler.py +++ b/services/hackbot-api/tests/test_try_server_handler.py @@ -15,10 +15,12 @@ import pytest from app.action_handlers import ApplyContext, try_server_handler -from app.action_handlers.contract import TRY_ACTION_TYPES from app.action_handlers.registry import get_handler from lando_client import LandoClient +# Actions that submit source changes to the Try server. +TRY_ACTION_TYPES = frozenset({"try_server.push"}) + def _git(repo, *args: str) -> str: """Run a git command in ``repo`` and return its stdout."""