Skip to content

Commit cdb432e

Browse files
committed
feat(config): support English schedule phrases
Add human-readable schedule syntax as alternative to cron expressions. Uses english-to-cron crate to convert phrases like "7pm every Thursday" or "every 5 minutes" into cron format. Supported patterns: - "every minute", "every 5 minutes" - "every day at 4:00 pm", "at 10:00 am" - "7pm every Thursday", "Sunday at 12:00" - "midnight on Tuesdays" Standard cron syntax is tried first; English is used as fallback.
1 parent 52bd2de commit cdb432e

File tree

5 files changed

+141
-8
lines changed

5 files changed

+141
-8
lines changed

CLAUDE.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ src/
3232
// All support shorthand (string) or full (object) form via #[serde(untagged)]
3333

3434
enum ScheduleConfigRaw {
35-
Simple(String), // "*/5 * * * *"
36-
Full { cron: String, timezone: Option<String> },
35+
Simple(String), // "*/5 * * * *" or "7pm every Thursday"
36+
Full { cron: String, timezone: Option<String> }, // cron also accepts English
3737
}
3838

3939
enum BuildConfigRaw {
@@ -150,6 +150,31 @@ jobs:
150150
log: output.log # Shorthand for { file: "..." }
151151
```
152152
153+
### English schedule (alternative to cron)
154+
155+
```yaml
156+
jobs:
157+
weekly:
158+
schedule: "7pm every Thursday" # Human-readable schedule
159+
run: ./weekly-report.sh
160+
161+
daily:
162+
schedule: "every day at 4:00 pm" # Also supported
163+
run: ./backup.sh
164+
165+
frequent:
166+
schedule: "every 5 minutes" # Interval-based
167+
run: ./health-check.sh
168+
```
169+
170+
Supported patterns (via [english-to-cron](https://github.com/kaplanelad/english-to-cron)):
171+
- `"every minute"`, `"every 5 minutes"`
172+
- `"every day at 4:00 pm"`, `"at 10:00 am"`
173+
- `"7pm every Thursday"`, `"Sunday at 12:00"`
174+
- `"midnight on Tuesdays"`, `"midnight on the 1st and 15th"`
175+
176+
Standard cron syntax is tried first; English is used as fallback.
177+
153178
### Full syntax (all options)
154179
155180
```yaml
@@ -225,7 +250,7 @@ mise exec -- cargo test # Run tests
225250
2. **Tar available**: `tar` command for archive extraction
226251
3. **Shell available**: Jobs run via `sh -c "<command>"`
227252
4. **Remote auth**: SSH keys or credentials pre-configured for remote repos
228-
5. **Cron format**: Standard 5-field cron (via `croner` crate)
253+
5. **Schedule format**: Standard cron or English phrases (via `croner` + `english-to-cron`)
229254

230255
## Key Flows
231256

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2024"
66
[dependencies]
77
tokio = { version = "1", features = ["full"] }
88
croner = "3"
9+
english-to-cron = "0.1"
910
chrono = "0.4"
1011
chrono-tz = "0.10"
1112
anyhow = "1"

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ jobs:
4949
delay: 10s
5050
```
5151
52+
### Human-readable schedules
53+
54+
```yaml
55+
jobs:
56+
weekly:
57+
schedule: "7pm every Thursday"
58+
run: ./weekly-report.sh
59+
60+
daily:
61+
schedule: "every day at 4:00 pm"
62+
run: ./backup.sh
63+
64+
frequent:
65+
schedule: "every 5 minutes"
66+
run: ./health-check.sh
67+
```
68+
5269
### Build and run (compiled languages)
5370
5471
```yaml
@@ -178,13 +195,13 @@ Options:
178195

179196
#### `jobs.<job-id>.schedule`
180197

181-
Shorthand: `schedule: "*/5 * * * *"`
198+
Shorthand: `schedule: "*/5 * * * *"` or `schedule: "7pm every Thursday"`
182199

183200
Full form:
184201

185202
| Field | Type | Default | Description |
186203
|-------|------|---------|-------------|
187-
| `cron` | string | **required** | Cron expression (5 fields: min hour day month weekday) |
204+
| `cron` | string | **required** | Cron expression or English phrase |
188205
| `timezone` | string, optional | runner's | Job-specific timezone override |
189206

190207
#### `jobs.<job-id>.build` (optional)
@@ -248,7 +265,9 @@ Full form:
248265

249266
**Size**: `512K`, `10M`, `1G`, or bytes
250267

251-
**Cron**: `min hour day month weekday` (e.g., `*/5 * * * *` = every 5 minutes)
268+
**Schedule**: Cron or English phrase
269+
- Cron: `min hour day month weekday` (e.g., `*/5 * * * *` = every 5 minutes)
270+
- English: `every 5 minutes`, `every day at 4:00 pm`, `7pm every Thursday`, `Sunday at 12:00`, `midnight on Tuesdays`
252271

253272
### Environment variable priority
254273

src/config.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,7 @@ fn parse_job(
313313
ScheduleConfigRaw::Full(full) => (full.cron, full.timezone),
314314
};
315315

316-
let schedule = Cron::from_str(&cron_expr)
317-
.map_err(|e| anyhow!("Invalid cron '{}': {}", cron_expr, e))?;
316+
let schedule = parse_schedule(&cron_expr)?;
318317

319318
// Extract run config
320319
let (run_sh, run_timeout, run_concurrency, run_retry, run_working_dir, run_env_file, run_env) =
@@ -456,6 +455,20 @@ fn parse_size(s: &str) -> Result<u64> {
456455
}
457456
}
458457

