From 4d5e6d661dfc83ded522c735e8713d7c2364bf17 Mon Sep 17 00:00:00 2001 From: Rory Byrne Date: Sat, 1 Aug 2026 13:32:30 +0100 Subject: [PATCH 1/2] chore: sync uv.lock to 0.8.0 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index c93575d..95798f4 100644 --- a/uv.lock +++ b/uv.lock @@ -140,7 +140,7 @@ wheels = [ [[package]] name = "osa-py" -version = "0.7.0" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From c6fca15929ad509f3aac6dd9a0b6eebc158efd8c Mon Sep 17 00:00:00 2001 From: Rory Byrne Date: Sat, 1 Aug 2026 13:32:40 +0100 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20drop=20compose=20profiles=20?= =?UTF-8?q?=E2=80=94=20osa=20start/stop=20manage=20the=20whole=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiles added indirection for no benefit now that the dashboard is a default service. Remove them so `osa start` brings up every service in the compose template and `osa stop` (docker compose down, which is project-wide) tears them all down. - compose: drop `profiles: - ui` from the dashboard; comment out the out-of-date `web` service (uncomment to re-enable once it's updated). - instance: `_build_compose_command` no longer takes/emits profiles; `start_instance` drops the `with_ui` param and always reports the dashboard URL. Fix the stale `--with-ui` note in the .env template. - main: remove the `--no-ui` flag. - tests: assert `--profile` is never emitted; drop the with_ui/no-ui cases. --- osa/cli/instance.py | 30 ++++++---------- osa/cli/main.py | 9 +---- osa/cli/templates/docker-compose.yml | 29 +++++++-------- tests/test_instance.py | 54 ++++++++++------------------ 4 files changed, 44 insertions(+), 78 deletions(-) diff --git a/osa/cli/instance.py b/osa/cli/instance.py index 35164eb..8dc8786 100644 --- a/osa/cli/instance.py +++ b/osa/cli/instance.py @@ -258,7 +258,7 @@ def _store_dev_credentials(project_dir: Path) -> None: ORCID_SANDBOX=true ORCID_ADMINS=[] -# === Dashboard (management UI, started with `osa start --with-ui`) === +# === Dashboard (management UI, started with `osa start`) === # The dashboard mints its archive token with JWT_SECRET (shared with the # server) and signs its session cookie with SESSION_SECRET. Log in with # DASHBOARD_USERNAME/PASSWORD, or just run `osa dashboard` to open it signed in. @@ -367,7 +367,6 @@ def _build_compose_command( project_dir: Path, project_name: str, source: Path | None = None, - profiles: list[str] | None = None, ) -> list[str]: cmd = [ "docker", @@ -388,9 +387,6 @@ def _build_compose_command( if override_path.exists(): cmd.extend(["-f", str(override_path)]) - for profile in profiles or []: - cmd.extend(["--profile", profile]) - cmd.extend(["--env-file", str(project_dir / ".env")]) return cmd @@ -462,13 +458,11 @@ def start_instance( project_dir: Path, detach: bool = True, source: Path | None = None, - with_ui: bool = True, osa_version: str | None = None, ui: UI | None = None, ) -> None: ui = ui or UI.create() project_name = _read_project_name(project_dir) - profiles = ["ui"] if with_ui else [] if osa_version is not None: image_version = osa_version @@ -481,7 +475,6 @@ def start_instance( project_dir=project_dir, project_name=project_name, source=source, - profiles=profiles, ) args = [*cmd, "up"] @@ -514,17 +507,16 @@ def start_instance( hint="Run `osa logs server --tail 50` for details", ) ui.success(f"OSA {image_version} running", arrow=LOCAL_SERVER_URL) - if with_ui: - # Report the actual configured port, not the default — the operator may - # have overridden it in .env. `or` (not a get-default) so a present-but- - # empty value falls back like docker compose's `${DASHBOARD_PORT:-8081}`. - dashboard_port = ( - _read_env_file(project_dir / ".env").get("DASHBOARD_PORT") or "8081" - ) - ui.info( - f"Dashboard http://localhost:{dashboard_port}" - " · run `osa dashboard` to open it signed in" - ) + # Report the actual configured port, not the default — the operator may + # have overridden it in .env. `or` (not a get-default) so a present-but- + # empty value falls back like docker compose's `${DASHBOARD_PORT:-8081}`. + dashboard_port = ( + _read_env_file(project_dir / ".env").get("DASHBOARD_PORT") or "8081" + ) + ui.info( + f"Dashboard http://localhost:{dashboard_port}" + " · run `osa dashboard` to open it signed in" + ) def open_dashboard(*, project_dir: Path, ui: UI | None = None) -> None: diff --git a/osa/cli/main.py b/osa/cli/main.py index 4f4a294..75f61dd 100644 --- a/osa/cli/main.py +++ b/osa/cli/main.py @@ -314,18 +314,12 @@ def start( Optional[Path], typer.Option("--source", help="Path to OSA server source for dev mode."), ] = None, - no_ui: Annotated[ - bool, - typer.Option( - "--no-ui", help="Start only the API, without the web UI + dashboard." - ), - ] = False, osa_version: Annotated[ Optional[str], typer.Option("--osa-version", help="OSA server image version tag."), ] = None, ) -> None: - """Start the local OSA instance (API + web UI + dashboard).""" + """Start the local OSA instance (API + dashboard).""" from osa.cli.instance import InstanceError, start_instance ui = _ui(ctx) @@ -334,7 +328,6 @@ def start( project_dir=Path.cwd(), detach=detach, source=source.resolve() if source else None, - with_ui=not no_ui, osa_version=osa_version, ui=ui, ) diff --git a/osa/cli/templates/docker-compose.yml b/osa/cli/templates/docker-compose.yml index 4696e0d..1249f89 100644 --- a/osa/cli/templates/docker-compose.yml +++ b/osa/cli/templates/docker-compose.yml @@ -72,20 +72,19 @@ services: start_period: 5s restart: unless-stopped - # The web app is out of date and excluded from `osa start` for now — it sits - # in its own `web` profile (the default `ui` profile is server + dashboard). - web: - image: ghcr.io/opensciencearchive/osa-web:${OSA_IMAGE_VERSION:-latest} - environment: - API_URL: http://server:8000 - ports: - - "${WEB_PORT:-8080}:3000" - depends_on: - server: - condition: service_healthy - profiles: - - web - restart: unless-stopped + # The end-user web app is out of date, so `osa start` does not run it. Kept + # here (commented out) to make re-enabling trivial once it's updated: drop the + # comments and it joins the stack like every other service. + # web: + # image: ghcr.io/opensciencearchive/osa-web:${OSA_IMAGE_VERSION:-latest} + # environment: + # API_URL: http://server:8000 + # ports: + # - "${WEB_PORT:-8080}:3000" + # depends_on: + # server: + # condition: service_healthy + # restart: unless-stopped # Management dashboard. Logs in with the dashboard credential, then mints a # SUPERADMIN token signed with the SHARED JWT_SECRET (same value the server @@ -107,8 +106,6 @@ services: depends_on: server: condition: service_healthy - profiles: - - ui restart: unless-stopped volumes: diff --git a/tests/test_instance.py b/tests/test_instance.py index 7f75271..310b55d 100644 --- a/tests/test_instance.py +++ b/tests/test_instance.py @@ -372,13 +372,11 @@ def test_without_override_file(self, tmp_path: Path) -> None: cmd = _build_compose_command(project_dir=tmp_path, project_name="test") assert "docker-compose.override.yml" not in " ".join(cmd) - def test_with_profiles(self, tmp_path: Path) -> None: + def test_no_profiles(self, tmp_path: Path) -> None: + # Profiles are gone — every service in the template is always managed. _write_osa_yaml(tmp_path) - cmd = _build_compose_command( - project_dir=tmp_path, project_name="test", profiles=["ui"] - ) - idx = cmd.index("--profile") - assert cmd[idx + 1] == "ui" + cmd = _build_compose_command(project_dir=tmp_path, project_name="test") + assert "--profile" not in cmd def test_source_adds_dev_override(self, tmp_path: Path) -> None: _write_osa_yaml(tmp_path) @@ -492,9 +490,16 @@ def test_with_source_adds_build(self, tmp_path: Path) -> None: args = mock_run.call_args[0][0] assert "--build" in args - def test_source_still_includes_ui(self, tmp_path: Path) -> None: - # `--source` builds the server from source but must still bring up the - # UI profile (web + dashboard) — it's independent of the source build. + def test_uses_no_profiles(self, tmp_path: Path) -> None: + # Profiles are gone — `osa start` manages every service in the template. + _write_osa_yaml(tmp_path) + with _mock_streamed() as mock_run: + start_instance(project_dir=tmp_path, osa_version="v0.0.0") + assert "--profile" not in mock_run.call_args[0][0] + + def test_source_uses_no_profiles(self, tmp_path: Path) -> None: + # `--source` builds the server from source and still runs the whole + # stack (dashboard included) without any profile gating. _write_osa_yaml(tmp_path) source = tmp_path / "server-src" source.mkdir() @@ -502,26 +507,9 @@ def test_source_still_includes_ui(self, tmp_path: Path) -> None: start_instance(project_dir=tmp_path, source=source, osa_version="v0.0.0") args = mock_run.call_args[0][0] assert "--build" in args - assert "--profile" in args - assert args[args.index("--profile") + 1] == "ui" - - def test_ui_profile_is_on_by_default(self, tmp_path: Path) -> None: - # `osa start` brings up the web UI + dashboard by default. - _write_osa_yaml(tmp_path) - with _mock_streamed() as mock_run: - start_instance(project_dir=tmp_path, osa_version="v0.0.0") - args = mock_run.call_args[0][0] - assert "--profile" in args - assert args[args.index("--profile") + 1] == "ui" - - def test_no_ui_omits_profile(self, tmp_path: Path) -> None: - # `osa start --no-ui` (with_ui=False) starts only the API. - _write_osa_yaml(tmp_path) - with _mock_streamed() as mock_run: - start_instance(project_dir=tmp_path, with_ui=False, osa_version="v0.0.0") - assert "--profile" not in mock_run.call_args[0][0] + assert "--profile" not in args - def test_with_ui_reports_dashboard_port(self, tmp_path: Path) -> None: + def test_reports_dashboard_port(self, tmp_path: Path) -> None: # The printed dashboard URL must reflect the .env port override. from unittest.mock import MagicMock @@ -529,14 +517,12 @@ def test_with_ui_reports_dashboard_port(self, tmp_path: Path) -> None: (tmp_path / ".env").write_text("JWT_SECRET=x\nDASHBOARD_PORT=9091\n") ui = MagicMock() with _mock_streamed(): - start_instance( - project_dir=tmp_path, with_ui=True, osa_version="v0.0.0", ui=ui - ) + start_instance(project_dir=tmp_path, osa_version="v0.0.0", ui=ui) printed = " ".join(str(call) for call in ui.info.call_args_list) assert "9091" in printed # DASHBOARD_PORT assert "Web UI" not in printed # web is excluded from `osa start` - def test_with_ui_empty_port_falls_back_like_compose(self, tmp_path: Path) -> None: + def test_empty_port_falls_back_like_compose(self, tmp_path: Path) -> None: # A present-but-empty port must fall back to the default (as compose's # `:-` does), not print a blank port. from unittest.mock import MagicMock @@ -545,9 +531,7 @@ def test_with_ui_empty_port_falls_back_like_compose(self, tmp_path: Path) -> Non (tmp_path / ".env").write_text("JWT_SECRET=x\nDASHBOARD_PORT=\n") ui = MagicMock() with _mock_streamed(): - start_instance( - project_dir=tmp_path, with_ui=True, osa_version="v0.0.0", ui=ui - ) + start_instance(project_dir=tmp_path, osa_version="v0.0.0", ui=ui) printed = " ".join(str(call) for call in ui.info.call_args_list) assert "localhost:8081" in printed