flowstash-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowstash_cli-0.1.0/PKG-INFO +19 -0
- flowstash_cli-0.1.0/flowstash/__init__.py +0 -0
- flowstash_cli-0.1.0/flowstash/commands/__init__.py +0 -0
- flowstash_cli-0.1.0/flowstash/commands/auth.py +112 -0
- flowstash_cli-0.1.0/flowstash/commands/build.py +127 -0
- flowstash_cli-0.1.0/flowstash/commands/deploy.py +143 -0
- flowstash_cli-0.1.0/flowstash/commands/project.py +555 -0
- flowstash_cli-0.1.0/flowstash/commands/run.py +65 -0
- flowstash_cli-0.1.0/flowstash/core/__init__.py +0 -0
- flowstash_cli-0.1.0/flowstash/core/api_client.py +62 -0
- flowstash_cli-0.1.0/flowstash/core/auth_server.py +45 -0
- flowstash_cli-0.1.0/flowstash/core/builder.py +40 -0
- flowstash_cli-0.1.0/flowstash/core/config.py +81 -0
- flowstash_cli-0.1.0/flowstash/core/docker_utils.py +33 -0
- flowstash_cli-0.1.0/flowstash/main.py +269 -0
- flowstash_cli-0.1.0/flowstash/templates/AGENTS.md +222 -0
- flowstash_cli-0.1.0/flowstash/templates/README.md +135 -0
- flowstash_cli-0.1.0/flowstash/templates/_.dockerignore +8 -0
- flowstash_cli-0.1.0/flowstash/templates/_.flowstash +4 -0
- flowstash_cli-0.1.0/flowstash/templates/_api_main.py +21 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/[env]/_backend.yaml +4 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/shared/.env +1 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/shared/backend.yaml +4 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/shared/clients/demoClient.yaml +39 -0
- flowstash_cli-0.1.0/flowstash/templates/_config/shared/clients.yaml +3 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +24 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/[env]/.env +3 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/shared/.env +5 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/shared/api.Dockerfile +18 -0
- flowstash_cli-0.1.0/flowstash/templates/_deployment/shared/worker.Dockerfile +18 -0
- flowstash_cli-0.1.0/flowstash/templates/_pyproject.toml +40 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_api/__init__.py +1 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_api/_routes/webhooks.py +25 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_shared/__init__.py +1 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_shared/clients/client.py +18 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_shared/models/models.py +1 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_shared/tasks/sharedTasks.py +10 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_worker/__init__.py +1 -0
- flowstash_cli-0.1.0/flowstash/templates/_src/_worker/tasks/tasks.py +15 -0
- flowstash_cli-0.1.0/flowstash/templates/_worker_main.py +25 -0
- flowstash_cli-0.1.0/flowstash/ui/__init__.py +0 -0
- flowstash_cli-0.1.0/pyproject.toml +31 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flowstash-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the flowstash Managed Platform
|
|
5
|
+
Author: juraj.bezdek@gmail.com
|
|
6
|
+
Author-email: juraj.bezdek@gmail.com
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: flowstash-runtime (>=0.3.0,<0.4.0)
|
|
13
|
+
Requires-Dist: httpx (>=0.27.0)
|
|
14
|
+
Requires-Dist: keyring (>=25.0.0)
|
|
15
|
+
Requires-Dist: pydantic (>=2.0.0)
|
|
16
|
+
Requires-Dist: pyyaml (>=6.0.1)
|
|
17
|
+
Requires-Dist: questionary (>=2.0.1)
|
|
18
|
+
Requires-Dist: rich (>=13.7.0)
|
|
19
|
+
Requires-Dist: typer[all] (>=0.12.0)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import typer
|
|
3
|
+
import webbrowser
|
|
4
|
+
import uuid
|
|
5
|
+
import secrets
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
import asyncio
|
|
8
|
+
import httpx
|
|
9
|
+
from ..core.auth_server import start_callback_server
|
|
10
|
+
from ..core.config import (
|
|
11
|
+
load_global_config,
|
|
12
|
+
set_access_token,
|
|
13
|
+
get_access_token,
|
|
14
|
+
delete_access_token,
|
|
15
|
+
load_project_config
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app = typer.Typer()
|
|
19
|
+
console = Console()
|
|
20
|
+
import os
|
|
21
|
+
API_URL = os.getenv("flowstash_API_URL", "https://api.flowstash.dev")
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def login(
|
|
25
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="Username for manual login"),
|
|
26
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Password for manual login")
|
|
27
|
+
):
|
|
28
|
+
"""Log in to the flowstash Managed Platform."""
|
|
29
|
+
if username and password:
|
|
30
|
+
# Manual login
|
|
31
|
+
console.print(f"Logging in to {API_URL} as {username}...")
|
|
32
|
+
|
|
33
|
+
async def do_login():
|
|
34
|
+
async with httpx.AsyncClient() as client:
|
|
35
|
+
try:
|
|
36
|
+
resp = await client.post(
|
|
37
|
+
f"{API_URL}/v1/auth/login",
|
|
38
|
+
json={"email": username, "password": password}
|
|
39
|
+
)
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
return resp.json()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
console.print(f"[red]Login failed: {e}[/red]")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
result = asyncio.run(do_login())
|
|
47
|
+
if not result:
|
|
48
|
+
raise typer.Exit(code=1)
|
|
49
|
+
|
|
50
|
+
access_token = result.get("access_token")
|
|
51
|
+
tenant_id = result.get("tenant_id")
|
|
52
|
+
|
|
53
|
+
set_access_token(access_token)
|
|
54
|
+
console.print(f"[green]Successfully logged in as tenant: {tenant_id}[/green]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Browser login
|
|
58
|
+
state = secrets.token_urlsafe(16)
|
|
59
|
+
port = 8888
|
|
60
|
+
callback_url = f"http://localhost:{port}/callback"
|
|
61
|
+
|
|
62
|
+
login_url = f"{API_URL}/cli-login?redirect_uri={callback_url}&state={state}"
|
|
63
|
+
|
|
64
|
+
console.print(f"Opening your browser to authenticate...")
|
|
65
|
+
console.print(f"If the browser doesn't open, visit: [link={login_url}]{login_url}[/link]")
|
|
66
|
+
|
|
67
|
+
webbrowser.open(login_url)
|
|
68
|
+
|
|
69
|
+
console.print("Waiting for authentication...")
|
|
70
|
+
result = start_callback_server(port)
|
|
71
|
+
|
|
72
|
+
if not result:
|
|
73
|
+
console.print("[red]Authentication timed out or failed.[/red]")
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
|
|
76
|
+
if result.get("state") != state:
|
|
77
|
+
console.print("[red]Invalid state received. Auth session might be compromised.[/red]")
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
80
|
+
access_token = result.get("access_token")
|
|
81
|
+
tenant_id = result.get("tenant_id")
|
|
82
|
+
|
|
83
|
+
if not access_token:
|
|
84
|
+
console.print("[red]No access token received.[/red]")
|
|
85
|
+
raise typer.Exit(code=1)
|
|
86
|
+
|
|
87
|
+
set_access_token(access_token)
|
|
88
|
+
|
|
89
|
+
console.print(f"[green]Successfully logged in as tenant: {tenant_id}[/green]")
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def logout():
|
|
93
|
+
"""Log out from the flowstash Managed Platform."""
|
|
94
|
+
delete_access_token()
|
|
95
|
+
console.print("[yellow]Logged out successfully.[/yellow]")
|
|
96
|
+
|
|
97
|
+
@app.command()
|
|
98
|
+
def whoami():
|
|
99
|
+
"""Show current login status."""
|
|
100
|
+
token = get_access_token()
|
|
101
|
+
global_config = load_global_config()
|
|
102
|
+
project_config = load_project_config()
|
|
103
|
+
|
|
104
|
+
if token:
|
|
105
|
+
console.print(f"API URL: {global_config.api_url}")
|
|
106
|
+
if project_config:
|
|
107
|
+
console.print(f"Logged in as tenant: [bold]{project_config.tenant_id}[/bold]")
|
|
108
|
+
console.print(f"Current Project: [bold]{project_config.project_id}[/bold]")
|
|
109
|
+
else:
|
|
110
|
+
console.print("Logged in, but no project context found in this directory.")
|
|
111
|
+
else:
|
|
112
|
+
console.print("Not logged in.")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import asyncio
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
7
|
+
from ..core.api_client import APIClient
|
|
8
|
+
from ..core.config import load_project_config
|
|
9
|
+
from ..core.builder import bundle_source
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
from .project import find_project_root
|
|
17
|
+
from ..core.docker_utils import check_docker_binary, check_docker_daemon, get_docker_compose_cmd
|
|
18
|
+
|
|
19
|
+
async def run_managed_build(tag: str = "latest"):
|
|
20
|
+
project_config = load_project_config()
|
|
21
|
+
if not project_config or not project_config.project_id:
|
|
22
|
+
console.print("[red]Project not linked. Run 'flowstash init' to link to a managed project.[/red]")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
|
|
25
|
+
api = APIClient()
|
|
26
|
+
# ... rest of existing managed build logic ...
|
|
27
|
+
# (I'll keep the existing implementation but wrap it)
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def build(
|
|
31
|
+
env: str = typer.Argument(..., help="Environment to build"),
|
|
32
|
+
tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
|
|
33
|
+
):
|
|
34
|
+
"""Build project artifacts/images for the specified environment."""
|
|
35
|
+
project_config = load_project_config()
|
|
36
|
+
|
|
37
|
+
# Check if env is managed
|
|
38
|
+
is_managed = False
|
|
39
|
+
if project_config:
|
|
40
|
+
for em in project_config.environments:
|
|
41
|
+
if em.name == env:
|
|
42
|
+
is_managed = em.managed
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if is_managed:
|
|
46
|
+
result = asyncio.run(run_build_flow(tag))
|
|
47
|
+
console.print(f"[green]Managed build completed successfully![/green]")
|
|
48
|
+
console.print(f"Artifact ID: [bold]{result['artifact_id']}[/bold]")
|
|
49
|
+
else:
|
|
50
|
+
# Local build
|
|
51
|
+
root = find_project_root()
|
|
52
|
+
if not root:
|
|
53
|
+
console.print("[red]Not in a flowstash project.[/red]")
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
|
|
56
|
+
if not check_docker_binary():
|
|
57
|
+
console.print("[red]Docker not found.[/red]")
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
|
|
60
|
+
compose_file = root / "deployment" / env / "docker-compose.yaml"
|
|
61
|
+
if not compose_file.exists():
|
|
62
|
+
console.print(f"[red]No docker-compose.yaml found for env '{env}' at {compose_file}[/red]")
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
|
|
65
|
+
cmd = get_docker_compose_cmd() + ["-f", str(compose_file), "build"]
|
|
66
|
+
console.print(f"Building images for [bold]{env}[/bold]...")
|
|
67
|
+
try:
|
|
68
|
+
subprocess.run(cmd, check=True, cwd=root)
|
|
69
|
+
console.print(f"[green]Local build for '{env}' complete![/green]")
|
|
70
|
+
except subprocess.CalledProcessError:
|
|
71
|
+
console.print("[red]Local build failed.[/red]")
|
|
72
|
+
raise typer.Exit(code=1)
|
|
73
|
+
|
|
74
|
+
async def run_build_flow(tag: str = "latest"):
|
|
75
|
+
# (Moved existing run_build_flow logic here for completeness in the file)
|
|
76
|
+
project_config = load_project_config()
|
|
77
|
+
project_id = project_config.project_id
|
|
78
|
+
api = APIClient()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with Progress(
|
|
82
|
+
SpinnerColumn(),
|
|
83
|
+
TextColumn("[progress.description]{task.description}"),
|
|
84
|
+
) as progress:
|
|
85
|
+
task = progress.add_task(description="Bundling source code...", total=None)
|
|
86
|
+
tar_path = bundle_source(Path.cwd())
|
|
87
|
+
progress.update(task, description="Source bundled.")
|
|
88
|
+
|
|
89
|
+
task = progress.add_task(description="Requesting upload path...", total=None)
|
|
90
|
+
upload_data = await api.get("/v1/builds/upload-path")
|
|
91
|
+
build_id = upload_data["build_id"]
|
|
92
|
+
upload_url = upload_data["upload_url"]
|
|
93
|
+
progress.update(task, description="Upload path received.")
|
|
94
|
+
|
|
95
|
+
task = progress.add_task(description="Uploading source...", total=None)
|
|
96
|
+
with open(tar_path, "rb") as f:
|
|
97
|
+
data = f.read()
|
|
98
|
+
await api.put_binary(upload_url, data, headers={"Content-Type": "application/gzip"})
|
|
99
|
+
progress.update(task, description="Source uploaded.")
|
|
100
|
+
|
|
101
|
+
task = progress.add_task(description="Triggering build...", total=None)
|
|
102
|
+
trigger_resp = await api.post(f"/v1/builds/{build_id}/trigger", json={"image_tag": tag})
|
|
103
|
+
|
|
104
|
+
# update build_id to the triggered true GCP build ID
|
|
105
|
+
build_id = trigger_resp["build_id"]
|
|
106
|
+
progress.update(task, description=f"Build triggered (ID: {build_id}).")
|
|
107
|
+
|
|
108
|
+
task = progress.add_task(description="Building...", total=None)
|
|
109
|
+
while True:
|
|
110
|
+
status_data = await api.get(f"/v1/builds/{build_id}/status")
|
|
111
|
+
status = status_data["status"]
|
|
112
|
+
if status == "SUCCESS":
|
|
113
|
+
progress.update(task, description="Build successful!")
|
|
114
|
+
return status_data
|
|
115
|
+
elif status in ["FAILURE", "INTERNAL_ERROR", "TIMEOUT", "CANCELLED"]:
|
|
116
|
+
progress.update(task, description=f"[red]Build failed: {status}[/red]")
|
|
117
|
+
console.print(f"[red]Build ended with status: {status}[/red]")
|
|
118
|
+
log_url = status_data.get('log_url')
|
|
119
|
+
if log_url:
|
|
120
|
+
console.print(f"Check logs here: [link={log_url}]{log_url}[/link]")
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
await asyncio.sleep(5)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if isinstance(e, typer.Exit):
|
|
125
|
+
raise
|
|
126
|
+
console.print(f"[red]Error during build: {e}[/red]")
|
|
127
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import asyncio
|
|
3
|
+
import typer
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
7
|
+
from ..core.api_client import APIClient
|
|
8
|
+
from ..core.config import load_project_config
|
|
9
|
+
from .build import run_build_flow
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
# Status labels shown to the user while polling
|
|
15
|
+
_STATUS_LABELS = {
|
|
16
|
+
"QUEUED": "Queued, waiting for deployment to start...",
|
|
17
|
+
"DEPLOYING": "Deploying services to Cloud Run...",
|
|
18
|
+
"HEALTH_CHECK": "Health-checking API and Worker...",
|
|
19
|
+
"SYNCING_SCHEDULES": "Fetching and syncing scheduled tasks...",
|
|
20
|
+
"DEPLOYED": "Deployed successfully ✓",
|
|
21
|
+
"FAILED": "Deployment failed.",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_TERMINAL_STATUSES = {"DEPLOYED", "FAILED"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run_deploy_flow(env: str, artifact_id: Optional[str] = None):
|
|
28
|
+
project_config = load_project_config()
|
|
29
|
+
if not project_config:
|
|
30
|
+
console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
project_id = project_config.project_id
|
|
34
|
+
if not project_id:
|
|
35
|
+
console.print("[red]project_id not found in .flowstash[/red]")
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
|
|
38
|
+
api = APIClient()
|
|
39
|
+
|
|
40
|
+
# 1. If artifact_id is not provided, run build first
|
|
41
|
+
if not artifact_id:
|
|
42
|
+
console.print("No artifact ID provided. Building first...")
|
|
43
|
+
build_result = await run_build_flow()
|
|
44
|
+
artifact_id = build_result["artifact_id"]
|
|
45
|
+
console.print(f"Build finished. Deploying artifact: [bold]{artifact_id}[/bold]")
|
|
46
|
+
|
|
47
|
+
with Progress(
|
|
48
|
+
SpinnerColumn(),
|
|
49
|
+
TextColumn("[progress.description]{task.description}"),
|
|
50
|
+
transient=True,
|
|
51
|
+
) as progress:
|
|
52
|
+
# 2. Trigger deployment
|
|
53
|
+
task = progress.add_task(description="Triggering deployment...", total=None)
|
|
54
|
+
deploy_data = await api.post("/v1/deploy", json={
|
|
55
|
+
"project_id": project_id,
|
|
56
|
+
"artifact_id": artifact_id,
|
|
57
|
+
"env_vars": {"ENVIRONMENT": env}
|
|
58
|
+
})
|
|
59
|
+
deploy_id = deploy_data["deploy_id"]
|
|
60
|
+
progress.update(task, description=f"Deployment triggered (ID: {deploy_id}).")
|
|
61
|
+
|
|
62
|
+
# 3. Poll status until terminal
|
|
63
|
+
last_status = None
|
|
64
|
+
while True:
|
|
65
|
+
status_data = await api.get(f"/v1/deploy/{deploy_id}/status")
|
|
66
|
+
current_status = status_data["status"]
|
|
67
|
+
|
|
68
|
+
if current_status != last_status:
|
|
69
|
+
label = _STATUS_LABELS.get(current_status, f"Status: {current_status}")
|
|
70
|
+
progress.update(task, description=label)
|
|
71
|
+
last_status = current_status
|
|
72
|
+
|
|
73
|
+
if current_status == "DEPLOYED":
|
|
74
|
+
break
|
|
75
|
+
elif current_status == "FAILED":
|
|
76
|
+
error = status_data.get("error_message", "Unknown error")
|
|
77
|
+
console.print(f"[red]Deployment failed: {error}[/red]")
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
80
|
+
await asyncio.sleep(5)
|
|
81
|
+
|
|
82
|
+
return status_data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command()
|
|
86
|
+
def deploy(
|
|
87
|
+
env: str = typer.Argument(..., help="Environment to deploy to"),
|
|
88
|
+
artifact: Optional[str] = typer.Option(None, "--artifact", "-a", help="Artifact ID to deploy"),
|
|
89
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Do not ask for confirmation")
|
|
90
|
+
):
|
|
91
|
+
"""Deploy an artifact to the managed platform for a specified environment."""
|
|
92
|
+
project_config = load_project_config()
|
|
93
|
+
if not project_config:
|
|
94
|
+
console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
|
|
95
|
+
raise typer.Exit(code=1)
|
|
96
|
+
|
|
97
|
+
# Ask for confirmation unless non-interactive is provided
|
|
98
|
+
if not non_interactive:
|
|
99
|
+
from rich.prompt import Confirm
|
|
100
|
+
if not Confirm.ask(f"Are you sure you want to deploy to '{env}'?"):
|
|
101
|
+
console.print("Deployment cancelled.")
|
|
102
|
+
raise typer.Exit(code=0)
|
|
103
|
+
|
|
104
|
+
# Check if env is managed
|
|
105
|
+
is_managed = False
|
|
106
|
+
for em in project_config.environments:
|
|
107
|
+
if em.name == env:
|
|
108
|
+
is_managed = em.managed
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if not is_managed:
|
|
112
|
+
console.print(f"[red]Environment '{env}' is not managed. Deployment is only supported for managed environments.[/red]")
|
|
113
|
+
console.print("[yellow]Update your .flowstash environments if this is incorrect.[/yellow]")
|
|
114
|
+
raise typer.Exit(code=1)
|
|
115
|
+
|
|
116
|
+
project_id = project_config.project_id
|
|
117
|
+
if not project_id:
|
|
118
|
+
if not non_interactive:
|
|
119
|
+
from rich.prompt import Confirm
|
|
120
|
+
if Confirm.ask("Project ID not found. Would you like to link to a managed project now?"):
|
|
121
|
+
from .project import _link_project
|
|
122
|
+
_link_project(project_config)
|
|
123
|
+
project_id = project_config.project_id
|
|
124
|
+
|
|
125
|
+
if not project_id:
|
|
126
|
+
console.print("[red]project_id not found in .flowstash. Use 'flowstash init' or link to a project.[/red]")
|
|
127
|
+
raise typer.Exit(code=1)
|
|
128
|
+
|
|
129
|
+
result = asyncio.run(run_deploy_flow(env, artifact))
|
|
130
|
+
|
|
131
|
+
api_url = result.get("api_url", "")
|
|
132
|
+
console.print(f"[green]✓ Deployment complete![/green]")
|
|
133
|
+
console.print(f" Deployment ID : [bold]{result['deploy_id']}[/bold]")
|
|
134
|
+
if api_url:
|
|
135
|
+
console.print(f" API URL : [bold]{api_url}[/bold]")
|
|
136
|
+
|
|
137
|
+
scheduled_tasks = result.get("scheduled_tasks")
|
|
138
|
+
if scheduled_tasks:
|
|
139
|
+
console.print(" Scheduled Tasks:")
|
|
140
|
+
for task in scheduled_tasks:
|
|
141
|
+
console.print(f" - [cyan]{task['task_name']}[/cyan] : [yellow]{task['cron']}[/yellow]")
|
|
142
|
+
elif scheduled_tasks is not None:
|
|
143
|
+
console.print(" Scheduled Tasks: [dim]None[/dim]")
|