Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This changelog documents user-relevant changes to the GitHub runner charm.

## 2026-04-03

- Fixed charm not entering blocked/waiting status when the image integration has no image available. Previously, some event handlers (`upgrade-charm`, `planner-relation-changed`, `planner-relation-broken`) would start the runner manager service without checking image readiness, causing the service to error with "No runner combinations configured."

## 2026-03-30

- Fixed stale `systemd` service files left behind when a unit is removed from a co-located machine. The service is now disabled, the service file removed, and the unit data cleaned up on stop.
Expand Down
7 changes: 1 addition & 6 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,6 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
]
flush_runners = True

self._check_image_ready()

self._reconcile(state)

if flush_runners:
Expand Down Expand Up @@ -389,6 +387,7 @@ def _reconcile(self, state: CharmState) -> None:
Args:
state: The charm state.
"""
self._check_image_ready()
self._setup_service(state)
self._update_planner_flavor(state)

Expand Down Expand Up @@ -475,8 +474,6 @@ def _apt_install(self, packages: Sequence[str]) -> None:
def _on_debug_ssh_relation_changed(self, _: ops.RelationChangedEvent) -> None:
"""Handle debug ssh relation changed event."""
self.unit.status = MaintenanceStatus("Added debug-ssh relation")
self._check_image_ready()

state = self._setup_state()
self._reconcile(state)

Expand All @@ -498,8 +495,6 @@ def _on_image_relation_joined(self, _: ops.RelationJoinedEvent) -> None:
def _on_image_relation_changed(self, _: ops.RelationChangedEvent) -> None:
"""Handle image relation changed event."""
self.unit.status = MaintenanceStatus("Update image for runners")
self._check_image_ready()

state = self._setup_state()
self._reconcile(state)

Expand Down
37 changes: 37 additions & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,42 @@ def test_on_config_changed_openstack_clouds_yaml(mock_side_effects):
assert harness.charm.unit.status == BlockedStatus("Please provide image integration.")


@pytest.mark.parametrize(
"handler_name, emit_event",
[
pytest.param(
"_on_upgrade_charm",
lambda h: h.charm.on.upgrade_charm.emit(),
id="upgrade-charm",
),
pytest.param(
"_on_planner_relation_changed",
lambda h: h.charm._on_planner_relation_changed(MagicMock()),
id="planner-relation-changed",
),
],
)
def test_reconcile_blocks_when_image_not_ready(
harness: Harness, mock_side_effects, monkeypatch, handler_name, emit_event
):
"""
arrange: Set up charm with no image integration.
act: Fire an event that calls _reconcile without an explicit _check_image_ready call.
assert: Charm enters blocked/waiting status and the service is stopped.
"""
harness.charm._setup_state = MagicMock()
manager_client_mock = MagicMock(spec=GitHubRunnerManagerClient)
harness.charm._manager_client = manager_client_mock
mock_mgr_service = MagicMock()
monkeypatch.setattr("charm.manager_service", mock_mgr_service)
monkeypatch.setattr("charm.execute_command", MagicMock(return_value=(0, "Mock_stdout")))
monkeypatch.setattr("charm.pathlib", MagicMock())

emit_event(harness)

assert harness.charm.unit.status.name == BlockedStatus.name


def test_metric_log_ownership_for_upgrade(
harness: Harness, mock_side_effects, tmp_path: Path, monkeypatch
):
Expand Down Expand Up @@ -686,6 +722,7 @@ def test_planner_relation_changed_writes_flavor(monkeypatch: pytest.MonkeyPatch)
state_mock.runner_config.openstack_image.tags = ["x64", "noble"]
harness.charm._setup_state = MagicMock(return_value=state_mock)
harness.charm._setup_service = MagicMock()
harness.charm._check_image_ready = MagicMock()

harness.update_relation_data(relation_id, "planner-app/0", {"endpoint": "http://example.com"})

Expand Down
Loading