Skip to content
40 changes: 40 additions & 0 deletions client-sdks/manager/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,32 @@
}
},
"/v1/releases": {
"get": {
"tags": [
"releases"
],
"operationId": "list_releases",
"responses": {
"200": {
"description": "Releases listed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListReleasesResponse"
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"bearer": []
}
]
},
"post": {
"tags": [
"releases"
Expand Down Expand Up @@ -9173,6 +9199,20 @@
}
}
},
"ListReleasesResponse": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ReleaseResponse"
}
}
}
},
"LocalArtifactRegistryHeartbeatData": {
"type": "object",
"required": [
Expand Down
40 changes: 40 additions & 0 deletions client-sdks/manager/rust/openapi-3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,32 @@
}
},
"/v1/releases": {
"get": {
"tags": [
"releases"
],
"operationId": "list_releases",
"responses": {
"200": {
"description": "Releases listed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListReleasesResponse"
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"bearer": []
}
]
},
"post": {
"tags": [
"releases"
Expand Down Expand Up @@ -7811,6 +7837,20 @@
}
}
},
"ListReleasesResponse": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ReleaseResponse"
}
}
}
},
"LocalArtifactRegistryHeartbeatData": {
"type": "object",
"required": [
Expand Down
21 changes: 12 additions & 9 deletions crates/alien-cli/src/commands/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ pub enum CommandsAction {

/// Execute commands task — works in all execution modes.
pub async fn commands_task(args: CommandsArgs, ctx: ExecutionMode) -> Result<()> {
let manager = ctx.server_sdk_client()?;
let manager_url = ctx.manager_url();

match args.action {
CommandsAction::Invoke {
deployment,
Expand All @@ -75,11 +72,17 @@ pub async fn commands_task(args: CommandsArgs, ctx: ExecutionMode) -> Result<()>
timeout,
} => {
let is_dev = ctx.is_dev();
let auth_token = ctx.auth_token().unwrap_or_default().to_string();

// Resolve the manager the same way `deployments` does. In platform
// mode this discovers the manager URL and builds a workspace-aware
// client; server_sdk_client() isn't available there.
let (_, project_link) = ctx.resolve_project(None, true).await?;
let manager = ctx.resolve_manager(&project_link.project_id, "aws").await?;

invoke_command(
&manager,
&manager_url,
&auth_token,
&manager.client,
&manager.manager_url,
manager.http_client.clone(),
&deployment,
&command,
&params,
Expand All @@ -100,7 +103,7 @@ pub async fn commands_task_dev(args: CommandsArgs, port: u16) -> Result<()> {
async fn invoke_command(
manager: &alien_manager_api::Client,
manager_url: &str,
auth_token: &str,
http_client: reqwest::Client,
deployment_name: &str,
command: &str,
params_json: &str,
Expand All @@ -125,7 +128,7 @@ async fn invoke_command(
..Default::default()
};
let commands_url = format!("{}/v1", manager_url.trim_end_matches('/'));
let client = CommandsClient::with_config(&commands_url, &deployment_id, auth_token, config);
let client = CommandsClient::with_http_client(&commands_url, &deployment_id, http_client, config);

let result: serde_json::Value = client
.invoke(command, params)
Expand Down
18 changes: 15 additions & 3 deletions crates/alien-cli/src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ pub async fn deploy_task(args: DeployArgs, ctx: ExecutionMode) -> Result<()> {
}
DeploymentToken::DeploymentGroup {
deployment_group_name,
workspace_id,
workspace_name,
project_id,
..
} => {
Expand Down Expand Up @@ -261,7 +261,7 @@ pub async fn deploy_task(args: DeployArgs, ctx: ExecutionMode) -> Result<()> {

let create_response = sdk_client
.create_deployment()
.workspace(&workspace_id)
.workspace(&workspace_name)
.body(alien_platform_api::types::NewDeploymentRequest {
name: args.name.clone().try_into().into_alien_error().context(
ErrorData::ValidationError {
Expand Down Expand Up @@ -366,7 +366,19 @@ pub async fn deploy_task(args: DeployArgs, ctx: ExecutionMode) -> Result<()> {
let manager_ctx = ctx
.resolve_manager(&tracked_deployment.project_id, &args.platform)
.await?;
let manager_client = manager_ctx.client;
// Provisioning calls the manager's sync endpoints, which require
// `managers.sync` — held by the deployment's own token, not the install
// token that resolved the manager. In platform mode (workspace is set),
// re-authenticate as the deployment for these calls.
let manager_client = if let Some(workspace) = manager_ctx.workspace.clone() {
let http_client = crate::auth::client_with_auth_and_workspace(
&format!("Bearer {}", tracked_deployment.api_key),
&workspace,
)?;
alien_manager_api::Client::new_with_client(&manager_ctx.manager_url, http_client)
} else {
manager_ctx.client
};

steps.complete(1, Some(format!("Manager: {}", manager_ctx.manager_url)));

Expand Down
2 changes: 1 addition & 1 deletion crates/alien-cli/src/commands/deployments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ pub async fn deployments_task(args: DeploymentsArgs, ctx: ExecutionMode) -> Resu
///
/// Uses the linked project (or `--project` override) to discover the manager URL
/// in platform mode. In dev/standalone modes, the manager URL is known directly.
async fn resolve_manager_client(
pub(crate) async fn resolve_manager_client(
ctx: &ExecutionMode,
project_override: Option<&str>,
) -> Result<alien_manager_api::Client> {
Expand Down
2 changes: 2 additions & 0 deletions crates/alien-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod dev_helpers;
pub mod init;
pub mod onboard;
pub mod release;
pub mod releases;
pub mod render;
pub mod vault;
pub mod whoami;
Expand Down Expand Up @@ -37,6 +38,7 @@ pub use dev_helpers::{
pub use init::{init_task, InitArgs};
pub use onboard::{onboard_task, OnboardArgs};
pub use release::{release_command, ReleaseArgs};
pub use releases::{releases_task, ReleasesArgs};
pub use render::{render_task, RenderArgs};
pub use vault::{vault_remote_task, vault_task, VaultArgs, VaultRemoteArgs};
pub use whoami::{whoami_task, WhoamiArgs};
103 changes: 103 additions & 0 deletions crates/alien-cli/src/commands/releases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! CLI command for listing releases.

use crate::error::{ErrorData, Result};
use crate::execution_context::ExecutionMode;
use crate::ui::{make_table, print_table};
use alien_error::Context;
use alien_manager_api::types::{ReleaseResponse, StackByPlatform};
use alien_manager_api::SdkResultExt as _;
use clap::{Parser, Subcommand};

#[derive(Parser, Debug, Clone)]
#[command(about = "List releases")]
pub struct ReleasesArgs {
#[command(subcommand)]
pub cmd: ReleasesCmd,
}

#[derive(Subcommand, Debug, Clone)]
pub enum ReleasesCmd {
/// List releases, newest first
Ls {
/// Project to list releases for (optional, uses linked project by default)
#[arg(long)]
project: Option<String>,
},
}

pub async fn releases_task(args: ReleasesArgs, ctx: ExecutionMode) -> Result<()> {
ctx.ensure_ready().await?;

match args.cmd {
ReleasesCmd::Ls { project } => {
// Releases are a core feature, so they go through the manager, not
// the platform API directly.
let manager =
crate::commands::deployments::resolve_manager_client(&ctx, project.as_deref())
.await?;
list_releases_task(&manager).await
}
}
}

async fn list_releases_task(client: &alien_manager_api::Client) -> Result<()> {
let response = client
.list_releases()
.send()
.await
.into_sdk_error()
.context(ErrorData::ApiRequestFailed {
message: "listing releases".to_string(),
url: None,
})?
.into_inner();

if response.items.is_empty() {
println!("(no releases)");
return Ok(());
}

let mut table = make_table(&["ID", "Created", "Commit", "Platforms"]);
for release in &response.items {
table.add_row(vec![
release.id.clone().into(),
release.created_at.clone().into(),
commit_cell(release),
platforms_cell(&release.stack),
]);
}
print_table(table);

Ok(())
}

/// A branch/tag ref reads better than a bare SHA, so prefer it.
fn commit_cell(release: &ReleaseResponse) -> comfy_table::Cell {
let label = release
.git_metadata
.as_ref()
.and_then(|g| g.commit_ref.clone().or_else(|| g.commit_sha.clone()))
.unwrap_or_else(|| "—".to_string());
comfy_table::Cell::new(label)
}

fn platforms_cell(stack: &StackByPlatform) -> comfy_table::Cell {
let names: Vec<&str> = [
("aws", stack.aws.is_some()),
("gcp", stack.gcp.is_some()),
("azure", stack.azure.is_some()),
("kubernetes", stack.kubernetes.is_some()),
("local", stack.local.is_some()),
("test", stack.test.is_some()),
]
.into_iter()
.filter_map(|(name, present)| present.then_some(name))
.collect();

let label = if names.is_empty() {
"—".to_string()
} else {
names.join(", ")
};
comfy_table::Cell::new(label)
}
18 changes: 12 additions & 6 deletions crates/alien-cli/src/deployment_tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub enum DeploymentToken {
deployment_group_id: String,
deployment_group_name: String,
project_id: String,
workspace_id: String,
workspace_name: String,
max_deployments: u32,
},
}
Expand Down Expand Up @@ -187,10 +187,16 @@ pub async fn validate_token(api_key: &str, base_url: &str) -> Result<DeploymentT
deployment_group_id,
project_id: _,
} => {
// Fetch deployment group details
// The group lookup keys on workspace name, not id (whoami provides the name).
let workspace_name = sa.workspace_name.clone().ok_or_else(|| {
AlienError::new(ErrorData::ValidationError {
field: "workspace".to_string(),
message: "token response is missing the workspace name".to_string(),
})
})?;
let deployment_group = fetch_deployment_group(
&deployment_group_id,
&sa.workspace_id,
&workspace_name,
api_key,
base_url,
)
Expand All @@ -200,7 +206,7 @@ pub async fn validate_token(api_key: &str, base_url: &str) -> Result<DeploymentT
deployment_group_id: deployment_group.id.to_string(),
deployment_group_name: deployment_group.name.to_string(),
project_id: deployment_group.project_id.to_string(),
workspace_id: sa.workspace_id,
workspace_name,
max_deployments: deployment_group.max_deployments.get() as u32,
})
}
Expand All @@ -221,7 +227,7 @@ pub async fn validate_token(api_key: &str, base_url: &str) -> Result<DeploymentT
/// Fetch deployment group details from the API
async fn fetch_deployment_group(
deployment_group_id: &str,
workspace_id: &str,
workspace_name: &str,
api_key: &str,
base_url: &str,
) -> Result<alien_platform_api::types::GetDeploymentGroupResponse> {
Expand Down Expand Up @@ -254,7 +260,7 @@ async fn fetch_deployment_group(
let response = sdk_client
.get_deployment_group()
.id(deployment_group_id)
.workspace(workspace_id)
.workspace(workspace_name)
.send()
.await
.into_alien_error()
Expand Down
Loading
Loading