Skip to content

feat: Display setup.sh command and output in UI #9318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions frontend/src/i18n/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ export enum I18nKey {
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
STATUS$SETTING_UP_WORKSPACE = "STATUS$SETTING_UP_WORKSPACE",
STATUS$SETTING_UP_GIT_HOOKS = "STATUS$SETTING_UP_GIT_HOOKS",
STATUS$SETUP_SCRIPT_SUCCESS = "STATUS$SETUP_SCRIPT_SUCCESS",
STATUS$SETUP_SCRIPT_FAILED = "STATUS$SETUP_SCRIPT_FAILED",
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
ACCOUNT_SETTINGS_MODAL$SAVE = "ACCOUNT_SETTINGS_MODAL$SAVE",
ACCOUNT_SETTINGS_MODAL$CLOSE = "ACCOUNT_SETTINGS_MODAL$CLOSE",
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6015,6 +6015,38 @@
"ja": "git フックを設定中...",
"uk": "Налаштування git-хуків..."
},
"STATUS$SETUP_SCRIPT_SUCCESS": {
"en": "Setup script completed successfully.",
"zh-CN": "设置脚本执行成功。",
"zh-TW": "設置腳本執行成功。",
"de": "Setup-Skript erfolgreich abgeschlossen.",
"ko-KR": "설정 스크립트가 성공적으로 완료되었습니다.",
"no": "Oppsettskript fullført vellykket.",
"it": "Script di configurazione completato con successo.",
"pt": "Script de configuração concluído com sucesso.",
"es": "Script de configuración completado exitosamente.",
"ar": "تم إكمال سكريبت الإعداد بنجاح.",
"fr": "Script de configuration terminé avec succès.",
"tr": "Kurulum betiği başarıyla tamamlandı.",
"ja": "セットアップスクリプトが正常に完了しました。",
"uk": "Скрипт налаштування успішно завершено."
},
"STATUS$SETUP_SCRIPT_FAILED": {
"en": "Setup script execution failed. Check the terminal output for details.",
"zh-CN": "设置脚本执行失败。请检查终端输出以获取详细信息。",
"zh-TW": "設置腳本執行失敗。請檢查終端輸出以獲取詳細資訊。",
"de": "Ausführung des Setup-Skripts fehlgeschlagen. Überprüfen Sie die Terminal-Ausgabe für Details.",
"ko-KR": "설정 스크립트 실행이 실패했습니다. 자세한 내용은 터미널 출력을 확인하세요.",
"no": "Utføring av oppsettskript mislyktes. Sjekk terminalutdata for detaljer.",
"it": "Esecuzione dello script di configurazione fallita. Controlla l'output del terminale per i dettagli.",
"pt": "Execução do script de configuração falhou. Verifique a saída do terminal para detalhes.",
"es": "La ejecución del script de configuración falló. Revisa la salida del terminal para más detalles.",
"ar": "فشل تنفيذ سكريبت الإعداد. تحقق من مخرجات الطرفية للحصول على التفاصيل.",
"fr": "L'exécution du script de configuration a échoué. Vérifiez la sortie du terminal pour plus de détails.",
"tr": "Kurulum betiği yürütme başarısız oldu. Ayrıntılar için terminal çıktısını kontrol edin.",
"ja": "セットアップスクリプトの実行に失敗しました。詳細については、ターミナル出力を確認してください。",
"uk": "Виконання скрипта налаштування не вдалося. Перевірте вивід терміналу для отримання деталей."
},
"ACCOUNT_SETTINGS_MODAL$DISCONNECT": {
"en": "Disconnect",
"es": "Desconectar",
Expand Down
28 changes: 27 additions & 1 deletion openhands/runtime/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,38 @@ def maybe_run_setup_script(self):

# setup scripts time out after 10 minutes
action = CmdRunAction(
f'chmod +x {setup_script} && source {setup_script}', blocking=True
f'chmod +x {setup_script} && source {setup_script}',
blocking=True,
thought='Running setup script to configure the workspace environment.',
)
action.set_hard_timeout(600)

# Add the action to the event stream so it's visible in the UI
if self.event_stream:
self.event_stream.add_event(action, EventSource.ENVIRONMENT)

obs = self.run_action(action)

# Add the observation to the event stream so the result is visible in the UI
if self.event_stream:
self.event_stream.add_event(obs, EventSource.ENVIRONMENT)

if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
self.log('error', f'Setup script failed: {obs.content}')
if self.status_callback:
self.status_callback(
'error',
'STATUS$SETUP_SCRIPT_FAILED',
'Setup script execution failed. Check the terminal output for details.',
)
else:
self.log('info', 'Setup script completed successfully')
if self.status_callback:
self.status_callback(
'info',
'STATUS$SETUP_SCRIPT_SUCCESS',
'Setup script completed successfully.',
)

@property
def workspace_root(self) -> Path:
Expand Down
161 changes: 157 additions & 4 deletions tests/runtime/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
)

