11use anyhow:: { anyhow, Result } ;
22use chrono_tz:: Tz ;
3- use cron :: Schedule ;
3+ use croner :: Cron ;
44use serde:: Deserialize ;
55use std:: collections:: HashMap ;
66use std:: str:: FromStr ;
@@ -145,7 +145,7 @@ fn default_log_max_size() -> String {
145145pub struct Job {
146146 pub id : String ,
147147 pub name : String ,
148- pub schedule : Schedule ,
148+ pub schedule : Cron ,
149149 pub command : String ,
150150 pub timeout : Duration ,
151151 pub concurrency : Concurrency ,
@@ -168,72 +168,6 @@ pub struct RetryConfig {
168168 pub jitter : Option < Duration > ,
169169}
170170
171- /// Normalize 5-field cron expression for compatibility with cron crate.
172- /// Converts day-of-week 0 (Sunday in POSIX) to 7 (Sunday in cron crate).
173- fn normalize_cron ( cron : & str ) -> String {
174- let fields: Vec < & str > = cron. split_whitespace ( ) . collect ( ) ;
175- if fields. len ( ) != 5 {
176- return cron. to_string ( ) ;
177- }
178-
179- // Day-of-week is the 5th field (index 4)
180- // Parse each element properly to avoid false replacements (e.g., "10" -> "17")
181- let dow = normalize_dow_field ( fields[ 4 ] ) ;
182-
183- format ! (
184- "{} {} {} {} {}" ,
185- fields[ 0 ] , fields[ 1 ] , fields[ 2 ] , fields[ 3 ] , dow
186- )
187- }
188-
189- /// Normalize a single day-of-week field, converting 0 (POSIX Sunday) to 7 (cron crate Sunday).
190- fn normalize_dow_field ( field : & str ) -> String {
191- // Handle special cases
192- if field == "*" || field == "?" {
193- return field. to_string ( ) ;
194- }
195-
196- // Split by comma for lists (e.g., "0,3,5")
197- field
198- . split ( ',' )
199- . map ( |part| normalize_dow_part ( part) )
200- . collect :: < Vec < _ > > ( )
201- . join ( "," )
202- }
203-
204- /// Normalize a single part of a day-of-week field (handles ranges like "0-6" and steps like "0/2").
205- fn normalize_dow_part ( part : & str ) -> String {
206- // Handle step values (e.g., "0/2" or "0-6/2")
207- if let Some ( ( range_part, step) ) = part. split_once ( '/' ) {
208- let normalized_range = normalize_dow_range ( range_part) ;
209- return format ! ( "{}/{}" , normalized_range, step) ;
210- }
211-
212- normalize_dow_range ( part)
213- }
214-
215- /// Normalize a range or single value (e.g., "0", "0-6").
216- fn normalize_dow_range ( part : & str ) -> String {
217- // Handle ranges (e.g., "0-6")
218- if let Some ( ( start, end) ) = part. split_once ( '-' ) {
219- let start_normalized = normalize_dow_value ( start) ;
220- let end_normalized = normalize_dow_value ( end) ;
221- return format ! ( "{}-{}" , start_normalized, end_normalized) ;
222- }
223-
224- // Single value
225- normalize_dow_value ( part)
226- }
227-
228- /// Normalize a single day-of-week value: "0" -> "7", others unchanged.
229- fn normalize_dow_value ( val : & str ) -> String {
230- if val == "0" {
231- "7" . to_string ( )
232- } else {
233- val. to_string ( )
234- }
235- }
236-
237171pub fn parse_config ( content : & str ) -> Result < ( RunnerConfig , Vec < Job > ) > {
238172 let config: Config =
239173 serde_yaml:: from_str ( content) . map_err ( |e| anyhow ! ( "Failed to parse YAML: {}" , e) ) ?;
@@ -281,21 +215,7 @@ fn parse_job(
281215) -> Result < Job > {
282216 validate_job_id ( id) ?;
283217
284- // Validate 5-field cron format
285- let cron_fields: Vec < & str > = job. schedule . cron . split_whitespace ( ) . collect ( ) ;
286- if cron_fields. len ( ) != 5 {
287- anyhow:: bail!(
288- "Invalid cron '{}': expected 5 fields (minute hour day month weekday), got {}" ,
289- job. schedule. cron,
290- cron_fields. len( )
291- ) ;
292- }
293-
294- // Convert 5-field cron to 6-field by prepending seconds (0)
295- // Also normalize day-of-week: 0 → 7 (cron crate uses 1-7, not 0-6)
296- let normalized_cron = normalize_cron ( & job. schedule . cron ) ;
297- let schedule_str = format ! ( "0 {}" , normalized_cron) ;
298- let schedule = Schedule :: from_str ( & schedule_str)
218+ let schedule = Cron :: from_str ( & job. schedule . cron )
299219 . map_err ( |e| anyhow ! ( "Invalid cron '{}': {}" , job. schedule. cron, e) ) ?;
300220
301221 let timeout = parse_duration ( & job. timeout )
@@ -1053,38 +973,4 @@ jobs:
1053973 assert_eq ! ( jobs[ 0 ] . webhook. len( ) , 3 ) ;
1054974 }
1055975
1056- #[ test]
1057- fn test_normalize_dow_field ( ) {
1058- // Standalone 0 -> 7
1059- assert_eq ! ( normalize_dow_field( "0" ) , "7" ) ;
1060- // Other values unchanged
1061- assert_eq ! ( normalize_dow_field( "1" ) , "1" ) ;
1062- assert_eq ! ( normalize_dow_field( "*" ) , "*" ) ;
1063- assert_eq ! ( normalize_dow_field( "?" ) , "?" ) ;
1064- // List with 0
1065- assert_eq ! ( normalize_dow_field( "0,3,5" ) , "7,3,5" ) ;
1066- assert_eq ! ( normalize_dow_field( "1,0,3" ) , "1,7,3" ) ;
1067- // Range starting with 0
1068- assert_eq ! ( normalize_dow_field( "0-6" ) , "7-6" ) ;
1069- // Step value
1070- assert_eq ! ( normalize_dow_field( "0/2" ) , "7/2" ) ;
1071- assert_eq ! ( normalize_dow_field( "0-6/2" ) , "7-6/2" ) ;
1072- // Values that should NOT be affected (regression test for "10" -> "17" bug)
1073- assert_eq ! ( normalize_dow_field( "10" ) , "10" ) ; // Invalid but should not be mangled
1074- assert_eq ! ( normalize_dow_field( "1-5" ) , "1-5" ) ;
1075- assert_eq ! ( normalize_dow_field( "20" ) , "20" ) ; // Invalid but should not be mangled
1076- }
1077-
1078- #[ test]
1079- fn skip_6_field_cron ( ) {
1080- let yaml = r#"
1081- jobs:
1082- test:
1083- schedule:
1084- cron: "0 0 7 * * 7"
1085- run: echo test
1086- "# ;
1087- let ( _, jobs) = parse_config ( yaml) . unwrap ( ) ;
1088- assert ! ( jobs. is_empty( ) ) ; // Invalid job is skipped
1089- }
1090976}
0 commit comments