From 4c5a99d5549c50d3bb61270fe51cfccac126dcff Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 2 Feb 2026 01:23:10 +0300 Subject: [PATCH] crates/common: add branch and scheduling_shares to jobset models Signed-off-by: NotAShelf Change-Id: Ie19897f5ffdfb44654891511ce669d806a6a6964 --- crates/common/src/config.rs | 182 ++++++++++++++++++++++++++++++---- crates/common/src/models.rs | 8 ++ crates/common/src/validate.rs | 4 + 3 files changed, 175 insertions(+), 19 deletions(-) diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index ba26b3d..2346627 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -16,6 +16,11 @@ pub struct Config { pub notifications: NotificationsConfig, pub cache: CacheConfig, pub signing: SigningConfig, + #[serde(default)] + pub cache_upload: CacheUploadConfig, + pub tracing: TracingConfig, + #[serde(default)] + pub declarative: DeclarativeConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -79,6 +84,7 @@ pub struct LogConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] +#[derive(Default)] pub struct NotificationsConfig { pub run_command: Option, pub github_token: Option, @@ -107,11 +113,91 @@ pub struct CacheConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] +#[derive(Default)] pub struct SigningConfig { pub enabled: bool, pub key_file: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +#[derive(Default)] +pub struct CacheUploadConfig { + pub enabled: bool, + pub store_uri: Option, +} + + +/// Declarative project/jobset/api-key definitions. +/// These are upserted on server startup, enabling fully declarative operation. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct DeclarativeConfig { + pub projects: Vec, + pub api_keys: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeclarativeProject { + pub name: String, + pub repository_url: String, + pub description: Option, + #[serde(default)] + pub jobsets: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeclarativeJobset { + pub name: String, + pub nix_expression: String, + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default = "default_true")] + pub flake_mode: bool, + #[serde(default = "default_check_interval")] + pub check_interval: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeclarativeApiKey { + pub name: String, + pub key: String, + #[serde(default = "default_role")] + pub role: String, +} + +fn default_true() -> bool { + true +} + +fn default_check_interval() -> i32 { + 60 +} + +fn default_role() -> String { + "admin".to_string() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct TracingConfig { + pub level: String, + pub format: String, + pub show_targets: bool, + pub show_timestamps: bool, +} + +impl Default for TracingConfig { + fn default() -> Self { + Self { + level: "info".to_string(), + format: "compact".to_string(), + show_targets: true, + show_timestamps: true, + } + } +} + impl Default for DatabaseConfig { fn default() -> Self { Self { @@ -214,17 +300,6 @@ impl Default for LogConfig { } } -impl Default for NotificationsConfig { - fn default() -> Self { - Self { - run_command: None, - github_token: None, - gitea_url: None, - gitea_token: None, - email: None, - } - } -} impl Default for CacheConfig { fn default() -> Self { @@ -235,14 +310,6 @@ impl Default for CacheConfig { } } -impl Default for SigningConfig { - fn default() -> Self { - Self { - enabled: false, - key_file: None, - } - } -} impl Config { pub fn load() -> anyhow::Result { @@ -376,6 +443,83 @@ mod tests { assert!(config.validate().is_err()); } + #[test] + fn test_declarative_config_default_is_empty() { + let config = DeclarativeConfig::default(); + assert!(config.projects.is_empty()); + assert!(config.api_keys.is_empty()); + } + + #[test] + fn test_declarative_config_deserialization() { + let toml_str = r#" + [[projects]] + name = "my-project" + repository_url = "https://github.com/test/repo" + description = "Test project" + + [[projects.jobsets]] + name = "packages" + nix_expression = "packages" + + [[api_keys]] + name = "admin-key" + key = "fc_secret_key_123" + role = "admin" + "#; + let config: DeclarativeConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.projects.len(), 1); + assert_eq!(config.projects[0].name, "my-project"); + assert_eq!(config.projects[0].jobsets.len(), 1); + assert_eq!(config.projects[0].jobsets[0].name, "packages"); + assert!(config.projects[0].jobsets[0].enabled); // default true + assert!(config.projects[0].jobsets[0].flake_mode); // default true + assert_eq!(config.api_keys.len(), 1); + assert_eq!(config.api_keys[0].role, "admin"); + } + + #[test] + fn test_declarative_config_serialization_roundtrip() { + let config = DeclarativeConfig { + projects: vec![DeclarativeProject { + name: "test".to_string(), + repository_url: "https://example.com/repo".to_string(), + description: Some("desc".to_string()), + jobsets: vec![DeclarativeJobset { + name: "checks".to_string(), + nix_expression: "checks".to_string(), + enabled: true, + flake_mode: true, + check_interval: 300, + }], + }], + api_keys: vec![DeclarativeApiKey { + name: "test-key".to_string(), + key: "fc_test".to_string(), + role: "admin".to_string(), + }], + }; + + let json = serde_json::to_string(&config).unwrap(); + let parsed: DeclarativeConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.projects.len(), 1); + assert_eq!(parsed.projects[0].jobsets[0].check_interval, 300); + assert_eq!(parsed.api_keys[0].name, "test-key"); + } + + #[test] + fn test_declarative_config_with_main_config() { + // Ensure declarative section is optional (default empty) + // Use the config crate loader which provides defaults for missing fields + let config = Config::default(); + assert!(config.declarative.projects.is_empty()); + assert!(config.declarative.api_keys.is_empty()); + // And that the Config can be serialized back with declarative section + let toml_str = toml::to_string_pretty(&config).unwrap(); + let parsed: Config = toml::from_str(&toml_str).unwrap(); + assert!(parsed.declarative.projects.is_empty()); + } + #[test] fn test_environment_override() { // Test environment variable parsing directly diff --git a/crates/common/src/models.rs b/crates/common/src/models.rs index a73148c..0d63faa 100644 --- a/crates/common/src/models.rs +++ b/crates/common/src/models.rs @@ -24,6 +24,8 @@ pub struct Jobset { 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, } @@ -128,6 +130,8 @@ pub struct ActiveJobset { 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 project_name: String, @@ -278,6 +282,8 @@ pub struct CreateJobset { pub enabled: Option, pub flake_mode: Option, pub check_interval: Option, + pub branch: Option, + pub scheduling_shares: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -287,6 +293,8 @@ pub struct UpdateJobset { pub enabled: Option, pub flake_mode: Option, pub check_interval: Option, + pub branch: Option, + pub scheduling_shares: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/common/src/validate.rs b/crates/common/src/validate.rs index cc68592..5ae857f 100644 --- a/crates/common/src/validate.rs +++ b/crates/common/src/validate.rs @@ -469,6 +469,8 @@ mod tests { enabled: None, flake_mode: None, check_interval: Some(300), + branch: None, + scheduling_shares: None, }; assert!(j.validate().is_ok()); } @@ -482,6 +484,8 @@ mod tests { enabled: None, flake_mode: None, check_interval: Some(5), + branch: None, + scheduling_shares: None, }; assert!(j.validate().is_err()); }