from openhands.core.setup import initialize_repository_for_runtime
from openhands.events.action import FileReadAction, FileWriteAction
from openhands.events.observation import FileReadObservation, FileWriteObservation
from openhands.events.action import CmdRunAction, FileReadAction, FileWriteAction
from openhands.events.event import EventSource
from openhands.events.observation import (
CmdOutputObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.integrations.service_types import ProviderType, Repository


Expand Down Expand Up @@ -38,10 +43,19 @@ def test_maybe_run_setup_script(temp_dir, runtime_cls, run_as_openhands):
"""Test that setup script is executed when it exists."""
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)

# Create an empty README.md file first to ensure we start with a clean state
write_readme = runtime.write(
FileWriteAction(
path='README.md',
content='',
)
)
assert isinstance(write_readme, FileWriteObservation)

setup_script = '.openhands/setup.sh'
write_obs = runtime.write(
FileWriteAction(
path=setup_script, content="#!/bin/bash\necho 'Hello World' >> README.md\n"
path=setup_script, content="#!/bin/bash\necho 'Hello World' > README.md\n"
)
)
assert isinstance(write_obs, FileWriteObservation)
Expand All @@ -66,11 +80,20 @@ def test_maybe_run_setup_script_with_long_timeout(
runtime_startup_env_vars={'NO_CHANGE_TIMEOUT_SECONDS': '1'},
)

# Create an empty README.md file first to ensure we start with a clean state
write_readme = runtime.write(
FileWriteAction(
path='README.md',
content='',
)
)
assert isinstance(write_readme, FileWriteObservation)

setup_script = '.openhands/setup.sh'
write_obs = runtime.write(
FileWriteAction(
path=setup_script,
content="#!/bin/bash\nsleep 3 && echo 'Hello World' >> README.md\n",
content="#!/bin/bash\nsleep 3 && echo 'Hello World' > README.md\n",
)
)
assert isinstance(write_obs, FileWriteObservation)
Expand All @@ -82,3 +105,133 @@ def test_maybe_run_setup_script_with_long_timeout(
read_obs = runtime.read(FileReadAction(path='README.md'))
assert isinstance(read_obs, FileReadObservation)
assert read_obs.content == 'Hello World\n'


def test_setup_script_events_added_to_stream(temp_dir, runtime_cls, run_as_openhands):
"""Test that setup script command and output are added to the event stream for UI visibility."""
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)

setup_script = '.openhands/setup.sh'
write_obs = runtime.write(
FileWriteAction(
path=setup_script,
content="#!/bin/bash\necho 'Setup completed successfully'\n",
)
)
assert isinstance(write_obs, FileWriteObservation)

# Get initial events
initial_events = list(runtime.event_stream.search_events())
initial_event_count = len(initial_events)

# Run setup script
runtime.maybe_run_setup_script()

