rustlift 2.0.1

A typestate-driven deployment agent for Azure Web Apps
Documentation
// Copyright (c) 2026 Hamze Ghalebi. All rights reserved.
// Licensed under the Rustlift Non-Commercial Licence v1.0.

//! Command-line entrypoint for the Rustlift deployment agent.
//!
//! This binary initialises structured logging, chains the four pipeline
//! stages, and translates any [`DeployError`] into a human-readable hint
//! before exiting with a non-zero status code.
//!
//! # Learning: Separation of Concerns
//!
//! `main.rs` is intentionally thin. It does three things:
//!
//! 1. **Bootstrap** — set up logging.
//! 2. **Delegate** — call `run()` which chains the pipeline.
//! 3. **Report** — translate errors into user-friendly messages.
//!
//! The actual logic lives in the library crate (`lib.rs`, `pipeline.rs`).
//! This separation means the pipeline can be tested, benchmarked, or
//! embedded in another tool without pulling in the CLI layer.
//!
//! # Environment Variables
//!
//! | Variable                | Required | Default               |
//! |-------------------------|----------|-----------------------|
//! | `AZURE_SUBSCRIPTION_ID` | **yes**  | —                     |
//! | `APP_NAME`              | no       | `rust-enterprise-api` |
//! | `AZURE_LOCATION`        | no       | `eastus`              |
//! | `RUST_LOG`              | no       | `info`                |
//!
//! # Usage
//!
//! ```bash
//! export AZURE_SUBSCRIPTION_ID="..."
//! RUST_LOG=debug cargo run --bin rustlift  # verbose
//! RUST_LOG=info  cargo run --bin rustlift  # production
//! ```

use tracing::{error, info};

use rustlift::errors::DeployError;
use rustlift::pipeline::{Init, Pipeline};

/// The async runtime entry point.
///
/// # Learning: `#[tokio::main]`
///
/// This attribute macro transforms the `async fn main()` into a regular
/// `fn main()` that creates a Tokio runtime and blocks on the future.
/// Under the hood, it expands to roughly:
///
/// ```rust,ignore
/// fn main() {
///     tokio::runtime::Runtime::new().unwrap().block_on(async { ... })
/// }
/// ```
///
/// We use the default multi-threaded scheduler, which is appropriate
/// for I/O-bound workloads like HTTP requests and process spawning.
#[tokio::main]
async fn main() {
    // Structured logging — honours the RUST_LOG environment variable.
    //
    // Learning: `EnvFilter` reads `RUST_LOG` to decide which log levels
    // to display. If the variable is not set, we default to `info`.
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    info!("🤖 Agent Initialized.");

    // Delegate to `run()` and handle any error at the top level.
    //
    // Learning: The `if let Err(e)` pattern is idiomatic Rust for
    // "do something only if the result is an error". It avoids
    // nesting inside a full `match` block.
    if let Err(e) = run().await {
        error!("🛑 CRITICAL FAILURE");
        error!("==================================================");
        error!("{}", e);
        error!("==================================================");

        // Provide actionable hints based on the error category.
        //
        // Learning: Pattern matching on an error enum lets us give
        // the user specific remediation advice. This is far more
        // helpful than a generic "something went wrong" message.
        match e {
            DeployError::Dependency(_) => {
                error!("💡 Hint: Install missing tools (az, cargo, cross).")
            }
            DeployError::Build(_) => {
                error!("💡 Hint: Ensure 'rustup target add x86_64-unknown-linux-musl' is run.")
            }
            DeployError::Auth(_) => {
                error!("💡 Hint: Run 'az login' to refresh your session.")
            }
            _ => {}
        }

        std::process::exit(1);
    } else {
        info!("✅ SYSTEM LIVE");
    }
}

/// Chains the four pipeline stages into a single async flow.
///
/// Each method call consumes the current `Pipeline<State>` and produces
/// the next state. This is the **typestate pattern** in action — the
/// compiler will refuse to compile code that calls `.deploy_and_verify()`
/// before `.build_and_package()`.
///
/// # Learning: The `?` Operator
///
/// The `?` at the end of each `.await?` does two things:
///
/// 1. If the result is `Ok(value)`, it unwraps `value` and continues.
/// 2. If the result is `Err(e)`, it returns `Err(e)` from the function
///    immediately — no explicit `match` or `if let` needed.
///
/// This makes the "happy path" read like a simple sequential script,
/// while errors propagate automatically to the caller.
///
/// # Errors
///
/// Returns any [`DeployError`] emitted by any stage of the pipeline.
async fn run() -> Result<(), DeployError> {
    Pipeline::<Init>::new()?
        .authenticate()
        .await?
        .ensure_infrastructure()
        .await?
        .build_and_package()
        .await?
        .deploy_and_verify()
        .await?;
    Ok(())
}