Spec: Cron Jobs
Issue: MAR-25 Date: 2026-04-24 Status: In Review
Overview
Cron jobs schedule recurring tasks on deployed servers. Shipper configures them via CronConfig and delegates to provider-specific implementations (Ploi, Forge). Each entry maps a human-readable name to a command, frequency expression, and system user.
Current Implementation
Key Classes / Files
| Class/File | Role |
|---|---|
App\Config\CronConfig |
Value object holding cron config (command, frequency, user) |
App\Deployment\Contracts\CronManagerInterface |
Interface defining plan() and apply() |
App\Deployment\Providers\Ploi\PloiCronManager |
Creates cron jobs via Ploi API |
App\Deployment\Providers\Forge\ForgeCronManager |
Creates cron jobs via Forge API |
tests/Unit/Config/CronConfigTest.php |
Unit tests for config and getters |
Functional Requirements
FR-001 — Cron Config with Defaults
CronConfig requires command and frequency as positional arguments. The user defaults to 'ploi'.
FR-002 — Named Cron Entries
A project exposes multiple named cron jobs via ProjectConfig::cron(), returning array<string, CronConfig>. Each entry is a distinct scheduled task.
FR-003 — Plan Output
plan() returns a list of actions, one per cron job, describing the frequency.
FR-004 — Apply via Provider
apply() iterates over array<string, CronConfig> and calls the provider API to create each cron job. The {site} placeholder in the command is replaced with the actual site domain before sending to the API.
FR-005 — User Context
Cron jobs run under a configurable system user (defaults to ploi). Both providers accept a user parameter.
Configuration Interface
cron:
scheduler:
command: php artisan schedule:run
frequency: "* * * * *"
user: ploi
backup:
command: php artisan backup:run
frequency: "0 2 * * *"
user: www-data
reports:
command: php artisan reports:generate
frequency: "0 9 * * 1"
user: ploi
Data Contracts
// CronConfig
final class CronConfig {
public function __construct(
private readonly string $command,
private readonly string $frequency,
private readonly string $user = 'ploi',
) {}
public function command(): string { ... }
public function frequency(): string { ... }
public function user(): string { ... }
}
// CronManagerInterface
interface CronManagerInterface {
/** @return array<string> */
public function plan(DeploymentContext $context): array;
/** @param array<string, CronData> $cron */
public function apply(SiteContext $site, array $cron): OperationResult;
}
Edge Cases
- Empty cron config:
plan()returns an empty list;apply()succeeds with no-op. - API failure on one cron: Returns
OperationResult::fail()immediately with the cron name in the message. - Invalid frequency expression: Not validated at config level; passed as-is to provider API.
{site}placeholder not present:str_replaceleaves the command unchanged.- Root user cron: Valid but potentially risky; no guard in place.
Acceptance Criteria
-
CronConfigdefaults produceuser = 'ploi'when only command and frequency are provided -
plan()output format matches"Create cron job: {$cronName} ({$cronConfig->frequency()})" -
{site}in command is replaced with$site->domainbefore calling provider API - Both providers pass
usercorrectly to their respective API methods - Unit tests in
CronConfigTest.phpcontinue to pass
Open Questions / Potential Concerns
- Should there be validation for frequency expression format?
- Should
rootbe allowed as a cron user, or should that be blocked? - Is there a limit on how many cron jobs a single site can register?