# Get all events after running setup script
all_events = list(runtime.event_stream.search_events())
new_events = all_events[initial_event_count:]

# Should have at least 2 new events: the action and the observation
assert len(new_events) >= 2

# Find the setup command action
setup_action = None
setup_observation = None

for event in new_events:
if (
isinstance(event, CmdRunAction)
and 'chmod +x .openhands/setup.sh && source .openhands/setup.sh'
in event.command
):
setup_action = event
elif (
isinstance(event, CmdOutputObservation)
and hasattr(event, '_cause')
and setup_action
and event._cause == setup_action.id
):
setup_observation = event

# Verify the setup action was added to the event stream
assert setup_action is not None, (
'Setup command action should be added to event stream'
)
assert (
setup_action.command
== 'chmod +x .openhands/setup.sh && source .openhands/setup.sh'
)
assert (
setup_action.thought
== 'Running setup script to configure the workspace environment.'
)
assert setup_action.source == EventSource.ENVIRONMENT

# Verify the setup observation was added to the event stream
assert setup_observation is not None, (
'Setup command observation should be added to event stream'
)
assert setup_observation.source == EventSource.ENVIRONMENT
assert 'Setup completed successfully' in setup_observation.content


def test_setup_script_failure_events_added_to_stream(
temp_dir, runtime_cls, run_as_openhands
):
"""Test that setup script failure is properly shown in the event stream."""
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)

setup_script = '.openhands/setup.sh'
write_obs = runtime.write(
FileWriteAction(
path=setup_script, content="#!/bin/bash\necho 'Setup failed' && exit 1\n"
)
)
assert isinstance(write_obs, FileWriteObservation)

# Get initial events
initial_events = list(runtime.event_stream.search_events())
initial_event_count = len(initial_events)

# Run setup script
runtime.maybe_run_setup_script()

# Get all events after running setup script
all_events = list(runtime.event_stream.search_events())
new_events = all_events[initial_event_count:]

# Should have at least 2 new events: the action and the observation
assert len(new_events) >= 2

# Find the setup command action and observation
setup_action = None
setup_observation = None

for event in new_events:
if (
isinstance(event, CmdRunAction)
and 'chmod +x .openhands/setup.sh && source .openhands/setup.sh'
in event.command
):
setup_action = event
elif (
isinstance(event, CmdOutputObservation)
and hasattr(event, '_cause')
and setup_action
and event._cause == setup_action.id
):
setup_observation = event

# Verify the setup action was added to the event stream
assert setup_action is not None, (
'Setup command action should be added to event stream'
)
assert setup_action.source == EventSource.ENVIRONMENT

# Verify the setup observation was added to the event stream and shows failure
assert setup_observation is not None, (
'Setup command observation should be added to event stream'
)
assert setup_observation.source == EventSource.ENVIRONMENT
assert setup_observation.exit_code != 0, 'Setup script should have failed'
assert 'Setup failed' in setup_observation.content
28 changes: 28 additions & 0 deletions tests/runtime/test_setup_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from openhands.events.action import CmdRunAction
from openhands.events.event import EventSource
from openhands.events.observation import CmdOutputObservation


def test_setup_script_events_source_property():
"""Test that setup script events use the source property correctly."""
# Create a mock action with the ENVIRONMENT source
action = CmdRunAction(
command='chmod +x .openhands/setup.sh && source .openhands/setup.sh',
thought='Running setup script to configure the workspace environment.',
)
action._source = EventSource.ENVIRONMENT

# Verify the source property works correctly
assert action.source == EventSource.ENVIRONMENT

# Create a mock observation with the ENVIRONMENT source
observation = CmdOutputObservation(
command='chmod +x .openhands/setup.sh && source .openhands/setup.sh',
content='Setup completed successfully',
exit_code=0,
)
observation._source = EventSource.ENVIRONMENT
observation._cause = 1 # Mock ID of the action

# Verify the source property works correctly
assert observation.source == EventSource.ENVIRONMENT
Loading