Skip to content

Commit 3f2d764

Browse files
committed
feat(debug): add --attach flag for non-interactive debug runs
1 parent 27646a4 commit 3f2d764

5 files changed

Lines changed: 113 additions & 13 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.53"
3+
version = "2.10.54"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath-core>=0.5.8, <0.6.0",
9-
"uipath-runtime>=0.10.0, <0.11.0",
9+
"uipath-runtime>=0.10.1, <0.11.0",
1010
"uipath-platform>=0.1.13, <0.2.0",
1111
"click>=8.3.1",
1212
"httpx>=0.28.1",

packages/uipath/src/uipath/_cli/_debug/_bridge.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
UiPathRuntimeResult,
2020
UiPathRuntimeStatus,
2121
)
22-
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugQuitError
22+
from uipath.runtime.debug import (
23+
DetachedDebugBridge,
24+
UiPathDebugProtocol,
25+
UiPathDebugQuitError,
26+
)
2327
from uipath.runtime.events import UiPathRuntimeStateEvent, UiPathRuntimeStatePhase
2428

29+
DebugAttachMode = Literal["signalr", "console", "none"]
30+
2531
logger = logging.getLogger(__name__)
2632

2733

@@ -871,18 +877,30 @@ def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugProtoco
871877

872878

873879
def get_debug_bridge(
874-
context: UiPathRuntimeContext, verbose: bool = True
880+
context: UiPathRuntimeContext,
881+
verbose: bool = True,
882+
attach: DebugAttachMode | None = None,
875883
) -> UiPathDebugProtocol:
876884
"""Factory to get appropriate debug bridge based on context.
877885
878886
Args:
879887
context: The runtime context containing debug configuration.
880888
verbose: If True, console bridge shows all state updates. If False, only breakpoints.
889+
attach: Explicit attach mode. ``"none"`` returns a `DetachedDebugBridge`
890+
(no handshake, no blocking wait) — for callers that run under
891+
``uipath debug`` but cannot speak the SignalR debug protocol.
892+
``"signalr"`` / ``"console"`` force that bridge. ``None`` falls
893+
back to the legacy selection based on ``context.job_id``.
881894
882895
Returns:
883896
An instance of UiPathDebugBridge suitable for the context.
884897
"""
885-
if context.job_id:
898+
if attach == "none":
899+
return DetachedDebugBridge()
900+
if attach == "signalr":
886901
return get_remote_debug_bridge(context)
887-
else:
902+
if attach == "console":
888903
return ConsoleDebugBridge(verbose=verbose)
904+
if context.job_id:
905+
return get_remote_debug_bridge(context)
906+
return ConsoleDebugBridge(verbose=verbose)

packages/uipath/src/uipath/_cli/cli_debug.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import asyncio
22
import logging
3+
from typing import cast, get_args
34

45
import click
56

67
from uipath._cli._chat._bridge import get_chat_bridge
7-
from uipath._cli._debug._bridge import get_debug_bridge
8+
from uipath._cli._debug._bridge import DebugAttachMode, get_debug_bridge
89
from uipath._cli._utils._debug import setup_debugging
910
from uipath._cli._utils._studio_project import StudioClient
1011
from uipath.core.tracing import UiPathTraceManager
@@ -64,6 +65,15 @@
6465
default=5678,
6566
help="Port for the debug server (default: 5678)",
6667
)
68+
@click.option(
69+
"--attach",
70+
type=click.Choice(list(get_args(DebugAttachMode)), case_sensitive=False),
71+
default=None,
72+
help=(
73+
"Debugger attach mode. Defaults to 'signalr' for cloud runs, "
74+
"'console' for local runs."
75+
),
76+
)
6777
@track_command("debug")
6878
def debug(
6979
entrypoint: str | None,
@@ -74,13 +84,18 @@ def debug(
7484
output_file: str | None,
7585
debug: bool,
7686
debug_port: int,
87+
attach: str | None,
7788
) -> None:
7889
"""Debug the project."""
7990
input_file = file or input_file
8091
# Setup debugging if requested
8192
if not setup_debugging(debug, debug_port):
8293
console.error(f"Failed to start debug server on port {debug_port}")
8394

95+
attach_mode: DebugAttachMode | None = (
96+
cast(DebugAttachMode, attach.lower()) if attach else None
97+
)
98+
8499
result = Middlewares.next(
85100
"debug",
86101
entrypoint,
@@ -90,6 +105,7 @@ def debug(
90105
output_file=output_file,
91106
debug=debug,
92107
debug_port=debug_port,
108+
attach=attach_mode,
93109
)
94110

95111
if result.error_message:
@@ -141,7 +157,9 @@ async def execute_debug_runtime():
141157

142158
async def execute_debug_runtime():
143159
chat_runtime: UiPathRuntimeProtocol | None = None
144-
debug_bridge: UiPathDebugProtocol = get_debug_bridge(ctx)
160+
debug_bridge: UiPathDebugProtocol = get_debug_bridge(
161+
ctx, attach=attach_mode
162+
)
145163

146164
runtime = await factory.new_runtime(
147165
entrypoint,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for `get_debug_bridge()` selection matrix.
2+
3+
Locks in the non-breaking-change contract: absence of `attach` preserves the
4+
legacy `job_id`-based selection. Explicit `attach` overrides that selection.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import pytest
10+
11+
from uipath._cli._debug._bridge import (
12+
ConsoleDebugBridge,
13+
SignalRDebugBridge,
14+
get_debug_bridge,
15+
)
16+
from uipath.runtime import UiPathRuntimeContext
17+
from uipath.runtime.debug import DetachedDebugBridge
18+
19+
20+
def _ctx(**overrides) -> UiPathRuntimeContext:
21+
return UiPathRuntimeContext(**overrides)
22+
23+
24+
def test_attach_none_returns_detached_bridge_without_job_id():
25+
bridge = get_debug_bridge(_ctx(), attach="none")
26+
assert isinstance(bridge, DetachedDebugBridge)
27+
28+
29+
def test_attach_none_returns_detached_bridge_even_when_job_id_set(monkeypatch):
30+
"""'none' wins over job_id — this is the whole point of the flag."""
31+
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
32+
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="none")
33+
assert isinstance(bridge, DetachedDebugBridge)
34+
35+
36+
def test_attach_signalr_forces_signalr_bridge(monkeypatch):
37+
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
38+
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="signalr")
39+
assert isinstance(bridge, SignalRDebugBridge)
40+
41+
42+
def test_attach_console_forces_console_bridge_even_when_job_id_set(monkeypatch):
43+
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
44+
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="console")
45+
assert isinstance(bridge, ConsoleDebugBridge)
46+
47+
48+
def test_legacy_selection_signalr_when_job_id_set_and_no_attach(monkeypatch):
49+
"""Non-breaking change assertion: absence of `attach` preserves today's behavior."""
50+
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
51+
bridge = get_debug_bridge(_ctx(job_id="job-123"))
52+
assert isinstance(bridge, SignalRDebugBridge)
53+
54+
55+
def test_legacy_selection_console_when_no_job_id_and_no_attach():
56+
"""Non-breaking change assertion: absence of `attach` preserves today's behavior."""
57+
bridge = get_debug_bridge(_ctx())
58+
assert isinstance(bridge, ConsoleDebugBridge)
59+
60+
61+
def test_attach_signalr_without_job_id_raises():
62+
"""Explicit signalr without job_id is a user error — surface it loudly."""
63+
with pytest.raises(ValueError, match="UIPATH_URL and UIPATH_JOB_KEY"):
64+
get_debug_bridge(_ctx(), attach="signalr")

packages/uipath/uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)