feat: add plan step

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-15 22:38:18 +01:00
parent 7eb6ae7cbb
commit 04e452ecc3
71 changed files with 1059 additions and 319 deletions

View File

@@ -520,6 +520,7 @@ pub fn format_pipeline_blocks(
"RUNNING" => ":arrows_counterclockwise:",
"FAILED" => ":x:",
"CANCELLED" => ":no_entry_sign:",
"AWAITING_APPROVAL" => ":shield:",
_ => ":radio_button:", // PENDING
};
@@ -546,6 +547,16 @@ pub fn format_pipeline_blocks(
_ => format!("Wait {dur_str}"),
}
}
"plan" => {
let env = stage.environment.as_deref().unwrap_or("unknown");
match stage.status.as_str() {
"SUCCEEDED" => format!("Plan approved for `{env}`"),
"RUNNING" => format!("Planning `{env}`"),
"AWAITING_APPROVAL" => format!("Awaiting plan approval for `{env}`"),
"FAILED" => format!("Plan failed for `{env}`"),
_ => format!("Plan `{env}`"),
}
}
_ => format!("Stage {}", stage.stage_id),
};
@@ -928,6 +939,8 @@ mod tests {
error_message: None,
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
},
PipelineRunStageState {
stage_id: "s2".into(),
@@ -942,6 +955,8 @@ mod tests {
error_message: None,
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
},
PipelineRunStageState {
stage_id: "s3".into(),
@@ -956,6 +971,8 @@ mod tests {
error_message: None,
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
},
PipelineRunStageState {
stage_id: "s4".into(),
@@ -970,6 +987,8 @@ mod tests {
error_message: None,
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
},
PipelineRunStageState {
stage_id: "s5".into(),
@@ -984,6 +1003,8 @@ mod tests {
error_message: None,
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
},
];
@@ -1024,6 +1045,8 @@ mod tests {
error_message: Some("OOM killed".into()),
wait_until: None,
release_ids: vec![],
approval_status: None,
auto_approve: None,
}];
let blocks = format_pipeline_blocks(&stages);

View File

@@ -130,8 +130,8 @@ pub struct DestinationState {
pub struct PipelineRunStageState {
pub stage_id: String,
pub depends_on: Vec<String>,
pub stage_type: String, // "deploy" or "wait"
pub status: String, // "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"
pub stage_type: String, // "deploy", "wait", or "plan"
pub status: String, // "PENDING", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "AWAITING_APPROVAL"
pub environment: Option<String>,
pub duration_seconds: Option<i64>,
pub queued_at: Option<String>,
@@ -141,6 +141,10 @@ pub struct PipelineRunStageState {
pub wait_until: Option<String>,
#[serde(default)]
pub release_ids: Vec<String>,
#[serde(default)]
pub approval_status: Option<String>,
#[serde(default)]
pub auto_approve: Option<bool>,
}
/// Combined response from get_destination_states: destinations only.
@@ -302,6 +306,7 @@ pub struct PipelineStage {
pub enum PipelineStageConfig {
Deploy { environment: String },
Wait { duration_seconds: i64 },
Plan { environment: String, auto_approve: bool },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -628,6 +633,43 @@ pub trait ForestPlatform: Send + Sync {
release_intent_id: &str,
target_environment: &str,
) -> Result<ApprovalState, PlatformError>;
async fn approve_plan_stage(
&self,
access_token: &str,
release_intent_id: &str,
stage_id: &str,
) -> Result<(), PlatformError>;
async fn reject_plan_stage(
&self,
access_token: &str,
release_intent_id: &str,
stage_id: &str,
reason: Option<&str>,
) -> Result<(), PlatformError>;
async fn get_plan_output(
&self,
access_token: &str,
release_intent_id: &str,
stage_id: &str,
) -> Result<PlanOutput, PlatformError>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanOutput {
pub plan_output: String,
pub status: String,
pub outputs: Vec<PlanDestinationOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanDestinationOutput {
pub destination_id: String,
pub destination_name: String,
pub plan_output: String,
pub status: String,
}
#[cfg(test)]