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