Skip to main content

systemprompt_cli/commands/cloud/deploy/
mod.rs

1//! `cloud deploy` command: prompts, preflight, and progress rendering.
2//!
3//! Resolves the active cloud profile and tenant, runs the `cloud doctor`
4//! preflight (deploys hard-block on a failing report), then hands the typed
5//! request to [`DeployOrchestrator`] in `systemprompt-sync`, which owns the
6//! pre-deploy sync, image build/push, secret provisioning, and the deploy
7//! call. Rendering flows back through [`CliDeployProgress`].
8
9pub mod progress;
10mod select;
11
12pub(in crate::commands::cloud) use progress::CliDeployProgress;
13pub(in crate::commands::cloud) use select::resolve_profile;
14
15use anyhow::{Context, Result, anyhow, bail};
16use systemprompt_cloud::{CloudPath, ProfilePath, TenantStore, get_cloud_paths};
17use systemprompt_identifiers::TenantId;
18use systemprompt_logging::CliService;
19use systemprompt_sync::deploy::{
20    DeployOptions, DeployOrchestrator, DeployOutcome, DeployRequest, PreSyncOptions,
21};
22
23use super::tenant::get_credentials;
24use crate::cli_settings::CliConfig;
25use crate::interactive::Prompter;
26use crate::shared::project::ProjectRoot;
27
28pub(super) struct DeployArgs {
29    pub skip_push: bool,
30    pub profile_name: Option<String>,
31    pub no_sync: bool,
32    pub yes: bool,
33    pub dry_run: bool,
34    pub check: bool,
35}
36
37pub(super) async fn execute(
38    args: DeployArgs,
39    prompter: &dyn Prompter,
40    config: &CliConfig,
41) -> Result<()> {
42    CliService::section("systemprompt.io Cloud Deploy");
43
44    let (profile, profile_path) = resolve_profile(prompter, args.profile_name.as_deref(), config)?;
45
46    if profile.target != systemprompt_models::ProfileType::Cloud {
47        bail!(
48            "Cannot deploy a local profile. Create a cloud profile with: systemprompt cloud \
49             profile create <name>"
50        );
51    }
52
53    let profile_dir = profile_path
54        .parent()
55        .ok_or_else(|| anyhow!("Invalid profile path"))?;
56    let report = super::doctor::run(&profile, profile_dir).await;
57    report.render();
58    if report.has_blocking() {
59        bail!("Deploy preflight failed — fix the items above before deploying.");
60    }
61    if args.check {
62        CliService::success("Deploy preflight passed (--check; nothing deployed)");
63        return Ok(());
64    }
65
66    let target = resolve_deploy_target(&profile)?;
67    let project = ProjectRoot::discover().map_err(|e| anyhow!("{}", e))?;
68
69    let request = DeployRequest {
70        tenant_id: target.tenant_id,
71        tenant_name: target.tenant_name,
72        profile_name: profile.name.clone(),
73        project_root: project.as_path().to_path_buf(),
74        credentials: target.creds,
75        hostname: target.hostname,
76        secrets_path: ProfilePath::Secrets.resolve(profile_dir),
77        signing_key_path: super::doctor::resolve_signing_key_path(&profile, profile_dir),
78        options: DeployOptions {
79            skip_push: args.skip_push,
80            dry_run: args.dry_run,
81            pre_sync: Some(PreSyncOptions {
82                no_sync: args.no_sync,
83                assume_yes: args.yes,
84            }),
85        },
86    };
87
88    let progress = CliDeployProgress::new(prompter, config);
89    let report = DeployOrchestrator::new()
90        .deploy(&request, &progress)
91        .await?;
92
93    if matches!(report.outcome, DeployOutcome::DryRun) {
94        CliService::info("Dry run complete. No deployment performed.");
95    }
96
97    Ok(())
98}
99
100struct DeployTarget {
101    tenant_id: TenantId,
102    tenant_name: String,
103    hostname: Option<String>,
104    creds: systemprompt_cloud::CloudCredentials,
105}
106
107fn resolve_deploy_target(profile: &systemprompt_models::Profile) -> Result<DeployTarget> {
108    let cloud_config = profile
109        .cloud
110        .as_ref()
111        .ok_or_else(|| anyhow!("No cloud configuration in profile"))?;
112
113    let tenant_id = cloud_config
114        .tenant_id
115        .as_ref()
116        .map(TenantId::new)
117        .ok_or_else(|| anyhow!("No tenant configured. Run 'systemprompt cloud config'"))?;
118
119    let creds = get_credentials()?;
120    if creds.is_token_expired() {
121        bail!("Token expired. Run 'systemprompt cloud login' to refresh.");
122    }
123
124    let cloud_paths = get_cloud_paths();
125    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
126    let tenant_store = TenantStore::load_from_path(&tenants_path)
127        .context("Tenants not synced. Run 'systemprompt cloud login'")?;
128
129    let tenant = tenant_store.find_tenant(&tenant_id).ok_or_else(|| {
130        anyhow!(
131            "Tenant {} not found. Run 'systemprompt cloud login'",
132            tenant_id
133        )
134    })?;
135
136    Ok(DeployTarget {
137        tenant_id,
138        tenant_name: tenant.name.clone(),
139        hostname: tenant.hostname.clone(),
140        creds,
141    })
142}