Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions libs/hackbot-runtime/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]

Expand All @@ -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"]
Expand Down
6 changes: 0 additions & 6 deletions libs/hackbot-runtime/tests/test_testrail_action.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@
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

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}"
Comment on lines +36 to +37

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we adding this?


# 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
Expand All @@ -56,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
Expand Down Expand Up @@ -127,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"),
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
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

# Actions that submit source changes to Phabricator.
PATCH_ACTION_TYPES = frozenset({"phabricator.submit_patch", "phabricator.update_patch"})
Comment on lines +20 to +21

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a duplicate now? If so, we should file a follow-up issue and link it here, so we could fix that later.

If we have other cases, we could use the same issue to track all of them.


# 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@

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

log = logging.getLogger(__name__)

_HACKBOT_UI_URL = "https://hackbot.moz.tools"

_TIMEOUT_SECONDS = 30


Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
12 changes: 6 additions & 6 deletions services/hackbot-api/app/actions_applier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion services/hackbot-api/app/routers/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down
10 changes: 8 additions & 2 deletions services/hackbot-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ 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]
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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
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.registry import PATCH_ACTION_TYPES, get_handler


@pytest.fixture(autouse=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for the apply-side TestRail action handler."""

from hackbot_runtime.actions.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():
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,29 @@
"""

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.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."""
return subprocess.run(
["git", "-C", str(repo), *args],
check=True,
capture_output=True,
text=True,
).stdout

Comment on lines +25 to +33

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here? The hackbot-api service does not have a git repo!


_SUBMISSION = {
"base_commit": "a" * 40,
"base_commit_vcs": "git",
Expand Down
Loading