diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a8f2c0..e6796652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ For detailed SDK migration guides, see [RELEASE_NOTES.md](RELEASE_NOTES.md). ## [Unreleased] +## [0.7.3] — 2026-05-08 + +`stardag modal deploy` and `stardag modal stardag-api-key create` now +display the slug that actually corresponds to the resolved +workspace/environment UUID. Previously the slug was read from the active +CLI profile's TOML, which could be unrelated to the resolved UUID when +env vars or a custom `config_provider` override the IDs — producing +misleading lines pairing the resolved UUID with a slug from an unrelated +profile. No client-code changes — `pip install -U stardag` is +sufficient. See +[RELEASE_NOTES.md](RELEASE_NOTES.md#v073--correct-slug-display-in-stardag-modal-cli) +for details. + +### SDK + +- **`stardag/_cli/modal.py`**: replaced `_get_profile_slugs` with + `_resolve_display_slugs`, which reverse-looks up the slug from the + resolved UUID via the id-cache. Slug is omitted when no cache hit + rather than guessing from the active profile. +- **`stardag/config/cache.py`**: added `get_cached_workspace_slug` and + `get_cached_environment_slug` (UUID → slug reverse lookups). + ## [0.7.2] — 2026-05-08 `sd.build(resume_build_id=...)` now fires a `BUILD_RESUMED` event so diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a80efc9d..8edf00e0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,28 @@ For changes to the Registry API, UI, and other components, see [CHANGELOG.md](CH --- +## v0.7.3 — Correct slug display in `stardag modal` CLI + +Fixes a misleading display in `stardag modal deploy` and +`stardag modal stardag-api-key create` where the workspace/environment +slug printed alongside the resolved UUID could come from a different +profile than the one whose IDs were actually used. The CLI now +reverse-looks up the slug from the resolved UUID via the id-cache, or +omits it when no cache entry exists. + +This typically affected setups that override `STARDAG_WORKSPACE_ID` / +`STARDAG_ENVIRONMENT_ID` via env vars or a custom `config_provider` +default factory while leaving an unrelated profile active — for example +hypothetical output like +`Environment: (env-b-slug)`, where the UUID resolves to +environment A but the slug is read from a profile pointing at +environment B. The deployed bytes were already correct; only the +printed slug was wrong. + +**No client-code changes** — `pip install -U stardag` is sufficient. + +--- + ## v0.7.2 — Build resume status fix and SKIPPED UI polish Fixes a UX bug where `sd.build(resume_build_id=...)` silently reused the diff --git a/lib/stardag/src/stardag/_cli/modal.py b/lib/stardag/src/stardag/_cli/modal.py index 2bca9e46..e51b0f35 100644 --- a/lib/stardag/src/stardag/_cli/modal.py +++ b/lib/stardag/src/stardag/_cli/modal.py @@ -30,11 +30,11 @@ from stardag._cli._helpers import get_authenticated_client from stardag._cli.credentials import ( - get_active_profile, - list_profiles, + list_registries, resolve_environment_slug_to_id, resolve_workspace_slug_to_id, ) +from stardag.config.cache import get_cached_slugs from stardag.config.loader import clear_config_cache, get_config from stardag.integration.modal import StardagApp @@ -61,37 +61,47 @@ app.add_typer(stardag_api_key_app, name="stardag-api-key") -def _get_profile_slugs( - profile_name: str | None, - workspace_id: str | None = None, - environment_id: str | None = None, -) -> tuple[str | None, str | None]: - """Get workspace and environment slugs from a profile. +def _registry_name_for_url(api_url: str | None) -> str | None: + """Look up the configured registry name for a given URL. - Returns (workspace_slug, environment_slug) where slug is None if - the profile value matches the resolved ID (i.e. it's already a UUID). + The id-cache is keyed by registry *name* (the TOML key under ``[registry.]``) + rather than URL, so we reverse-lookup the name from the URL the resolved IDs were + fetched against. Returns ``None`` if no registry with a matching URL is configured + (e.g. STARDAG_API_URL is set without a corresponding TOML entry). """ - if not profile_name: - profile_name_resolved, _ = get_active_profile() - profile_name = profile_name_resolved - - if not profile_name: - return None, None - - profiles = list_profiles() - prof = profiles.get(profile_name) - if not prof: + if not api_url: + return None + target = api_url.rstrip("/") + for name, url in list_registries().items(): + if url.rstrip("/") == target: + return name + return None + + +def _resolve_display_slugs( + api_url: str | None, + workspace_id: str | None, + environment_id: str | None, +) -> tuple[str | None, str | None]: + """Reverse-lookup workspace/environment slugs for the resolved UUIDs. + + Why: the resolved UUIDs may come from env vars or a custom config_provider + override and have no relation to the active profile's TOML slugs. Reading + `prof["environment"]` could display a slug for a totally different env + than the resolved UUID. We also can't trust ``get_config().context.registry_name`` + because callers like ``deploy(--profile foo)`` and ``stardag_api_key_create`` + resolve IDs under a temporarily-overridden profile, then restore the env + before this function runs — leaving ``get_config()`` pointing at the + *active* profile, which may use a different registry. Pin the registry to + the URL the IDs were actually resolved against. + """ + registry_name = _registry_name_for_url(api_url) + if not registry_name: return None, None - - ws_slug = prof["workspace"] if prof["workspace"] != workspace_id else None - env_slug = prof["environment"] if prof["environment"] != environment_id else None - return ws_slug, env_slug + return get_cached_slugs(registry_name, workspace_id, environment_id) -def _print_stardag_context( - env_vars: dict[str, str], - profile_name: str | None = None, -) -> None: +def _print_stardag_context(env_vars: dict[str, str]) -> None: """Print stardag context info (registry, workspace, environment, target roots).""" import json @@ -99,7 +109,9 @@ def _print_stardag_context( ws_id = env_vars.get("STARDAG_WORKSPACE_ID", "N/A") env_id = env_vars.get("STARDAG_ENVIRONMENT_ID", "N/A") - ws_slug, env_slug = _get_profile_slugs(profile_name, ws_id, env_id) + ws_slug, env_slug = _resolve_display_slugs( + env_vars.get("STARDAG_API_URL"), ws_id, env_id + ) console.print(f"[dim] Registry: {registry}[/dim]") @@ -297,7 +309,7 @@ def stardag_api_key_create( if api_key_name is None: api_key_name = f"modal-{modal_env or 'default'}" - ws_slug, env_slug = _get_profile_slugs(stardag_profile, ws_id, env_id) + ws_slug, env_slug = _resolve_display_slugs(api_url, ws_id, env_id) ws_display = f"{ws_id} ({ws_slug})" if ws_slug else ws_id env_display = f"{env_id} ({env_slug})" if env_slug else env_id @@ -561,7 +573,7 @@ def deploy( console.print(f"[cyan]Using stardag profile: {profile}[/cyan]") env_vars = get_profile_env_vars(profile) if env_vars: - _print_stardag_context(env_vars, profile) + _print_stardag_context(env_vars) extra_secrets.append( modal.Secret.from_dict(dict(env_vars)) # type: ignore[arg-type] ) diff --git a/lib/stardag/src/stardag/config/cache.py b/lib/stardag/src/stardag/config/cache.py index 3912c3ef..e019be4b 100644 --- a/lib/stardag/src/stardag/config/cache.py +++ b/lib/stardag/src/stardag/config/cache.py @@ -188,6 +188,32 @@ def cache_environment_id( save_id_cache(cache) +def get_cached_slugs( + registry: str, + workspace_id: str | None, + environment_id: str | None, +) -> tuple[str | None, str | None]: + """Reverse lookup: cached (workspace_slug, environment_slug) for the given UUIDs. + + Loads the id-cache once. Returns ``(None, None)`` for inputs without a cache hit. + """ + cache = load_id_cache() + ws_slug: str | None = None + if workspace_id: + for slug, ws_id in cache.workspaces.get(registry, {}).items(): + if ws_id == workspace_id: + ws_slug = slug + break + env_slug: str | None = None + if workspace_id and environment_id: + envs = cache.environments.get(registry, {}).get(workspace_id, {}) + for slug, env_id in envs.items(): + if env_id == environment_id: + env_slug = slug + break + return ws_slug, env_slug + + def _looks_like_uuid(value: str) -> bool: """Check if a string looks like a UUID.""" uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"