Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/cli/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,18 @@
"double_dash": "Optional",
"hide": false
}
},
{
"name": "cron-immediate",
"usage": "--cron-immediate",
"help": "Trigger cron immediately on first check (default: false)",
"help_first_line": "Trigger cron immediately on first check (default: false)",
"short": [],
"long": [
"cron-immediate"
],
"hide": false,
"global": false
}
],
"mounts": [],
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/config/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,7 @@ Cron schedule expression (6 fields: second minute hour day month weekday)
### `--cron-retrigger <CRON_RETRIGGER>`

Cron retrigger behavior: finish, always, success, fail

### `--cron-immediate`

Trigger cron immediately on first check (default: false)
10 changes: 10 additions & 0 deletions docs/guides/scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ Uses standard 6-field cron format:
- `0 0 0 * * 0` - Weekly on Sunday at midnight
- `0 30 9 * * 1-5` - Weekdays at 9:30 AM

## First Trigger on Start

By default, a cron daemon does **not** execute immediately when you run `pitchfork start`. It waits for the next scheduled time. If you want a scheduled time within the last 10 seconds before startup to also trigger the daemon, set `immediate = true`:

```toml
[daemons.backup]
run = "./backup.sh"
cron = { schedule = "0 0 2 * * *", immediate = true }
```

Comment thread
greptile-apps[bot] marked this conversation as resolved.
## Retrigger Modes