458+
/// Parse schedule expression - supports both cron syntax and English phrases
459+
fn parse_schedule(expr: &str) -> Result<Cron> {
460+
// Try standard cron first
461+
Cron::from_str(expr).or_else(|cron_err| {
462+
// Try English phrase (e.g., "7pm every Thursday")
463+
english_to_cron::str_cron_syntax(expr)
464+
.map_err(|_| anyhow!("Invalid schedule '{}': {}", expr, cron_err))
465+
.and_then(|converted| {
466+
Cron::from_str(&converted)
467+
.map_err(|e| anyhow!("Invalid schedule '{}' (converted to '{}'): {}", expr, converted, e))
468+
})
469+
})
470+
}
471+
459472
#[cfg(test)]
460473
mod tests {
461474
use super::*;
@@ -1559,4 +1572,59 @@ jobs:
15591572
assert_eq!(jobs[0].timeout, Duration::from_secs(30));
15601573
assert_eq!(jobs[0].log_file.as_deref(), Some("app.log"));
15611574
}
1575+
1576+
#[test]
1577+
fn parse_english_schedule() {
1578+
let yaml = r#"
1579+
jobs:
1580+
weekly:
1581+
schedule: "7pm every Thursday"
1582+
run: echo weekly
1583+
"#;
1584+
let (_, jobs) = parse_config(yaml).unwrap();
1585+
assert_eq!(jobs.len(), 1);
1586+
assert_eq!(jobs[0].command, "echo weekly");
1587+
}
1588+
1589+
#[test]
1590+
fn parse_english_schedule_daily() {
1591+
let yaml = r#"
1592+
jobs:
1593+
daily:
1594+
schedule: "every day at 4:00 pm"
1595+
run: echo daily
1596+
"#;
1597+
let (_, jobs) = parse_config(yaml).unwrap();
1598+
assert_eq!(jobs.len(), 1);
1599+
}
1600+
1601+
#[test]
1602+
fn parse_english_schedule_every_minute() {
1603+
let yaml = r#"
1604+
jobs:
1605+
frequent:
1606+
schedule: "every 5 minutes"
1607+
run: echo ping
1608+
"#;
1609+
let (_, jobs) = parse_config(yaml).unwrap();
1610+
assert_eq!(jobs.len(), 1);
1611+
}
1612+
1613+
#[test]
1614+
fn parse_english_schedule_with_full_syntax() {
1615+
let yaml = r#"
1616+
jobs:
1617+
test:
1618+
schedule:
1619+
cron: "Sunday at 12:00"
1620+
timezone: Asia/Tokyo
1621+
run: echo sunday
1622+
"#;
1623+
let (_, jobs) = parse_config(yaml).unwrap();
1624+
assert_eq!(jobs.len(), 1);
1625+
assert_eq!(
1626+
jobs[0].timezone,
1627+
Some(TimezoneConfig::Named(chrono_tz::Asia::Tokyo))
1628+
);
1629+
}
15621630
}

0 commit comments

Comments
 (0)