chore: env path refactor (#3691)

Co-authored-by: Suchintan <suchintan@users.noreply.github.com>
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
greg niemeyer
2025-10-12 10:36:24 -07:00
committed by GitHub
parent 9b2bbda3c8
commit cf9e1c2552
11 changed files with 101 additions and 76 deletions

View File

@@ -1,6 +1,8 @@
import typer import typer
from dotenv import load_dotenv from dotenv import load_dotenv
from skyvern.utils.env_paths import resolve_backend_env_path
from .docs import docs_app from .docs import docs_app
from .init_command import init, init_browser from .init_command import init, init_browser
from .quickstart import quickstart_app from .quickstart import quickstart_app
@@ -54,5 +56,5 @@ def init_browser_command() -> None:
if __name__ == "__main__": # pragma: no cover - manual CLI invocation if __name__ == "__main__": # pragma: no cover - manual CLI invocation
load_dotenv() load_dotenv(resolve_backend_env_path())
cli_app() cli_app()

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import os
import subprocess import subprocess
import uuid import uuid
@@ -10,6 +9,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm, Prompt from rich.prompt import Confirm, Prompt
from skyvern.utils import migrate_db from skyvern.utils import migrate_db
from skyvern.utils.env_paths import resolve_backend_env_path
from .browser import setup_browser_config from .browser import setup_browser_config
from .console import console from .console import console
@@ -50,8 +50,9 @@ def init(
else: else:
console.print("[red]Failed to generate local organization API key. Please check server logs.[/red]") console.print("[red]Failed to generate local organization API key. Please check server logs.[/red]")
if os.path.exists(".env"): backend_env_path = resolve_backend_env_path()
console.print("💡 [.env] file already exists.", style="yellow") if backend_env_path.exists():
console.print(f"💡 [{backend_env_path}] file already exists.", style="yellow")
redo_llm_setup = Confirm.ask( redo_llm_setup = Confirm.ask(
"Do you want to go through [bold yellow]LLM provider setup again[/bold yellow]?", "Do you want to go through [bold yellow]LLM provider setup again[/bold yellow]?",
default=False, default=False,
@@ -108,7 +109,7 @@ def init(
analytics_id = analytics_id_input if analytics_id_input else str(uuid.uuid4()) analytics_id = analytics_id_input if analytics_id_input else str(uuid.uuid4())
update_or_add_env_var("ANALYTICS_ID", analytics_id) update_or_add_env_var("ANALYTICS_ID", analytics_id)
update_or_add_env_var("SKYVERN_API_KEY", api_key) update_or_add_env_var("SKYVERN_API_KEY", api_key)
console.print("✅ [green].env file has been initialized.[/green]") console.print(f"✅ [green]{resolve_backend_env_path()} file has been initialized.[/green]")
if Confirm.ask("\nWould you like to [bold yellow]configure the MCP server[/bold yellow]?", default=True): if Confirm.ask("\nWould you like to [bold yellow]configure the MCP server[/bold yellow]?", default=True):
setup_mcp() setup_mcp()

View File

@@ -1,15 +1,15 @@
from pathlib import Path
from dotenv import load_dotenv, set_key from dotenv import load_dotenv, set_key
from rich.panel import Panel from rich.panel import Panel
from rich.prompt import Confirm, Prompt from rich.prompt import Confirm, Prompt
from skyvern.utils.env_paths import resolve_backend_env_path
from .console import console from .console import console
def update_or_add_env_var(key: str, value: str) -> None: def update_or_add_env_var(key: str, value: str) -> None:
"""Update or add environment variable in .env file.""" """Update or add environment variable in .env file."""
env_path = Path(".env") env_path = resolve_backend_env_path()
if not env_path.exists(): if not env_path.exists():
env_path.touch() env_path.touch()
defaults = { defaults = {

View File

@@ -11,6 +11,7 @@ from skyvern.forge import app
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.library import Skyvern from skyvern.library import Skyvern
from skyvern.utils import detect_os, get_windows_appdata_roaming from skyvern.utils import detect_os, get_windows_appdata_roaming
from skyvern.utils.env_paths import resolve_backend_env_path
from .console import console from .console import console
@@ -132,12 +133,13 @@ def setup_claude_desktop_config(host_system: str, path_to_env: str) -> bool:
with open(path_claude_config, "w") as f: with open(path_claude_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2) json.dump({"mcpServers": {}}, f, indent=2)
load_dotenv(".env") backend_env_path = resolve_backend_env_path()
load_dotenv(backend_env_path)
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key: if not skyvern_base_url or not skyvern_api_key:
console.print( console.print(
f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Claude MCP. Please open {path_claude_config} and set these variables manually.[/red]" f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Claude MCP. Please open {path_claude_config} and set these variables manually.[/red]"
) )
return False return False
@@ -184,12 +186,13 @@ def setup_cursor_config(host_system: str, path_to_env: str) -> bool:
with open(path_cursor_config, "w") as f: with open(path_cursor_config, "w") as f:
json.dump({"mcpServers": {}}, f, indent=2) json.dump({"mcpServers": {}}, f, indent=2)
load_dotenv(".env") backend_env_path = resolve_backend_env_path()
load_dotenv(backend_env_path)
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key: if not skyvern_base_url or not skyvern_api_key:
console.print( console.print(
f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Cursor MCP. Please open [link]{path_cursor_config}[/link] and set these variables manually.[/red]" f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Cursor MCP. Please open [link]{path_cursor_config}[/link] and set these variables manually.[/red]"
) )
return False return False
@@ -228,12 +231,13 @@ def setup_windsurf_config(host_system: str, path_to_env: str) -> bool:
return False return False
path_windsurf_config = get_windsurf_config_path(host_system) path_windsurf_config = get_windsurf_config_path(host_system)
load_dotenv(".env") backend_env_path = resolve_backend_env_path()
load_dotenv(backend_env_path)
skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "")
skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "")
if not skyvern_base_url or not skyvern_api_key: if not skyvern_base_url or not skyvern_api_key:
console.print( console.print(
f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Windsurf MCP. Please open {path_windsurf_config} and set these variables manually.[/red]" f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Windsurf MCP. Please open {path_windsurf_config} and set these variables manually.[/red]"
) )
try: try:

View File

@@ -2,7 +2,6 @@ import asyncio
import os import os
import shutil import shutil
import subprocess import subprocess
from pathlib import Path
from typing import Any, List from typing import Any, List
import psutil import psutil
@@ -17,6 +16,7 @@ from skyvern.cli.utils import start_services
from skyvern.config import settings from skyvern.config import settings
from skyvern.library.skyvern import Skyvern from skyvern.library.skyvern import Skyvern
from skyvern.utils import detect_os from skyvern.utils import detect_os
from skyvern.utils.env_paths import resolve_backend_env_path, resolve_frontend_env_path
from .console import console from .console import console
@@ -83,8 +83,7 @@ def kill_pids(pids: List[int]) -> None:
@run_app.command(name="server") @run_app.command(name="server")
def run_server() -> None: def run_server() -> None:
"""Run the Skyvern API server.""" """Run the Skyvern API server."""
load_dotenv() load_dotenv(resolve_backend_env_path())
load_dotenv(".env")
from skyvern.config import settings # noqa: PLC0415 from skyvern.config import settings # noqa: PLC0415
port = settings.PORT port = settings.PORT
@@ -117,68 +116,27 @@ def run_ui() -> None:
except Exception as e: # pragma: no cover - CLI safeguards except Exception as e: # pragma: no cover - CLI safeguards
console.print(f"[red]Error checking for process: {e}[/red]") console.print(f"[red]Error checking for process: {e}[/red]")
# Try multiple methods to find the frontend directory frontend_env_path = resolve_frontend_env_path()
frontend_dir = None if frontend_env_path is None:
console.print("[bold red]ERROR: Skyvern Frontend directory not found.[/bold red]")
# Method 1: Relative to current working directory
cwd_frontend = Path.cwd() / "skyvern-frontend"
if cwd_frontend.exists():
frontend_dir = cwd_frontend
# Method 2: Relative to the module file (original method)
if frontend_dir is None:
module_based_frontend = Path(__file__).parent.parent.parent / "skyvern-frontend"
if module_based_frontend.exists():
frontend_dir = module_based_frontend
# Method 3: Search up the directory tree from current working directory
if frontend_dir is None:
current = Path.cwd()
while current != current.parent: # Stop at filesystem root
candidate = current / "skyvern-frontend"
if candidate.exists() and candidate.is_dir():
frontend_dir = candidate
break
current = current.parent
if frontend_dir is None:
console.print(
f"[bold red]ERROR: Skyvern Frontend directory not found. Searched in:[/bold red]\n"
f"{cwd_frontend}\n"
f"{Path(__file__).parent.parent.parent / 'skyvern-frontend'}\n"
f" • Parent directories of {Path.cwd()}\n"
f"[bold red]Are you in the right repo?[/bold red]"
)
return return
frontend_env_path = frontend_dir / ".env" frontend_dir = frontend_env_path.parent
if not frontend_env_path.exists(): if not frontend_env_path.exists():
console.print("[bold blue]Setting up frontend .env file...[/bold blue]") console.print("[bold blue]Setting up frontend .env file...[/bold blue]")
shutil.copy(frontend_dir / ".env.example", frontend_env_path) shutil.copy(frontend_dir / ".env.example", frontend_env_path)
console.print("✅ [green]Successfully set up frontend .env file[/green]") console.print("✅ [green]Successfully set up frontend .env file[/green]")
# Look for .env file in multiple locations backend_env_path = resolve_backend_env_path()
main_env_path = None if backend_env_path.exists():
env_search_paths = [ load_dotenv(backend_env_path)
Path.cwd() / ".env",
frontend_dir.parent / ".env",
Path(__file__).parent.parent.parent / ".env",
]
for env_path in env_search_paths:
if env_path.exists():
main_env_path = env_path
break
if main_env_path and main_env_path.exists():
load_dotenv(main_env_path)
skyvern_api_key = os.getenv("SKYVERN_API_KEY") skyvern_api_key = os.getenv("SKYVERN_API_KEY")
if skyvern_api_key: if skyvern_api_key:
set_key(str(frontend_env_path), "VITE_SKYVERN_API_KEY", skyvern_api_key) set_key(frontend_env_path, "VITE_SKYVERN_API_KEY", skyvern_api_key)
else: else:
console.print("[red]ERROR: SKYVERN_API_KEY not found in .env file[/red]") console.print("[red]ERROR: SKYVERN_API_KEY not found in .env file[/red]")
else: else:
console.print("[red]ERROR: .env file not found[/red]") console.print(f"[red]ERROR: Backend .env file not found at {backend_env_path}[/red]")
os.chdir(frontend_dir) os.chdir(frontend_dir)

View File

@@ -11,6 +11,7 @@ from rich.panel import Panel
from skyvern.client import Skyvern from skyvern.client import Skyvern
from skyvern.config import settings from skyvern.config import settings
from skyvern.utils.env_paths import resolve_backend_env_path
from .console import console from .console import console
@@ -33,8 +34,7 @@ def tasks_callback(
def _get_client(api_key: str | None = None) -> Skyvern: def _get_client(api_key: str | None = None) -> Skyvern:
"""Instantiate a Skyvern SDK client using environment variables.""" """Instantiate a Skyvern SDK client using environment variables."""
load_dotenv() load_dotenv(resolve_backend_env_path())
load_dotenv(".env")
key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY
return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key) return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key)

View File

@@ -5,6 +5,7 @@ import sys
import typer import typer
from skyvern.cli.console import console from skyvern.cli.console import console
from skyvern.utils.env_paths import resolve_backend_env_path
async def start_services(server_only: bool = False) -> None: async def start_services(server_only: bool = False) -> None:
@@ -28,7 +29,7 @@ async def start_services(server_only: bool = False) -> None:
console.print("\n🎉 [bold green]Skyvern is now running![/bold green]") console.print("\n🎉 [bold green]Skyvern is now running![/bold green]")
console.print("🌐 [bold]Access the UI at:[/bold] [cyan]http://localhost:8080[/cyan]") console.print("🌐 [bold]Access the UI at:[/bold] [cyan]http://localhost:8080[/cyan]")
console.print("🔑 [bold]Your API key is in your .env file as SKYVERN_API_KEY[/bold]") console.print(f"🔑 [bold]Your API key is in {resolve_backend_env_path()} as SKYVERN_API_KEY[/bold]")
# Wait for processes to complete (they won't unless killed) # Wait for processes to complete (they won't unless killed)
if not server_only: if not server_only:

View File

@@ -11,6 +11,7 @@ from rich.panel import Panel
from skyvern.client import Skyvern from skyvern.client import Skyvern
from skyvern.config import settings from skyvern.config import settings
from skyvern.utils.env_paths import resolve_backend_env_path
from .console import console from .console import console
from .tasks import _list_workflow_tasks from .tasks import _list_workflow_tasks
@@ -34,8 +35,7 @@ def workflow_callback(
def _get_client(api_key: str | None = None) -> Skyvern: def _get_client(api_key: str | None = None) -> Skyvern:
"""Instantiate a Skyvern SDK client using environment variables.""" """Instantiate a Skyvern SDK client using environment variables."""
load_dotenv() load_dotenv(resolve_backend_env_path())
load_dotenv(".env")
key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY
return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key) return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key)

View File

@@ -2,10 +2,22 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from skyvern import constants from skyvern import constants
from skyvern.constants import SKYVERN_DIR from skyvern.constants import SKYVERN_DIR
from skyvern.utils.env_paths import resolve_backend_env_path
# NOTE: _DEFAULT_ENV_FILES resolves .env paths at import time and assumes
# the process has changed dir to the desired project root by this time.
# Even if we were to resolve paths at instantiation time, the global `settings`
# singleton instantiation at the bottom of this file also runs at import time
# and relies on the same assumption.
_DEFAULT_ENV_FILES = (
resolve_backend_env_path(".env"),
resolve_backend_env_path(".env.staging"),
resolve_backend_env_path(".env.prod"),
)
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=(".env", ".env.staging", ".env.prod"), extra="ignore") model_config = SettingsConfigDict(env_file=_DEFAULT_ENV_FILES, extra="ignore")
# settings for experimentation # settings for experimentation
ENABLE_EXP_ALL_TEXTUAL_ELEMENTS_INTERACTABLE: bool = False ENABLE_EXP_ALL_TEXTUAL_ELEMENTS_INTERACTABLE: bool = False

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
import os
import typing import typing
from typing import Any from typing import Any
@@ -28,6 +27,7 @@ from skyvern.schemas.run_blocks import CredentialType
from skyvern.schemas.runs import CUA_ENGINES, ProxyLocation, RunEngine, RunStatus, RunType from skyvern.schemas.runs import CUA_ENGINES, ProxyLocation, RunEngine, RunStatus, RunType
from skyvern.services import run_service, task_v1_service, task_v2_service from skyvern.services import run_service, task_v1_service, task_v2_service
from skyvern.utils import migrate_db from skyvern.utils import migrate_db
from skyvern.utils.env_paths import resolve_backend_env_path
class Skyvern(AsyncSkyvern): class Skyvern(AsyncSkyvern):
@@ -54,10 +54,11 @@ class Skyvern(AsyncSkyvern):
httpx_client=httpx_client, httpx_client=httpx_client,
) )
if base_url is None and api_key is None: if base_url is None and api_key is None:
if not os.path.exists(".env"): env_path = resolve_backend_env_path()
if not env_path.exists():
raise Exception("No .env file found. Please run 'skyvern init' first to set up your environment.") raise Exception("No .env file found. Please run 'skyvern init' first to set up your environment.")
load_dotenv(".env") load_dotenv(env_path)
migrate_db() migrate_db()
self._api_key = api_key self._api_key = api_key

