rustlift 2.0.2

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.

//! Deployment error taxonomy for the Rustlift pipeline.
//!
//! This module defines a single, exhaustive error type — [`DeployError`] —
//! that every fallible function in the crate returns. Each variant maps to
//! a distinct failure domain (network, config, build, etc.), and the
//! [`crate::resilience::reliable_op`] wrapper uses this taxonomy to decide
//! whether to **retry** or **abort immediately**.
//!
//! # Learning: Why a Single Error Enum?
//!
//! In Rust, error handling follows a principle: **errors are values**, not
//! exceptions. By collecting every failure mode into one enum, callers can
//! pattern-match exhaustively. The compiler guarantees that you have
//! handled every case — no uncaught exceptions, no surprise panics.
//!
//! The `#[derive(Error)]` macro from the [`thiserror`] crate generates
//! the boilerplate implementations of [`std::fmt::Display`] and
//! [`std::error::Error`] automatically, keeping this file concise.
//!
//! # Fatal vs. Transient
//!
//! The retry layer in [`crate::resilience`] uses a simple rule:
//!
//! | Category      | Variants                                       | Behaviour         |
//! |---------------|------------------------------------------------|-------------------|
//! | **Fatal**     | `Config`, `Dependency`, `Build`, `PathEncoding` | Abort immediately |
//! | **Transient** | Everything else                                | Retry with backoff|
//!
//! Fatal errors stem from **user mistakes** (wrong config, missing tools)
//! that no amount of retrying will fix. Transient errors stem from
//! **external systems** (network blips, Azure throttling) that often
//! resolve on their own.

use thiserror::Error;

/// Exhaustive error type for every failure mode in the deployment pipeline.
///
/// # Design: The `thiserror` Derive Macro
///
/// The `#[derive(Error)]` attribute auto-generates:
/// - `impl Display` using the `#[error("...")]` format strings.
/// - `impl Error` with proper `source()` chaining via `#[from]` and `#[source]`.
///
/// This eliminates hundreds of lines of boilerplate while keeping errors
/// strongly typed and pattern-matchable.
///
/// # Examples
///
/// Creating and displaying an error:
///
/// ```
/// use rustlift::errors::DeployError;
///
/// let missing_env = DeployError::Config("Missing AZURE_SUBSCRIPTION_ID".into());
/// assert_eq!(
///     missing_env.to_string().contains("Configuration Error"),
///     true
/// );
/// ```
///
/// Pattern-matching to provide recovery hints:
///
/// ```
/// use rustlift::errors::DeployError;
///
/// let error = DeployError::Dependency("Azure CLI ('az') not found".into());
///
/// let hint = match &error {
///     DeployError::Dependency(msg) => msg.as_str(),
///     _ => "unreachable",
/// };
///
/// assert_eq!(hint, "Azure CLI ('az') not found");
/// ```
///
/// # Panics
///
/// Constructing and matching on [`DeployError`] does not panic.
///
/// # Safety
///
/// This enum is safe to construct and pattern-match. It does not expose
/// unsafe memory or lifetime requirements.
#[derive(Error, Debug)]
pub enum DeployError {
    // ------------------------------------------------------------------
    // Fatal Errors — abort immediately, retrying will not help
    // ------------------------------------------------------------------
    /// A required environment variable or configuration value is missing or
    /// malformed.
    ///
    /// **Example triggers:** `AZURE_SUBSCRIPTION_ID` not set, invalid region.
    #[error("⛔ Configuration Error: {0}")]
    Config(String),

    /// A required external tool is not installed on the system.
    ///
    /// Rustlift depends on `az` (Azure CLI), `cargo`, and `cross`.
    /// This variant fires during the pre-flight check in [`crate::pipeline::Pipeline::new`].
    #[error("⛔ Missing Dependency: {0}")]
    Dependency(String),

    /// The `cargo build` or `cross build` command returned a non-zero exit
    /// code.
    ///
    /// This is fatal because compilation errors require human intervention.
    #[error("⛔ Build Failed: {0}")]
    Build(String),