Control what happens when the schedule triggers while the previous run is still active:
Expand Down
6 changes: 5 additions & 1 deletion docs/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
]
},
"PitchforkTomlCron": {
"description": "Cron scheduling: a cron expression string, or { schedule, retrigger } object",
"description": "Cron scheduling: a cron expression string, or { schedule, retrigger, immediate } object",
"oneOf": [
{
"description": "Cron expression (e.g. '0 * * * *')",
Expand All @@ -129,6 +129,10 @@
{
"type": "object",
"properties": {
"immediate": {
"description": "Trigger immediately on first check (default: false)",
"type": "boolean"
},
"retrigger": {
"$ref": "#/$defs/CronRetrigger"
},
Expand Down
1 change: 1 addition & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ cron = { schedule = "0 0 2 * * *", retrigger = "always" }
**Fields:**
- `schedule` - Cron expression (6 fields: second, minute, hour, day, month, weekday)
- `retrigger` - Behavior when schedule fires: `"finish"` (default), `"always"`, `"success"`, `"fail"`
- `immediate` - Also fire if a scheduled time occurred within the 10 seconds before the daemon started. Default: `false`

### `mise`

Expand Down
1 change: 1 addition & 0 deletions pitchfork.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ cmd config help="manage/edit pitchfork.toml files" {
flag --cron-retrigger help="Cron retrigger behavior: finish, always, success, fail" {
arg <CRON_RETRIGGER>
}
flag --cron-immediate help="Trigger cron immediately on first check (default: false)"
arg <ID> help="ID of the daemon to add (e.g., \"api\" or \"namespace/api\")"
arg "[ARGS]…" help="Arguments to pass to the daemon (alternative to --run)" required=#false double_dash=automatic var=#true
}
Expand Down
4 changes: 4 additions & 0 deletions src/cli/config/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ pub struct Add {
/// Cron retrigger behavior: finish, always, success, fail
#[clap(long)]
cron_retrigger: Option<String>,
/// Trigger cron immediately on first check (default: false)
#[clap(long)]
cron_immediate: bool,
Comment on lines +123 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
# Verify that --cron-immediate currently has no declared dependency and is conditionally ignored.
rg -n -C3 'cron_immediate|cron_schedule|PitchforkTomlCron' src/cli/config/add.rs

Repository: endevco/pitchfork

Length of output: 2203


🌐 Web query:

endetvco/pitchfork src/cli/config/add.rs cron_immediate cron_schedule

💡 Result:

In the context of the pitchfork CLI (endevco/pitchfork), cron_immediate and cron_schedule are related to the configuration of daemons managed by the tool. Pitchfork allows users to run daemons on a schedule using cron expressions [1]. While the specific identifiers cron_immediate and cron_schedule do not appear as top-level CLI command flags for pitchfork config add, they correspond to components of the cron configuration within the project configuration file (pitchfork.toml) [2][1]. When configuring a daemon with a cron schedule, the full form of the configuration uses a table containing a schedule field [1]: [daemons.example] run = "./script.sh" cron = { schedule = "0 0 2 * * *", retrigger = "finish" } The configuration logic within the pitchfork codebase handles how these schedules are interpreted and executed. Specifically, the supervisor component of pitchfork includes a Cron Watcher that runs at a configurable interval (defaulting to 10 seconds) to check if any cron-scheduled daemons should be triggered [3][4][5]. The retrigger modes (such as finish, always, success, and fail) define how the system behaves if a scheduled task is triggered while a previous execution is still active [1].

Citations:


🌐 Web query:

endevco/pitchfork "src/cli/config/add.rs" cron_immediate cron_schedule

💡 Result:

In the endevco/pitchfork project, cron scheduling is managed via a configuration field in the daemon definition [1]. While the specific file path src/cli/config/add.rs relates to the CLI command used to add new daemon configurations [2], the core logic for cron scheduling relies on a 'cron' field in the pitchfork.toml file [1]. This 'cron' field accepts either a cron expression string (shorthand) or an inline table (full form) containing a 'schedule' parameter [1]. The 'cron_schedule' terminology is explicitly used in the full form configuration, such as in: [daemons.backup] run = "./scripts/backup.sh" cron = { schedule = "0 0 2 * * *", retrigger = "finish" } Although you mentioned 'cron_immediate', Pitchfork's scheduling documentation primarily emphasizes recurring 'cron' expressions and 'retrigger' modes (such as 'finish', 'always', and 'success') to control daemon execution behavior [1]. The system checks for these scheduled tasks at an interval defined by the 'supervisor.cron_check_interval' setting (defaulting to 10 seconds) [3][4].

Citations:


🌐 Web query:

raw.githubusercontent.com endevco pitchfork src/cli/config/add.rs cron_immediate

💡 Result:

The query “raw.githubusercontent.com endevco pitchfork src/cli/config/add.rs cron_immediate” appears to be looking for Pitchfork source code in endevco/pitchfork (specifically src/cli/config/add.rs) and a symbol/flag named “cron_immediate”. Based on the authoritative sources I found, Pitchfork supports cron scheduling via the daemon configuration field cron = ... and provides cron “retrigger modes” such as finish (default), always, success, and fail (but I did not find any documentation or evidence for a config/CLI option literally named cron_immediate). In particular, the official Pitchfork cron guide describes the cron field and “Retrigger Modes” and does not mention cron_immediate as a mode or flag. [1] I also found official Pitchfork CLI documentation for pitchfork config add, including arguments --cron-schedule and --cron-retrigger, with retrigger behavior restricted to finish, always, success, and fail. This again does not mention any cron_immediate option. [2] So, with high confidence from the cited docs: there is no documented cron_immediate option in the Pitchfork configuration/CLI interface; cron behavior is controlled via --cron-schedule plus --cron-retrigger. [2][1] If you want, paste the exact raw.githubusercontent.com/... URL you expected (or the exact snippet mentioning “cron_immediate”), and I can locate the precise code path/constant for it in the repository.

Citations:


🌐 Web query:

"cron_immediate" "src/cli/config/add.rs" endevco/pitchfork

💡 Result:

The term cron_immediate does not appear to be a standard or public-facing identifier within the endevco/pitchfork project [1][2][3]. While the endevco/pitchfork CLI does provide functionality for managing cron jobs via the pitchfork config add command, its configuration options for cron are documented as --cron-schedule and --cron-retrigger [4]. It is possible that cron_immediate refers to internal source code or a specific private implementation detail within src/cli/config/add.rs that is not exposed in the public documentation or primary API [4][1]. In similar Rust-based scheduling contexts (such as zino-rs), immediate execution flags are sometimes used to run a job immediately upon initialization or scheduling [5][6], but no direct evidence links the specific string cron_immediate to the endevco/pitchfork codebase [4][1][2].

Citations:


🌐 Web query:

"cron-immediate" endevco/pitchfork clap

💡 Result:

The term cron-immediate does not appear to be an official feature, configuration option, or command in the Pitchfork daemon manager [1][2]. Pitchfork is a tool for managing daemons, including support for cron-based scheduling [1][3]. Its documentation defines retrigger modes for cron jobs, which dictate how the supervisor handles a new scheduled run while a previous one is still active [2]. The currently supported retrigger modes are [2]: finish (default): Only triggers a new run if the previous execution has finished [2]. always: Always triggers a new run, stopping the previous one if it is still active [2]. success: Only triggers if the previous execution succeeded (exit code 0) [2]. fail: Only triggers if the previous execution failed [2]. It is possible that "cron-immediate" was a misinterpreted reference to one of these modes (such as "always," which forces execution) or a misunderstanding of how Pitchfork's CLI interface (built with clap) handles task triggering [2][4]. Pitchfork CLI commands are implemented using the Rust clap crate, and interaction with the supervisor is handled via IPC [4][5].

Citations:


🌐 Web query:

"cron_immediate" "clap(" endevco/pitchfork

💡 Result:

The terms "cron_immediate" and "clap(" do not appear as a combined, functional feature within the endevco/pitchfork project [1][2][3]. Pitchfork is a CLI tool for managing daemons and recurring tasks [1][3]. While it does support cron scheduling [1][3] and uses the Rust clap library for its command-line interface [1][3], the specific string "cron_immediate" is not a recognized configuration or command parameter in the project's source code or documentation [1][4][3]. The search results indicate that "cron_immediate" is a parameter used in other software, such as the betterweb/cron Docker container [5], which allows for immediate execution of scheduled jobs. Additionally, "clap(" is a common pattern in Rust codebases when initializing the clap command-line parser [6][7], but it is not a specific identifier associated with a "cron_immediate" feature in pitchfork. If you are seeing these terms in relation to a specific codebase or configuration file, it is possible they originate from a different tool or a misinterpretation of a separate integration [5].

Citations:


Require --cron-schedule when --cron-immediate is set.

Pitchfork’s cron CLI/docs describe cron configuration via --cron-schedule (and retrigger via --cron-retrigger), so if src/cli/config/add.rs exposes --cron-immediate, it should not become an unused/no-op input. Add clap validation (e.g., requires = "cron_schedule" on the argument) and/or fail fast in run() when --cron-immediate is provided without --cron-schedule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/config/add.rs` around lines 123 - 125, The cron_immediate flag is a
no-op unless cron_schedule is provided; add validation by annotating the
cron_immediate field with clap's requires = "cron_schedule" (e.g., #[clap(long,
requires = "cron_schedule")]) and also add a defensive check in the run()
function (or the impl that processes the parsed args) to return an error or exit
early if cron_immediate is true while cron_schedule is empty, referencing the
cron_immediate and cron_schedule fields and the run() method to ensure both
compile-time clap validation and a runtime fail-fast path.

}

impl Add {
Expand Down Expand Up @@ -217,6 +220,7 @@ impl Add {
Some(PitchforkTomlCron {
schedule: schedule.clone(),
retrigger,
immediate: self.cron_immediate,
})
} else {
None
Expand Down
15 changes: 12 additions & 3 deletions src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,9 @@ pub struct PitchforkTomlCron {
pub schedule: String,
/// Behavior when cron triggers while previous run is still active
pub retrigger: CronRetrigger,
/// Whether to trigger immediately on first check when no prior trigger is recorded.
/// When false (default), the first trigger is deferred until the next scheduled time.
pub immediate: bool,
}

impl JsonSchema for PitchforkTomlCron {
Expand All @@ -637,14 +640,15 @@ impl JsonSchema for PitchforkTomlCron {

fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "Cron scheduling: a cron expression string, or { schedule, retrigger } object",
"description": "Cron scheduling: a cron expression string, or { schedule, retrigger, immediate } object",
"oneOf": [
{ "type": "string", "description": "Cron expression (e.g. '0 * * * *')" },
{
"type": "object",
"properties": {
"schedule": { "type": "string", "description": "Cron expression" },
"retrigger": generator.subschema_for::<CronRetrigger>()
"retrigger": generator.subschema_for::<CronRetrigger>(),
"immediate": { "type": "boolean", "description": "Trigger immediately on first check (default: false)" }
},
"required": ["schedule"]
}
Expand All @@ -659,6 +663,8 @@ pub struct PitchforkTomlCronRaw {
schedule: String,
#[serde(default)]
retrigger: CronRetrigger,
#[serde(default)]
immediate: bool,
}

impl StringOrStruct for PitchforkTomlCron {
Expand All @@ -669,18 +675,20 @@ impl StringOrStruct for PitchforkTomlCron {
Self {
schedule,
retrigger: CronRetrigger::default(),
immediate: false,
}
}

fn from_raw(raw: PitchforkTomlCronRaw) -> std::result::Result<Self, String> {
Ok(Self {
schedule: raw.schedule,
retrigger: raw.retrigger,
immediate: raw.immediate,
})
}

fn is_shorthand(&self) -> bool {
self.retrigger == CronRetrigger::default()
self.retrigger == CronRetrigger::default() && !self.immediate
}

fn to_short(&self) -> String {
Expand All @@ -691,6 +699,7 @@ impl StringOrStruct for PitchforkTomlCron {
PitchforkTomlCronRaw {
schedule: self.schedule.clone(),
retrigger: self.retrigger,
immediate: self.immediate,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub struct Daemon {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cron_retrigger: Option<CronRetrigger>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cron_immediate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_exit_success: Option<bool>,
Expand Down Expand Up @@ -155,6 +157,7 @@ pub struct RunOptions {
pub autostop: bool,
pub cron_schedule: Option<String>,
pub cron_retrigger: Option<CronRetrigger>,
pub cron_immediate: Option<bool>,
pub retry: Retry,
pub retry_count: u32,
pub ready_delay: Option<u64>,
Expand Down Expand Up @@ -237,6 +240,7 @@ impl Daemon {
autostop: self.autostop,
cron_schedule: self.cron_schedule.clone(),
cron_retrigger: self.cron_retrigger,
cron_immediate: self.cron_immediate,
retry: self.retry,
retry_count: self.retry_count,
ready_delay: self.ready_delay,
Expand Down
1 change: 1 addition & 0 deletions src/pitchfork_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,7 @@ impl PitchforkTomlDaemon {
autostop: self.auto.contains(&PitchforkTomlAuto::Stop),
cron_schedule: self.cron.as_ref().map(|c| c.schedule.clone()),
cron_retrigger: self.cron.as_ref().map(|c| c.retrigger),
cron_immediate: self.cron.as_ref().map(|c| c.immediate),
retry: self.retry,
retry_count: 0,
ready_delay: self.ready_delay,
Expand Down
1 change: 1 addition & 0 deletions src/supervisor/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ impl Supervisor {
o.autostop = opts.autostop;
o.cron_schedule = opts.cron_schedule.clone();
o.cron_retrigger = opts.cron_retrigger;
o.cron_immediate = opts.cron_immediate;
o.retry = Some(opts.retry);
o.retry_count = Some(opts.retry_count);
o.ready_delay = opts.ready_delay;
Expand Down
4 changes: 4 additions & 0 deletions src/supervisor/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) struct UpsertDaemonOpts {
pub autostop: bool,
pub cron_schedule: Option<String>,
pub cron_retrigger: Option<CronRetrigger>,
pub cron_immediate: Option<bool>,
pub last_exit_success: Option<bool>,
pub retry: Option<Retry>,
pub retry_count: Option<u32>,
Expand Down Expand Up @@ -138,6 +139,9 @@ impl Supervisor {
cron_retrigger: opts
.cron_retrigger
.or(existing.and_then(|d| d.cron_retrigger)),
cron_immediate: opts
.cron_immediate
.or(existing.and_then(|d| d.cron_immediate)),
last_cron_triggered: existing.and_then(|d| d.last_cron_triggered),
last_exit_success: opts
.last_exit_success
Expand Down
28 changes: 24 additions & 4 deletions src/supervisor/watchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,30 @@ impl Supervisor {
};

// Check if we should trigger: look for a scheduled time that has passed
// since our last trigger (or last 10 seconds if never triggered)
let check_since = daemon
.last_cron_triggered
.unwrap_or_else(|| now - chrono::Duration::seconds(10));
// since our last trigger.
let check_since = match daemon.last_cron_triggered {
Some(t) => t,
None => {
if daemon.cron_immediate.unwrap_or(false) {
// immediate=true: restore the old look-back behavior.
// A scheduled time within the last 10 seconds before startup
// will trigger immediately.
now - chrono::Duration::seconds(10)
} else {
// immediate=false (default): anchor last_cron_triggered to now
// so the next scheduled time is picked up, without firing now.
let mut state_file = self.state_file.lock().await;
if state_file.set_last_cron_triggered(&id, now) {
if let Err(e) = state_file.write() {
error!(
"failed to persist last_cron_triggered for daemon {id}: {e}"
);
}
}
continue;
}
}
};

// Find if there's a scheduled time between check_since and now
let should_trigger = schedule
Expand Down
14 changes: 14 additions & 0 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@ impl EditorState {
"Cron Retrigger",
"Behavior when cron triggers while previous run is active.",
),
FormField::optional_bool(
"cron_immediate",
"Cron Immediate",
"Trigger immediately on first check (default: false).",
),
]
}

Expand Down Expand Up @@ -504,6 +509,10 @@ impl EditorState {
.unwrap_or(CronRetrigger::Finish),
);
}
"cron_immediate" => {
field.value =
FormFieldValue::OptionalBoolean(config.cron.as_ref().map(|c| c.immediate));
}
_ => {}
}
}
Expand All @@ -520,6 +529,7 @@ impl EditorState {

let mut cron_schedule: Option<String> = None;
let mut cron_retrigger = CronRetrigger::Finish;
let mut cron_immediate = false;

for field in &self.fields {
match (field.name, &field.value) {
Expand Down Expand Up @@ -558,6 +568,9 @@ impl EditorState {
("watch", FormFieldValue::StringList(v)) => config.watch = v.clone(),
("cron_schedule", FormFieldValue::OptionalText(s)) => cron_schedule = s.clone(),
("cron_retrigger", FormFieldValue::Retrigger(r)) => cron_retrigger = *r,
("cron_immediate", FormFieldValue::OptionalBoolean(b)) => {
cron_immediate = b.unwrap_or(false);
}
_ => {}
}
}
Expand All @@ -566,6 +579,7 @@ impl EditorState {
config.cron = Some(PitchforkTomlCron {
schedule,
retrigger: cron_retrigger,
immediate: cron_immediate,
});
}

Expand Down
30 changes: 30 additions & 0 deletions tests/test_pitchfork_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,36 @@ retrigger = "always"
let cron = daemon.cron.as_ref().unwrap();
assert_eq!(cron.schedule, "0 0 * * *");
assert_eq!(cron.retrigger, pitchfork_toml::CronRetrigger::Always);
assert!(!cron.immediate);

Ok(())
}

/// Test daemon with cron.immediate = true
#[test]
fn test_daemon_with_cron_immediate() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let toml_path = temp_dir.path().join("pitchfork.toml");

let toml_content = r#"
[daemons.cron_daemon]
run = "echo 'cron job'"

[daemons.cron_daemon.cron]
schedule = "0 0 * * *"
immediate = true
"#;

fs::write(&toml_path, toml_content).unwrap();

let pt = pitchfork_toml::PitchforkToml::read(&toml_path)?;
let daemon = get_daemon_by_name(&pt, "cron_daemon").unwrap();

assert!(daemon.cron.is_some());
let cron = daemon.cron.as_ref().unwrap();
assert_eq!(cron.schedule, "0 0 * * *");
assert_eq!(cron.retrigger, pitchfork_toml::CronRetrigger::Finish);
assert!(cron.immediate);

Ok(())
}
Expand Down
Loading