swactor/crates/ci/src/lib.rs

288 lines
8.6 KiB
Rust

pub mod coordinator;
pub mod pipeline;
pub mod provisioner;
pub mod runner;
pub mod yaml;
use std::collections::HashMap;
use std::fmt;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};
// ─── Core Identifiers ───────────────────────────────────────────────────────
/// Unique identifier for a pipeline execution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PipelineId(pub u64);
impl fmt::Display for PipelineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pipeline-{}", self.0)
}
}
/// Unique identifier for a job within a pipeline.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct JobId {
pub pipeline_id: PipelineId,
pub job_name: String,
}
impl fmt::Display for JobId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.pipeline_id, self.job_name)
}
}
// ─── Job Status ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobStatus {
Pending,
WaitingForProvisioner,
Provisioning,
Running,
Passed,
Failed { reason: String },
Skipped,
Interrupted,
}
impl JobStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
JobStatus::Passed | JobStatus::Failed { .. } | JobStatus::Skipped | JobStatus::Interrupted
)
}
}
// ─── Pipeline Status ────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PipelineStatus {
Pending,
Running,
Passed,
Failed,
Error { reason: String },
}
impl PipelineStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
PipelineStatus::Passed | PipelineStatus::Failed | PipelineStatus::Error { .. }
)
}
/// Convert to Forgejo commit status string.
pub fn forgejo_state(&self) -> &'static str {
match self {
PipelineStatus::Pending => "pending",
PipelineStatus::Running => "pending",
PipelineStatus::Passed => "success",
PipelineStatus::Failed => "failure",
PipelineStatus::Error { .. } => "error",
}
}
}
// ─── Instance Types ─────────────────────────────────────────────────────────
/// Specification for a spot instance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceSpec {
pub min_cpus: u32,
pub min_ram_mb: u32,
pub min_disk_gb: u32,
pub docker_required: bool,
pub region_preferences: Vec<String>,
}
impl Default for InstanceSpec {
fn default() -> Self {
Self {
min_cpus: 2,
min_ram_mb: 2048,
min_disk_gb: 20,
docker_required: false,
region_preferences: Vec::new(),
}
}
}
/// Connection details for a provisioned spot instance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceReady {
pub instance_id: String,
pub ip: IpAddr,
pub ssh_port: u16,
pub ssh_host_key: String,
}
// ─── Provisioner ↔ Coordinator Messages ─────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionRequest {
pub job_id: JobId,
pub instance_spec: InstanceSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionResponse {
pub job_id: JobId,
pub result: Result<InstanceReady, ProvisionError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProvisionError {
NoCapacity,
ProviderError(String),
Timeout,
ProvisionerOffline,
}
impl fmt::Display for ProvisionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProvisionError::NoCapacity => write!(f, "no capacity available"),
ProvisionError::ProviderError(msg) => write!(f, "provider error: {msg}"),
ProvisionError::Timeout => write!(f, "provisioning timed out"),
ProvisionError::ProvisionerOffline => write!(f, "provisioner is offline"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminateRequest {
pub job_id: JobId,
pub instance_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminateAck {
pub job_id: JobId,
}
// ─── Coordinator ↔ RunnerSupervisor Messages ────────────────────────────────
/// Sent from Coordinator to RunnerSupervisor to begin a job.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartJob {
pub job_id: JobId,
pub instance: InstanceReady,
pub repo_url: String,
pub commit_sha: String,
pub job_def: JobDefinition,
}
/// Streamed output from RunnerSupervisor back to Coordinator.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobProgress {
pub job_id: JobId,
pub output_line: String,
}
/// Final result from RunnerSupervisor.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobComplete {
pub job_id: JobId,
pub result: Result<JobSuccess, JobFailure>,
pub artifacts: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobSuccess;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobFailure {
CommandFailed { exit_code: i32, last_lines: Vec<String> },
SshError(String),
Timeout,
Interrupted,
}
impl fmt::Display for JobFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JobFailure::CommandFailed { exit_code, .. } => {
write!(f, "command exited with code {exit_code}")
}
JobFailure::SshError(msg) => write!(f, "SSH error: {msg}"),
JobFailure::Timeout => write!(f, "job timed out"),
JobFailure::Interrupted => write!(f, "spot instance interrupted"),
}
}
}
// ─── Job Definition ─────────────────────────────────────────────────────────
/// A parsed job from the .ci.yml file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobDefinition {
pub name: String,
pub run: Vec<String>,
pub needs: Vec<String>,
pub timeout_secs: u64,
pub docker: bool,
pub artifacts: Vec<String>,
pub env: HashMap<String, String>,
}
// ─── Webhook Types ──────────────────────────────────────────────────────────
/// Parsed webhook event from Forgejo.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEvent {
pub event_type: EventType,
pub repo_owner: String,
pub repo_name: String,
pub branch: String,
pub commit_sha: String,
pub tag: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventType {
Push,
Tag,
Merge,
}
// ─── Forgejo Status Updates ─────────────────────────────────────────────────
/// A commit status update to send to Forgejo's API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusUpdate {
pub repo_owner: String,
pub repo_name: String,
pub commit_sha: String,
pub state: String,
pub context: String,
pub description: String,
}
// ─── Coordinator Config ─────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CiConfig {
pub webhook_port: u16,
pub webhook_secret: String,
pub forgejo_url: String,
pub forgejo_token: String,
pub data_dir: String,
}
impl Default for CiConfig {
fn default() -> Self {
Self {
webhook_port: 8787,
webhook_secret: String::new(),
forgejo_url: String::new(),
forgejo_token: String::new(),
data_dir: "./ci-data".into(),
}
}
}