Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9daf6bd
Update chart script for replicated agent-server images
aivong-openhands May 9, 2026
4e6d01d
feat: read runtime image tag from sandbox spec service file
aivong-openhands May 10, 2026
b4efd04
test: strengthen sandbox spec error message assertion
aivong-openhands May 10, 2026
3554389
feat: add --skip-version-check flag to force image tag updates
aivong-openhands May 11, 2026
b8c664e
fix: update runtime image patterns from runtime to agent-server
aivong-openhands May 11, 2026
67ff07b
PLTF-2045: improve test quality for update_openhands_charts
aivong-openhands May 11, 2026
5d2455e
fix: use correct agent-server image tag format (X.Y.Z-python)
aivong-openhands May 11, 2026
91cc0c6
test: move get_deploy_config mock response helpers to conftest
aivong-openhands May 11, 2026
8c7dc2e
test: pass count property names explicitly in parametrize
aivong-openhands May 11, 2026
b0f3675
test: split deploy_config success test into type and value checks
aivong-openhands May 11, 2026
b1ab18f
refactor: unify error_if_missing handling in update_tag_in_content
aivong-openhands May 11, 2026
9512f63
test: compare list counts to pre-update values instead of literals
aivong-openhands May 11, 2026
b002063
test: reuse mock_main_early_exit fixture in TestSkipVersionCheck
aivong-openhands May 11, 2026
852a574
test: extract stub fixtures for resolve_openhands_version and process…
aivong-openhands May 11, 2026
6af855e
test: parametrize main() output message format check
aivong-openhands May 11, 2026
2321166
refactor: use shared make_workflow_response factory in TestGetDeployC…
aivong-openhands May 11, 2026
9514e07
test: drop dedicated test classes for conftest assertion helpers
aivong-openhands May 11, 2026
479059c
test: parametrize ref-format check for cloud_tag_exists
aivong-openhands May 11, 2026
8e6e0b9
test: parametrize is_unchanged assertions in idempotency tests
aivong-openhands May 11, 2026
499f624
test: cover workflow orchestration and tighten existing tests
aivong-openhands May 11, 2026
8196276
ci: add PR check for update_openhands_charts tests
openhands-agent May 11, 2026
1f43e1a
ci: run all scripts tests in PR check
openhands-agent May 11, 2026
dc94f9e
PLTF-2502: upgrade laminar helm chart to address noisy postgres error…
aivong-openhands May 11, 2026
d04735e
Wire up Custom/Local LLM provider in Replicated installer (#626)
ak684 May 13, 2026
696e98b
restrict plugin marketplace source to supported URL schemes (#608)
jlav May 13, 2026
054b2c8
Fix support bundle errors and warnings, add runtime pod telemetry (#628)
jlav May 13, 2026
03bf6cc
PLTF-2504: upgrade laminar helm chart to 0.1.11 (rabbitmq+quickwit ha…
aivong-openhands May 13, 2026
ee1d4b2
bump chart version (#629)
aivong-openhands May 13, 2026
638770f
fix: update replicated/openhands.yaml in chart-bump script
aivong-openhands May 15, 2026
822064e
Merge remote-tracking branch 'origin/main' into av/update-openhands-c…
openhands-agent May 21, 2026
09532bd
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 22, 2026
ff3e522
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 26, 2026
62076d0
Merge branch 'main' into av/update-openhands-charts-for-agent-server-…
aivong-openhands May 26, 2026
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
35 changes: 35 additions & 0 deletions .github/workflows/test-update-openhands-charts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Test scripts

on:
pull_request:
paths:
- 'scripts/**'
- '.github/workflows/test-update-openhands-charts.yml'

jobs:
test-scripts:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Set up uv
uses: astral-sh/setup-uv@v5

- name: Run scripts tests
run: >-
uv run
--with pytest
--with requests
--with PyGithub
--with fastapi
--with uvicorn
--with httpx
--with ruamel.yaml
python -m pytest scripts -q
171 changes: 161 additions & 10 deletions scripts/update_openhands_charts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_chart_update(make_temp_yaml_file, sample_openhands_chart_minimal):
import base64
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, Mock

import pytest
from ruamel.yaml import YAML
Expand Down Expand Up @@ -73,6 +73,9 @@ def test_chart_update(make_temp_yaml_file, sample_openhands_chart_minimal):
# New versions used when testing chart updates
NEW_APP_VERSION = "cloud-2.0.0" # OpenHands appVersion uses cloud-X.Y.Z tags
NEW_RUNTIME_API_VERSION = "0.2.0"
# Runtime image tag constants — agent-server uses X.Y.Z-python format (no cloud- prefix)
RUNTIME_IMAGE_TAG = "1.0.0-python" # Baseline tag matching sample fixtures
NEW_RUNTIME_IMAGE_TAG = "1.1.0-python" # New tag used when testing updates


def get_dependency_version(file_path: Path, dep_name: str) -> str | None:
Expand Down Expand Up @@ -339,8 +342,8 @@ def sample_openhands_values_full():

runtime:
image:
repository: ghcr.io/openhands/runtime
tag: cloud-1.0.0-nikolaik
repository: ghcr.io/openhands/agent-server
tag: 1.0.0-python
runAsRoot: true

runtime-api:
Expand All @@ -351,7 +354,7 @@ def sample_openhands_values_full():
count: 1
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:1.0.0-python"
working_dir: "/openhands/code/"
"""

Expand All @@ -366,15 +369,15 @@ def sample_openhands_values_minimal():

runtime:
image:
repository: ghcr.io/openhands/runtime
tag: cloud-1.0.0-nikolaik
repository: ghcr.io/openhands/agent-server
tag: 1.0.0-python

runtime-api:
enabled: true
warmRuntimes:
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:1.0.0-python"
"""


Expand All @@ -398,12 +401,47 @@ def sample_runtime_api_values():
count: 0
configs:
- name: default
image: "ghcr.io/openhands/runtime:cloud-1.0.0-nikolaik"
image: "ghcr.io/openhands/agent-server:1.0.0-python"
working_dir: "/openhands/code/"
environment: {}
"""


@pytest.fixture
def sample_replicated_openhands_wrapper_values():
"""Sample replicated openhands wrapper YAML with agent-server image references.

The proxy block intentionally carries a commented-out alternate repository
line between repository: and tag: to mirror the real replicated/openhands.yaml,
where that comment documents the non-proxy fallback path.
"""
return """\
spec:
values:
runtime:
image:
# this is what we need to use for real deployments
repository: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/ghcr.io/openhands/agent-server'
# repository: 'ghcr.io/openhands/agent-server'
tag: '1.19.0-python'
warmRuntimes:
configs:
- name: default
image: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/ghcr.io/openhands/agent-server:1.19.0-python'
helmChart:
values:
runtime:
image:
repository: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server'
tag: '1.19.0-python'
warmRuntimes:
configs:
- name: default
image: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server:1.19.0-python'
"""



# =============================================================================
# GitHub API mock fixtures
# =============================================================================
Expand Down Expand Up @@ -460,7 +498,10 @@ def mock_main_early_exit(monkeypatch):
Sets up all mocks needed to run main() in a controlled way where it
exits early (when current appVersion matches latest cloud tag).

Returns a function that accepts a cloud_tag and sets up all necessary mocks.
Returns a function that accepts a cloud_tag and an optional
runtime_image_tag, and sets up all necessary mocks. Pass
runtime_image_tag (default: None) for tests that exercise the
--skip-version-check path past the early-exit guard.

Usage:
def test_something(mock_main_early_exit, capsys):
Expand All @@ -469,7 +510,7 @@ def test_something(mock_main_early_exit, capsys):
captured = capsys.readouterr()
assert "cloud-1.20.0" in captured.out
"""
def _mock_main(cloud_tag: str):
def _mock_main(cloud_tag: str, runtime_image_tag: str | None = None):
# Mock GITHUB_TOKEN environment variable
monkeypatch.setenv("GITHUB_TOKEN", "dummy-token")

Expand All @@ -488,10 +529,72 @@ def _mock_main(cloud_tag: str):
"update_openhands_charts.get_current_app_version",
lambda path: cloud_tag
)
# Mock sandbox spec fetch — required when callers skip the early-exit guard
monkeypatch.setattr(
"update_openhands_charts.get_runtime_image_tag_from_sandbox_spec",
lambda token, repo, ref: runtime_image_tag,
)

return _mock_main


@pytest.fixture
def stub_cloud_tag_exists(monkeypatch):
"""Factory fixture that stubs `update_openhands_charts.cloud_tag_exists` to return a fixed bool."""
def _stub(exists: bool):
monkeypatch.setattr(
"update_openhands_charts.cloud_tag_exists",
lambda token, repo, tag: exists,
)
return _stub


@pytest.fixture
def stub_latest_cloud_tag(monkeypatch):
"""Factory fixture that stubs `update_openhands_charts.get_latest_cloud_tag` to return a fixed value."""
def _stub(tag: str | None):
monkeypatch.setattr(
"update_openhands_charts.get_latest_cloud_tag",
lambda token, repo: tag,
)
return _stub


@pytest.fixture
def stub_process_updates_chain(monkeypatch):
"""Factory fixture for stubbing the call chain inside process_updates().

Defaults give a fully-successful chain up to the deploy-config fetch.
Pass None to any kwarg to simulate that step failing — this triggers the
corresponding early-return guard so tests can verify downstream calls
are skipped.

Usage:
def test_runtime_tag_guard(stub_process_updates_chain):
stub_process_updates_chain(runtime_image_tag=None)
process_updates("token")
# ... assert downstream call was NOT made
"""
def _stub(
openhands_version: str | None = "cloud-1.20.0",
current_app_version: str | None = "cloud-1.19.0",
runtime_image_tag: str | None = "1.20.0-python",
):
monkeypatch.setattr(
"update_openhands_charts.resolve_openhands_version",
lambda token, cloud_tag: openhands_version,
)
monkeypatch.setattr(
"update_openhands_charts.get_current_app_version",
lambda path: current_app_version,
)
monkeypatch.setattr(
"update_openhands_charts.get_runtime_image_tag_from_sandbox_spec",
lambda token, repo, ref: runtime_image_tag,
)
return _stub


@pytest.fixture
def make_workflow_response():
"""Factory fixture for creating mock GitHub API responses with workflow content.
Expand All @@ -517,6 +620,54 @@ def _make_response(yaml_content: str) -> MagicMock:
return _make_response


# =============================================================================
# Mock response helpers for get_deploy_config error path tests
# These plain functions (not fixtures) are used inside @pytest.mark.parametrize
# decorators, which are evaluated at class scope where fixtures cannot be
# injected. They centralize mock-response construction for error scenarios.
# =============================================================================

def make_http_error_response(status_code: int, message: str) -> Mock:
"""Create a mock requests.get that raises an exception on raise_for_status()."""
mock_response = Mock()
mock_response.status_code = status_code
mock_response.raise_for_status.side_effect = Exception(f"HTTP {status_code}: {message}")
return Mock(return_value=mock_response)


def make_json_error_response() -> Mock:
"""Create a mock requests.get whose .json() call raises an exception."""
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.side_effect = Exception("Invalid JSON")
return Mock(return_value=mock_response)


def make_missing_key_response(json_data: dict) -> Mock:
"""Create a mock requests.get returning JSON with the given (possibly incomplete) data."""
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.return_value = json_data
return Mock(return_value=mock_response)


def make_invalid_base64_response(invalid_content: str) -> Mock:
"""Create a mock requests.get returning JSON with malformed base64 content."""
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.return_value = {"content": invalid_content}
return Mock(return_value=mock_response)


def make_invalid_yaml_response(invalid_yaml: str) -> Mock:
"""Create a mock requests.get returning valid base64 but invalid YAML content."""
encoded = base64.b64encode(invalid_yaml.encode()).decode()
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.return_value = {"content": encoded}
return Mock(return_value=mock_response)


@pytest.fixture
def mock_github_ref(monkeypatch):
"""Factory fixture for mocking GitHub API git ref lookups.
Expand Down
6 changes: 6 additions & 0 deletions scripts/update_openhands_charts/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[mutmut]
paths_to_mutate=update_openhands_charts.py
also_copy=
conftest.py
test_update_openhands_charts.py
tests_dir=test_update_openhands_charts.py
Loading
Loading