View File

@@ -0,0 +1,46 @@
from pathlib import Path
from typing import Optional
BACKEND_ENV_DEFAULT = ".env"
FRONTEND_DIRNAME = "skyvern-frontend"
FRONTEND_ENV_FILENAME = ".env"
def resolve_backend_env_path(
basename: str = BACKEND_ENV_DEFAULT,
) -> Path:
"""Return the backend env file path inside the current working directory."""
env_path = Path.cwd() / basename
return env_path
def resolve_frontend_env_path() -> Optional[Path]:
"""Return the path to the frontend .env file (may not exist)."""
frontend_root: Optional[Path] = None
if frontend_root is None:
cwd_frontend = Path.cwd() / FRONTEND_DIRNAME
if cwd_frontend.exists() and cwd_frontend.is_dir():
frontend_root = cwd_frontend
if frontend_root is None:
module_based_frontend = Path(__file__).resolve().parent.parent.parent / FRONTEND_DIRNAME
if module_based_frontend.exists() and module_based_frontend.is_dir():
frontend_root = module_based_frontend
if frontend_root is None:
for parent in Path.cwd().parents:
candidate = parent / FRONTEND_DIRNAME
if candidate.exists() and candidate.is_dir():
frontend_root = candidate
break
if frontend_root is None:
return None
env_path = frontend_root / FRONTEND_ENV_FILENAME
return env_path