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