Skip to content

Commit 5fd6991

Browse files
committed
Merge #305: refactor: improve release handler code organization and reduce duplication
45588ce chore: remove release handler analysis document (Jose Celano) 629bed5 refactor: move service availability checks to release() functions (Jose Celano) fabe399 refactor: remove instance_ip parameter from release workflow (Jose Celano) da242e3 refactor: improve workflow module readability (Jose Celano) 806ea87 refactor: extract release workflow to dedicated module (Jose Celano) c031138 refactor: extract release steps into service-specific modules (Jose Celano) d5f95e1 refactor: normalize error types to include source for error chaining (Jose Celano) f87fa2c refactor: convert unused self methods to associated functions (Jose Celano) a7b2a17 refactor: remove unused instance_ip parameters from step methods (Jose Celano) 36dd723 refactor: extract AnsibleClient construction helper (Jose Celano) 8330d50 refactor: group release workflow steps by service (Jose Celano) c724492 docs: add code quality analysis for release handler (Jose Celano) Pull request description: ## Summary This PR refactors the release command handler to improve code organization, reduce duplication, and enhance maintainability. ## Changes ### Module Reorganization - **Extract service-specific steps into dedicated modules** under `steps/`: - `tracker.rs` - Tracker service release steps - `prometheus.rs` - Prometheus service release steps - `grafana.rs` - Grafana service release steps - `mysql.rs` - MySQL service release steps - `caddy.rs` - Caddy service release steps - `compose.rs` - Docker Compose deployment steps - `common.rs` - Shared utilities (ansible client factory) ### Workflow Extraction - **Extract release workflow to dedicated module** (`workflow.rs`) - Separates orchestration logic from handler state management ### Code Quality Improvements - **Remove `instance_ip` parameter from workflow** - The IP is now obtained from the environment object within each step, reducing coupling - **Move service availability checks to `release()` functions** - Eliminates repetitive conditional checks in every step function (~90 lines removed) - **Improve workflow module readability** - Use explicit imports and remove redundant comments ## Benefits 1. **Better separation of concerns**: Each service has its own module with clear responsibilities 2. **Reduced duplication**: Service availability checks now happen once per service, not per step 3. **Improved testability**: Smaller, focused modules are easier to test in isolation 4. **Enhanced maintainability**: Changes to one service don't affect others 5. **Clearer control flow**: The main `release()` function in each module shows the complete service workflow ## Testing - All pre-commit checks pass - E2E tests pass (both infrastructure lifecycle and deployment workflow tests) - Documentation builds successfully ACKs for top commit: josecelano: ACK 45588ce Tree-SHA512: 3b2e29f5b5068a5d70d50060638e750ff2934c82161c40c51520cdf6648851bfbb6aac18ad97158f02c562dec565ddd8f40fddf26b8ed731d677f36f59cea4a7
2 parents 4c05f19 + 45588ce commit 5fd6991

12 files changed

Lines changed: 1200 additions & 843 deletions

File tree

src/application/command_handlers/release/errors.rs

Lines changed: 307 additions & 81 deletions
Large diffs are not rendered by default.

src/application/command_handlers/release/handler.rs

Lines changed: 6 additions & 762 deletions
Large diffs are not rendered by default.

src/application/command_handlers/release/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
//! - **Explicit State Transitions**: Type-safe state machine for environment lifecycle
2020
//! - **Explicit Errors**: All errors implement `.help()` with actionable guidance
2121
//!
22+
//! ## Module Organization
23+
//!
24+
//! - `handler.rs` - Core handler with `execute()`, state transitions, workflow orchestration
25+
//! - `workflow.rs` - Release workflow orchestration (step coordination)
26+
//! - `errors.rs` - Error types for release operations
27+
//! - `steps/` - Service-specific step implementations (tracker, prometheus, etc.)
28+
//!
2229
//! ## Release Workflow
2330
//!
2431
//! The command handler orchestrates a multi-step workflow:
@@ -40,6 +47,8 @@
4047
4148
pub mod errors;
4249
pub mod handler;
50+
mod steps;
51+
mod workflow;
4352