    /// A filesystem path contained non-UTF-8 characters.
    ///
    /// Azure CLI requires UTF-8 path arguments; this variant prevents
    /// cryptic downstream failures.
    #[error("⛔ Path Encoding Error: {0}")]
    PathEncoding(String),

    // ------------------------------------------------------------------
    // Transient Errors — retry with exponential backoff
    // ------------------------------------------------------------------
    /// Azure CLI credential acquisition or token refresh failed.
    ///
    /// # Learning: `#[from]` Attribute
    ///
    /// The `#[from]` attribute auto-generates `impl From<azure_core::Error>
    /// for DeployError`, enabling the `?` operator to convert Azure SDK
    /// errors into this variant automatically.
    #[error("⛔ Azure Auth Failed: {0}")]
    Auth(#[from] azure_core::Error),

    /// Azure ARM provisioning failure (Resource Group, Plan, or Web App).
    ///
    /// Carries a human-readable summary of which Azure operation failed
    /// and the upstream error message.
    #[error("⚠️ Infrastructure Error: {0}")]
    Infra(String),

    /// Context-aware filesystem I/O error.
    ///
    /// # Learning: Structured Error Variants
    ///
    /// Instead of wrapping a bare `std::io::Error`, this variant includes
    /// a `context` field describing *what operation* failed. This avoids
    /// the common problem of "Permission denied" logs with no clue
    /// about *which* file or *which* operation.
    #[error("⚠️ I/O Failure at [{context}]: {source}")]
    Io {
        /// The underlying OS-level I/O error.
        #[source]
        source: std::io::Error,
        /// Human-readable label (e.g. "Reading binary", "Creating zip file").
        context: String,
    },

    /// Zip archive creation or entry write failed.
    #[error("⚠️ Zip Compression Error: {0}")]
    Zip(#[from] zip::result::ZipError),

    /// The `az webapp deployment` CLI command returned a non-zero exit code.
    ///
    /// The error string contains the captured `stderr` output.
    #[error("⚠️ Deployment CLI Error: {0}")]
    Cli(String),

    /// The deployed application's `/health` endpoint did not return HTTP 2xx.
    #[error("⚠️ Health Check Failed: {0}")]
    Health(String),

    /// HTTP-layer failure from `reqwest` (DNS resolution, TLS handshake,
    /// connection timeout).
    #[error("⚠️ Network Error: {0}")]
    Network(#[from] reqwest::Error),

    /// A `tokio::task::spawn_blocking` handle was dropped or the spawned
    /// closure panicked.
    ///
    /// # Learning: `JoinError`
    ///
    /// When blocking work is offloaded via `spawn_blocking`, the handle
    /// can fail if the task panics. Wrapping `JoinError` as a transient
    /// error means the retry layer will attempt the operation again.
    #[error("⛔ Background Task Failed: {0}")]
    JoinError(#[from] tokio::task::JoinError),
}

/// Convenience alias used throughout the pipeline.
///
/// Every fallible function in this crate returns `Result<T>` instead of
/// the verbose `std::result::Result<T, DeployError>`. This is a common
/// Rust idiom — crates define their own `Result` type to reduce visual
/// noise.
///
/// # Learning: Type Aliases
///
/// A type alias does not create a new type; it is purely syntactic sugar.
/// `Result<String>` and `std::result::Result<String, DeployError>` are
/// identical to the compiler. The advantage is readability.
///
/// # Examples
///
/// ```
/// use rustlift::errors::{DeployError, Result};
///
/// fn checked(input: i32) -> Result<i32> {
///     if input >= 0 {
///         Ok(input)
///     } else {
///         Err(DeployError::Config("input must be non-negative".into()))
///     }
/// }
///
/// assert_eq!(checked(7).unwrap(), 7);
/// assert_eq!(checked(-1).is_err(), true);
/// ```
///
/// # Panics
///
/// The alias itself cannot panic.
///
/// # Safety
///
/// This type alias has no unsafe preconditions.
pub type Result<T> = std::result::Result<T, DeployError>;