Scheduled pipelines
triggers.schedule (inside cicd({ pipeline: { triggers } }))
declares cron schedules in code, each in an optional local IANA timezone.
GitHub Actions and Azure Pipelines only understand UTC cron, so Zuke
compiles a { cron, tz } entry into the UTC cron(s) that fire
at the intended local wall-clock time.
Concept
A daylight-saving zone uses two UTC offsets across the year, so the
intended local time maps to two UTC crons (one per
offset). Zuke registers both and generates a wall-clock guard so only the
firing at the currently-correct offset proceeds. A fixed-offset zone (or
plain UTC / an omitted tz) needs a single cron and no guard.
Offsets are sampled from a pinned reference year (2024), so the generated crons never change as the calendar advances — the same input always produces the same output.
ScheduleEntry API
Each entry is a plain 5-field cron plus an optional IANA zone:
/** A scheduled trigger: a 5-field cron expression in an optional IANA timezone. */
export interface ScheduleEntry {
cron: string; // standard 5-field: minute hour day-of-month month day-of-week
tz?: string; // IANA zone (e.g. "Europe/Sofia"); omitted/"UTC" = already UTC, emitted verbatim
}
// on CiTriggers: schedule?: ScheduleEntry[];
// lives at spec.pipeline.triggers.schedule inside cicd(spec)
export function cicd(spec: CiFileSpec): CiFile
export interface CiFileSpec {
provider: CiProvider; // "github" | "gitlab" | "azure" | "bitbucket"
path?: string;
pipeline?: CiPipeline; // { name?, triggers?, permissions?, concurrency?, jobs? }
fanOut?: boolean | FanOutOptions;
}
// helpers: utcCronsFor(entry): string[]; scheduleNeedsGuard(entry): boolean;
// anyScheduleNeedsGuard(schedule): boolean; guardShell(schedule): string The canonical example — weekday mornings and afternoons, Sofia local time:
ci = cicd({
provider: "github",
pipeline: {
triggers: {
push: ["main"],
// Weekday mornings and afternoons, Sofia local time.
schedule: [{ cron: "30 9,13,15 * * 1-4", tz: "Europe/Sofia" }],
},
},
}); Per-provider compilation
| Provider | Behavior |
|---|---|
| GitHub (full) | on.schedule is the de-duplicated union of
utcCronsFor(entry) across all entries. The Sofia example
compiles to two UTC crons — EET (UTC+2): 30 7,11,13 * * 1-4,
EEST (UTC+3): 30 6,10,12 * * 1-4. If any entry needs a
guard, Zuke emits a zuke-schedule-guard job (runs on
ubuntu-latest, outputs run) that reads
TZ='<zone>' date and compares the wall-clock; every
other job gets needs: [..., zuke-schedule-guard] and an
if: ANDing
needs.zuke-schedule-guard.outputs.run == 'true'. On
non-schedule events the guard sets run=true
(push/PR/manual always proceed); on a scheduled event only the
firing at the correct offset proceeds.
|
| Azure |
Native schedules: for UTC/fixed-offset only —
{ cron, branches: { include: [...] }, always: true }.
A DST-zone entry throws (the guard is GitHub-only).
|
| GitLab & Bitbucket |
Documented limitation — the schedule trigger is
ignored. Schedules are configured in the provider web UI, not in the
generated file.
|
A DST zone registers two UTC crons, so it "fires" twice as often across the year on GitHub; the guard suppresses the off-offset firing and lets non-schedule events through unconditionally.
Cron grammar
Zuke supports a deliberate subset of cron syntax — five fields:
- minute (0–59), hour (0–23), day-of-month (1–31), month (1–12), day-of-week (0–7)
- each field:
*, a single number, comma lists, ranges (a-b), or slash steps (*/nora-b/n) - day-of-week
0and7both mean Sunday - the timezone must have a whole-hour UTC offset
Errors
An unsupported schedule is reported before any file is generated, with a friendly message pointing at the fix — wrong field count, named fields/hashes/non-numeric values, bad steps, out-of-range values, an unknown timezone, a fractional-hour zone, or a day-crossing shift while a day field is constrained:
cicd: unsupported schedule — <reason>. Write the cron in UTC directly (drop the tz), or simplify it.
# Azure, DST-zone entry:
cicd: Azure Pipelines schedules are UTC-only and Zuke's daylight-saving guard is
GitHub-only. Use a fixed-offset timezone, or write the cron in UTC, for the azure provider. Drift & determinism
tz omitted, "", or "UTC" emits the
cron verbatim — single, no guard. Overlapping UTC crons across entries are
collapsed via a Set. Because offsets are sampled from a pinned
reference year, the output is deterministic:
zuke generate-ci --check # fails on drift: "Run zuke generate-ci and commit the result."
zuke generate-ci # running the build also auto-regenerates zuke generate-ci --check verifies the on-disk file matches
the rendered output and fails on drift; running the build also
auto-regenerates the file. See
Code-first CI/CD for the rest of the
cicd() surface — providers, fan-out, and the portable step
subset.