4453
#[cfg(test)]
4554
mod tests;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//! Caddy service release steps
2+
//!
3+
//! This module contains all steps required to release the Caddy service:
4+
//! - Configuration template rendering
5+
//! - Configuration deployment to remote
6+
//!
7+
//! All steps are optional and only execute if HTTPS is configured.
8+
9+
use std::sync::Arc;
10+
11+
use tracing::info;
12+
13+
use super::common::ansible_client;
14+
use crate::application::command_handlers::common::StepResult;
15+
use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError;
16+
use crate::application::steps::application::DeployCaddyConfigStep;
17+
use crate::application::steps::rendering::RenderCaddyTemplatesStep;
18+
use crate::domain::environment::state::ReleaseStep;
19+
use crate::domain::environment::{Environment, Releasing};
20+
use crate::domain::template::TemplateManager;
21+
22+
/// Release the Caddy service (if HTTPS enabled)
23+
///
24+
/// Executes all steps required to release Caddy:
25+
/// 1. Render configuration templates
26+
/// 2. Deploy configuration to remote
27+
///
28+
/// If HTTPS is not configured, all steps are skipped.
29+
///
30+
/// # Errors
31+
///
32+
/// Returns a tuple of (error, step) if any Caddy step fails
33+
#[allow(clippy::result_large_err)]
34+
pub fn release(
35+
environment: &Environment<Releasing>,
36+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
37+
// Check if HTTPS is configured
38+
if environment.context().user_inputs.https().is_none() {
39+
info!(
40+
command = "release",
41+
service = "caddy",
42+
status = "skipped",
43+
"HTTPS not configured - skipping all Caddy steps"
44+
);
45+
return Ok(());
46+
}
47+
48+
render_templates(environment)?;
49+
deploy_config_to_remote(environment)?;
50+
Ok(())
51+
}
52+
53+
/// Render Caddy configuration templates
54+
///
55+
/// # Errors
56+
///
57+
/// Returns a tuple of (error, `ReleaseStep::RenderCaddyTemplates`) if rendering fails
58+
#[allow(clippy::result_large_err)]
59+
fn render_templates(
60+
environment: &Environment<Releasing>,
61+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
62+
let current_step = ReleaseStep::RenderCaddyTemplates;
63+
64+
let template_manager = Arc::new(TemplateManager::new(environment.templates_dir()));
65+
let step = RenderCaddyTemplatesStep::new(
66+
Arc::new(environment.clone()),
67+
template_manager,
68+
environment.build_dir().clone(),
69+
);
70+
71+
step.execute().map_err(|e| {
72+
(
73+
ReleaseCommandHandlerError::TemplateRendering {
74+
message: e.to_string(),
75+
source: Box::new(e),
76+
},
77+
current_step,
78+
)
79+
})?;
80+
81+
info!(
82+
command = "release",
83+
step = %current_step,
84+
"Caddy configuration templates rendered successfully"
85+
);
86+
87+
Ok(())
88+
}
89+
90+
/// Deploy Caddy configuration to the remote host
91+
///
92+
/// # Errors
93+
///
94+
/// Returns a tuple of (error, `ReleaseStep::DeployCaddyConfigToRemote`) if deployment fails
95+
#[allow(clippy::result_large_err)]
96+
fn deploy_config_to_remote(
97+
environment: &Environment<Releasing>,
98+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
99+
let current_step = ReleaseStep::DeployCaddyConfigToRemote;
100+
101+
DeployCaddyConfigStep::new(ansible_client(environment))
102+
.execute()
103+
.map_err(|e| {
104+
(
105+
ReleaseCommandHandlerError::CaddyConfigDeployment {
106+
message: e.to_string(),
107+
source: Box::new(e),
108+
},
109+
current_step,
110+
)
111+
})?;
112+
113+
info!(
114+
command = "release",
115+
step = %current_step,
116+
"Caddy configuration deployed to remote successfully"
117+
);
118+
119+
Ok(())
120+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! Common utilities shared across release steps
2+
3+
use std::sync::Arc;
4+
5+
use crate::adapters::ansible::AnsibleClient;
6+
use crate::domain::environment::{Environment, Releasing};
7+
8+
/// Create an Ansible client configured for the environment's build directory
9+
///
10+
/// This is a helper function to reduce duplication across step implementations.
11+
#[must_use]
12+
pub fn ansible_client(environment: &Environment<Releasing>) -> Arc<AnsibleClient> {
13+
Arc::new(AnsibleClient::new(environment.build_dir().join("ansible")))
14+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Docker Compose release steps
2+
//!
3+
//! This module contains all steps required to deploy Docker Compose:
4+
//! - Template rendering
5+
//! - Compose files deployment to remote
6+
7+
use std::path::{Path, PathBuf};
8+
use std::sync::Arc;
9+
10+
use tracing::info;
11+
12+
use crate::adapters::ansible::AnsibleClient;
13+
use crate::application::command_handlers::common::StepResult;
14+
use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError;
15+
use crate::application::steps::{DeployComposeFilesStep, RenderDockerComposeTemplatesStep};
16+
use crate::domain::environment::state::ReleaseStep;
17+
use crate::domain::environment::{Environment, Releasing};
18+
use crate::domain::template::TemplateManager;
19+
20+
/// Release Docker Compose configuration
21+
///
22+
/// Executes all steps required to deploy Docker Compose:
23+
/// 1. Render Docker Compose templates
24+
/// 2. Deploy compose files to remote
25+
///
26+
/// # Errors
27+
///
28+
/// Returns a tuple of (error, step) if any Docker Compose step fails
29+
pub async fn release(
30+
environment: &Environment<Releasing>,
31+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
32+
let compose_build_dir = render_templates(environment).await?;
33+
deploy_files_to_remote(environment, &compose_build_dir)?;
34+
Ok(())
35+
}
36+
37+
/// Render Docker Compose templates to the build directory
38+
///
39+
/// # Errors
40+
///
41+
/// Returns a tuple of (error, `ReleaseStep::RenderDockerComposeTemplates`) if rendering fails
42+
async fn render_templates(
43+
environment: &Environment<Releasing>,
44+
) -> StepResult<PathBuf, ReleaseCommandHandlerError, ReleaseStep> {
45+
let current_step = ReleaseStep::RenderDockerComposeTemplates;
46+
47+
let template_manager = Arc::new(TemplateManager::new(environment.templates_dir()));
48+
let step = RenderDockerComposeTemplatesStep::new(
49+
Arc::new(environment.clone()),
50+
template_manager,
51+
environment.build_dir().clone(),
52+
);
53+
54+
let compose_build_dir = step.execute().await.map_err(|e| {
55+
(
56+
ReleaseCommandHandlerError::TemplateRendering {
57+
message: e.to_string(),
58+
source: Box::new(e),
59+
},
60+
current_step,
61+
)
62+
})?;
63+
64+
info!(
65+
command = "release",
66+
compose_build_dir = %compose_build_dir.display(),
67+
"Docker Compose templates rendered successfully"
68+
);
69+
70+
Ok(compose_build_dir)
71+
}
72+
73+
/// Deploy compose files to the remote host via Ansible
74+
///
75+
/// # Arguments
76+
///
77+
/// * `environment` - The environment in Releasing state
78+
/// * `compose_build_dir` - Path to the rendered compose files
79+
///
80+
/// # Errors
81+
///
82+
/// Returns a tuple of (error, `ReleaseStep::DeployComposeFilesToRemote`) if deployment fails
83+
#[allow(clippy::result_large_err)]
84+
fn deploy_files_to_remote(
85+
environment: &Environment<Releasing>,
86+
compose_build_dir: &Path,
87+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
88+
let current_step = ReleaseStep::DeployComposeFilesToRemote;
89+
90+
let ansible_client = Arc::new(AnsibleClient::new(environment.ansible_build_dir()));
91+
let step = DeployComposeFilesStep::new(ansible_client, compose_build_dir.to_path_buf());
92+
93+
step.execute().map_err(|e| {
94+
(
95+
ReleaseCommandHandlerError::ComposeFilesDeployment {
96+
message: e.to_string(),
97+
source: Box::new(e),
98+
},
99+
current_step,
100+
)
101+
})?;
102+
103+
info!(
104+
command = "release",
105+
compose_build_dir = %compose_build_dir.display(),
106+
instance_ip = ?environment.instance_ip(),
107+
"Compose files deployed to remote host successfully"
108+
);
109+
110+
Ok(())
111+
}

0 commit comments

Comments
 (0)