//! Data models for CI use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Project { pub id: Uuid, pub name: String, pub description: Option, pub repository_url: String, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Jobset { pub id: Uuid, pub project_id: Uuid, pub name: String, pub nix_expression: String, pub enabled: bool, pub flake_mode: bool, pub check_interval: i32, pub branch: Option, pub scheduling_shares: i32, pub created_at: DateTime, pub updated_at: DateTime, pub state: JobsetState, pub last_checked_at: Option>, pub keep_nr: i32, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Evaluation { pub id: Uuid, pub jobset_id: Uuid, pub commit_hash: String, pub evaluation_time: DateTime, pub status: EvaluationStatus, pub error_message: Option, pub inputs_hash: Option, pub pr_number: Option, pub pr_head_branch: Option, pub pr_base_branch: Option, pub pr_action: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] #[serde(rename_all = "lowercase")] #[sqlx(type_name = "text", rename_all = "lowercase")] pub enum EvaluationStatus { Pending, Running, Completed, Failed, } /// Jobset scheduling state (Hydra-compatible). /// /// - `Disabled`: Jobset will not be evaluated /// - `Enabled`: Normal operation, evaluated according to `check_interval` /// - `OneShot`: Evaluated once, then automatically set to Disabled /// - `OneAtATime`: Only one build can run at a time for this jobset #[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type, Default, )] #[serde(rename_all = "snake_case")] #[sqlx(type_name = "varchar", rename_all = "snake_case")] pub enum JobsetState { Disabled, #[default] Enabled, OneShot, OneAtATime, } impl JobsetState { /// Returns true if this jobset state allows evaluation. #[must_use] pub const fn is_evaluable(&self) -> bool { matches!(self, Self::Enabled | Self::OneShot | Self::OneAtATime) } /// Returns the database string representation of this state. #[must_use] pub const fn as_str(&self) -> &'static str { match self { Self::Disabled => "disabled", Self::Enabled => "enabled", Self::OneShot => "one_shot", Self::OneAtATime => "one_at_a_time", } } } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Build { pub id: Uuid, pub evaluation_id: Uuid, pub job_name: String, pub drv_path: String, pub status: BuildStatus, pub started_at: Option>, pub completed_at: Option>, pub log_path: Option, pub build_output_path: Option, pub error_message: Option, pub system: Option, pub priority: i32, pub retry_count: i32, pub max_retries: i32, pub notification_pending_since: Option>, pub log_url: Option, pub created_at: DateTime, pub outputs: Option, pub is_aggregate: bool, pub constituents: Option, pub builder_id: Option, pub signed: bool, pub keep: bool, } #[derive( Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, )] #[serde(rename_all = "snake_case")] #[sqlx(type_name = "text", rename_all = "snake_case")] pub enum BuildStatus { Pending, Running, Succeeded, Failed, DependencyFailed, Aborted, Cancelled, FailedWithOutput, Timeout, CachedFailure, UnsupportedSystem, LogLimitExceeded, NarSizeLimitExceeded, NonDeterministic, } impl BuildStatus { /// Returns true if the build has completed (not pending or running). pub fn is_finished(&self) -> bool { !matches!(self, Self::Pending | Self::Running) } /// Returns true if the build succeeded. /// Note: Does NOT include CachedFailure - a cached failure is still a /// failure. pub fn is_success(&self) -> bool { matches!(self, Self::Succeeded) } /// Returns true if the build completed without needing a retry. /// This includes both successful builds and cached failures. pub fn is_terminal(&self) -> bool { matches!( self, Self::Succeeded | Self::Failed | Self::CachedFailure | Self::DependencyFailed | Self::Aborted | Self::Cancelled | Self::FailedWithOutput | Self::Timeout | Self::UnsupportedSystem | Self::LogLimitExceeded | Self::NarSizeLimitExceeded | Self::NonDeterministic ) } /// Returns the database integer representation of this status. /// Note: This uses an internal numbering scheme (0-13), not Hydra exit codes. pub fn as_i32(&self) -> i32 { match self { Self::Pending => 0, Self::Running => 1, Self::Succeeded => 2, Self::Failed => 3, Self::DependencyFailed => 4, Self::Aborted => 5, Self::Cancelled => 6, Self::FailedWithOutput => 7, Self::Timeout => 8, Self::CachedFailure => 9, Self::UnsupportedSystem => 10, Self::LogLimitExceeded => 11, Self::NarSizeLimitExceeded => 12, Self::NonDeterministic => 13, } } /// Converts a database integer to BuildStatus. /// This is the inverse of as_i32() for reading from the database. pub fn from_i32(code: i32) -> Option { match code { 0 => Some(Self::Pending), 1 => Some(Self::Running), 2 => Some(Self::Succeeded), 3 => Some(Self::Failed), 4 => Some(Self::DependencyFailed), 5 => Some(Self::Aborted), 6 => Some(Self::Cancelled), 7 => Some(Self::FailedWithOutput), 8 => Some(Self::Timeout), 9 => Some(Self::CachedFailure), 10 => Some(Self::UnsupportedSystem), 11 => Some(Self::LogLimitExceeded), 12 => Some(Self::NarSizeLimitExceeded), 13 => Some(Self::NonDeterministic), _ => None, } } /// Converts a Hydra-compatible exit code to a BuildStatus. /// Note: These codes follow Hydra's conventions and differ from /// as_i32/from_i32. pub fn from_exit_code(exit_code: i32) -> Self { match exit_code { 0 => Self::Succeeded, 1 => Self::Failed, 2 => Self::DependencyFailed, 3 => Self::Aborted, 4 => Self::Cancelled, 5 => Self::Aborted, // Obsolete in Hydra, treat as aborted 6 => Self::FailedWithOutput, 7 => Self::Timeout, 8 => Self::CachedFailure, 9 => Self::UnsupportedSystem, 10 => Self::LogLimitExceeded, 11 => Self::NarSizeLimitExceeded, 12 => Self::NonDeterministic, _ => Self::Failed, } } } impl std::fmt::Display for BuildStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::Pending => "pending", Self::Running => "running", Self::Succeeded => "succeeded", Self::Failed => "failed", Self::DependencyFailed => "dependency failed", Self::Aborted => "aborted", Self::Cancelled => "cancelled", Self::FailedWithOutput => "failed with output", Self::Timeout => "timeout", Self::CachedFailure => "cached failure", Self::UnsupportedSystem => "unsupported system", Self::LogLimitExceeded => "log limit exceeded", Self::NarSizeLimitExceeded => "nar size limit exceeded", Self::NonDeterministic => "non-deterministic", }; write!(f, "{}", s) } } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BuildProduct { pub id: Uuid, pub build_id: Uuid, pub name: String, pub path: String, pub sha256_hash: Option, pub file_size: Option, pub content_type: Option, pub is_directory: bool, pub gc_root_path: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BuildStep { pub id: Uuid, pub build_id: Uuid, pub step_number: i32, pub command: String, pub output: Option, pub error_output: Option, pub started_at: DateTime, pub completed_at: Option>, pub exit_code: Option, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BuildDependency { pub id: Uuid, pub build_id: Uuid, pub dependency_build_id: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BuildMetric { pub id: Uuid, pub build_id: Uuid, pub metric_name: String, pub metric_value: f64, pub unit: String, pub collected_at: DateTime, } pub mod metric_names { pub const BUILD_DURATION_SECONDS: &str = "build_duration_seconds"; pub const OUTPUT_SIZE_BYTES: &str = "output_size_bytes"; } pub mod metric_units { pub const SECONDS: &str = "seconds"; pub const BYTES: &str = "bytes"; } /// Active jobset view — enabled jobsets joined with project info. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct ActiveJobset { pub id: Uuid, pub project_id: Uuid, pub name: String, pub nix_expression: String, pub enabled: bool, pub flake_mode: bool, pub check_interval: i32, pub branch: Option, pub scheduling_shares: i32, pub created_at: DateTime, pub updated_at: DateTime, pub state: JobsetState, pub last_checked_at: Option>, pub keep_nr: i32, pub project_name: String, pub repository_url: String, } /// Build statistics from the `build_stats` view. #[derive(Debug, Clone, Serialize, Deserialize, FromRow, Default)] pub struct BuildStats { pub total_builds: Option, pub completed_builds: Option, pub failed_builds: Option, pub running_builds: Option, pub pending_builds: Option, pub avg_duration_seconds: Option, } /// API key for authentication. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct ApiKey { pub id: Uuid, pub name: String, pub key_hash: String, pub role: String, pub user_id: Option, pub created_at: DateTime, pub last_used_at: Option>, } /// Webhook configuration for a project. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct WebhookConfig { pub id: Uuid, pub project_id: Uuid, pub forge_type: String, pub secret_hash: Option, pub enabled: bool, pub created_at: DateTime, } /// Notification configuration for a project. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct NotificationConfig { pub id: Uuid, pub project_id: Uuid, pub notification_type: String, pub config: serde_json::Value, pub enabled: bool, pub created_at: DateTime, } /// Jobset input definition. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct JobsetInput { pub id: Uuid, pub jobset_id: Uuid, pub name: String, pub input_type: String, pub value: String, pub revision: Option, pub created_at: DateTime, } /// Release channel — tracks the latest "good" evaluation for a jobset. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Channel { pub id: Uuid, pub project_id: Uuid, pub name: String, pub jobset_id: Uuid, pub current_evaluation_id: Option, pub created_at: DateTime, pub updated_at: DateTime, } /// Remote builder for multi-machine / multi-arch builds. #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct RemoteBuilder { pub id: Uuid, pub name: String, pub ssh_uri: String, pub systems: Vec, pub max_jobs: i32, pub speed_factor: i32, pub supported_features: Vec, pub mandatory_features: Vec, pub enabled: bool, pub public_host_key: Option, pub ssh_key_file: Option, pub created_at: DateTime, pub consecutive_failures: i32, pub disabled_until: Option>, pub last_failure: Option>, } /// User account for authentication and personalization #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct User { pub id: Uuid, pub username: String, pub email: String, pub full_name: Option, pub password_hash: Option, pub user_type: UserType, pub role: String, pub enabled: bool, pub email_verified: bool, pub public_dashboard: bool, pub created_at: DateTime, pub updated_at: DateTime, pub last_login_at: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "varchar", rename_all = "lowercase")] pub enum UserType { Local, Github, Google, } /// Starred job for personalized dashboard #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct StarredJob { pub id: Uuid, pub user_id: Uuid, pub project_id: Uuid, pub jobset_id: Option, pub job_name: String, pub created_at: DateTime, } /// Project membership for per-project permissions #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct ProjectMember { pub id: Uuid, pub project_id: Uuid, pub user_id: Uuid, pub role: String, pub created_at: DateTime, } /// User session for persistent authentication #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct UserSession { pub id: Uuid, pub user_id: Uuid, pub session_token_hash: String, pub expires_at: DateTime, pub created_at: DateTime, pub last_used_at: Option>, } // Pagination #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationParams { pub limit: Option, pub offset: Option, } impl PaginationParams { #[must_use] pub fn limit(&self) -> i64 { self.limit.unwrap_or(50).clamp(1, 200) } #[must_use] pub fn offset(&self) -> i64 { self.offset.unwrap_or(0).max(0) } } impl Default for PaginationParams { fn default() -> Self { Self { limit: Some(50), offset: Some(0), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginatedResponse { pub items: Vec, pub total: i64, pub limit: i64, pub offset: i64, } // DTO structs for creation and updates #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateProject { pub name: String, pub description: Option, pub repository_url: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateProject { pub name: Option, pub description: Option, pub repository_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateJobset { pub project_id: Uuid, pub name: String, pub nix_expression: String, pub enabled: Option, pub flake_mode: Option, pub check_interval: Option, pub branch: Option, pub scheduling_shares: Option, pub state: Option, pub keep_nr: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateJobset { pub name: Option, pub nix_expression: Option, pub enabled: Option, pub flake_mode: Option, pub check_interval: Option, pub branch: Option, pub scheduling_shares: Option, pub state: Option, pub keep_nr: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateEvaluation { pub jobset_id: Uuid, pub commit_hash: String, pub pr_number: Option, pub pr_head_branch: Option, pub pr_base_branch: Option, pub pr_action: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateBuild { pub evaluation_id: Uuid, pub job_name: String, pub drv_path: String, pub system: Option, pub outputs: Option, pub is_aggregate: Option, pub constituents: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateBuildProduct { pub build_id: Uuid, pub name: String, pub path: String, pub sha256_hash: Option, pub file_size: Option, pub content_type: Option, pub is_directory: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateBuildStep { pub build_id: Uuid, pub step_number: i32, pub command: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateWebhookConfig { pub project_id: Uuid, pub forge_type: String, pub secret: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateNotificationConfig { pub project_id: Uuid, pub notification_type: String, pub config: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateChannel { pub project_id: Uuid, pub name: String, pub jobset_id: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateChannel { pub name: Option, pub jobset_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateRemoteBuilder { pub name: String, pub ssh_uri: String, pub systems: Vec, pub max_jobs: Option, pub speed_factor: Option, pub supported_features: Option>, pub mandatory_features: Option>, pub public_host_key: Option, pub ssh_key_file: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateRemoteBuilder { pub name: Option, pub ssh_uri: Option, pub systems: Option>, pub max_jobs: Option, pub speed_factor: Option, pub supported_features: Option>, pub mandatory_features: Option>, pub enabled: Option, pub public_host_key: Option, pub ssh_key_file: Option, } /// Summary of system status for the admin API. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemStatus { pub projects_count: i64, pub jobsets_count: i64, pub evaluations_count: i64, pub builds_pending: i64, pub builds_running: i64, pub builds_completed: i64, pub builds_failed: i64, pub remote_builders: i64, pub channels_count: i64, } // User DTOs #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateUser { pub username: String, pub email: String, pub full_name: Option, pub password: String, pub role: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateUser { pub email: Option, pub full_name: Option, pub password: Option, pub role: Option, pub enabled: Option, pub public_dashboard: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginCredentials { pub username: String, pub password: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateStarredJob { pub project_id: Uuid, pub jobset_id: Option, pub job_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateProjectMember { pub user_id: Uuid, pub role: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateProjectMember { pub role: Option, }