feat(cron): add Bun.cron API for OS-level cron jobs and expression parsing#26999
feat(cron): add Bun.cron API for OS-level cron jobs and expression parsing#26999Jarred-Sumner wants to merge 6 commits intomainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a Bun.cron feature: docs and types, a new Bun.cron JS API (register/remove/parse), Zig implementation (parser and cross-platform installers), CLI flags and cron execution mode, UTC date-time bindings, runtime wiring, and tests. Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
4bbbdf7 to
840dacd
Compare
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Fix all issues with AI agents
In `@packages/bun-types/bun.d.ts`:
- Line 7274: The example for Bun.cron.parse uses Date.UTC(2025, 6, 1) which
incorrectly sets the month to July because Date.UTC months are 0-based; update
the example so the Date.UTC month matches the cron string—either change
Date.UTC(2025, 6, 1) to Date.UTC(2025, 0, 1) to represent January, or change the
cron expression "0 0 1 JAN *" to use "JUL" to match month index 6; adjust the
example so Bun.cron.parse and Date.UTC refer to the same month.
- Around line 7259-7262: The example defining the variable nextJan1 uses
Date.UTC with month index 6 (July) but should represent January; update the
Date.UTC call in the nextJan1 example from Date.UTC(2025, 6, 1) to
Date.UTC(2025, 0, 1) so the month is correctly January (JavaScript Date months
are 0-based).
In `@src/bun.js.zig`:
- Around line 224-229: Replace std.posix.getcwd call with bun.sys.getcwd and
handle the Maybe return using a switch to extract the .result or handle .err;
ensure you allocate a buffer large enough for cwd + trigger.len (use
bun.PathBuffer or a larger custom buffer instead of the current fixed-size
eval_path_buf if needed), check the resulting cwd length against your buffer
capacity before memcpy, and propagate or log the bun.sys.Error on the error
branch rather than ignoring it; update references to cwd_slice, eval_path_buf,
and eval_entry_path accordingly.
In `@src/bun.js/api/cron_parser.zig`:
- Around line 35-61: Replace all uses of std.mem string helpers with bun.strings
SIMD-accelerated variants: change std.mem.trim(u8, input, " \t") in the cron
parser to bun.strings.trim, change std.mem.eql(u8, fields[2], "*") and
std.mem.eql(u8, fields[4], "*") to bun.strings.eql, and update helper functions
splitRange, parseWeekdayName, and parseMonthName to use bun.strings.indexOf /
bun.strings.indexOfScalar and bun.strings.eql instead of
std.mem.indexOfScalar/std.mem.eql; keep the same argument types and semantics so
parseNickname, parseField, and the returned struct usage remain unchanged.
In `@src/bun.js/api/cron.zig`:
- Around line 933-955: cronToCalendarInterval currently treats each cron field
as either "*" or a single integer (using std.fmt.parseInt) which rejects
ranges/lists/steps; fix by expanding fields into explicit integer lists and
emitting one <dict> per StartCalendarInterval combination: replace the
single-value parse in cronToCalendarInterval with a parser that returns []i32
for each field (supporting "*", single value, ranges, comma-separated lists, and
step syntax), then compute the cartesian product of those five integer lists and
for each tuple generate the same "<key>...<integer>..." block (append a separate
" <dict>\n... </dict>\n" for each combination to result). Ensure you keep
the existing keys array and result Managed(u8) usage, free any allocated temp
strings, and return error.InvalidCron on malformed field syntax.
- Around line 213-226: The temp file creation uses predictable names via
allocPrintZ and bun.sys.File.openat with O_CREAT|O_TRUNC (assigned to
this.tmp_path and written via file.writeAll), which is vulnerable to
symlink/TOCTOU attacks; change to create a secure unique temp file
(mkstemp-style) or open with O_EXCL and a cryptographically random suffix and
retry on EEXIST, or create a private temp directory per-run and use a randomized
filename, then store that secure path in this.tmp_path and proceed to write via
file.writeAll; ensure proper cleanup on errors and keep using defer file.close()
after successful open.
- Around line 744-749: The property name strings on the cron object are created
with jsc.ZigString.static; update getCronObject so the two cron_fn.put calls use
bun.String.static for the property keys instead (replace
jsc.ZigString.static("remove") and jsc.ZigString.static("parse") with
bun.String.static("remove") and bun.String.static("parse") respectively) to
follow the JS interop guideline; keep the same targets (cron_fn, remove_fn,
parse_fn) and call signatures.
- Around line 483-491: The parsed cron time/day values are being discarded:
extend the SchtasksParams struct to include start_time (string for /st) and
day_spec (string for /d), update cronToSchtasks to populate these fields for
DAILY (set start_time from parsed minute/hour) and WEEKLY (set start_time and
day_spec from parsed minute/hour/dow), and then modify startWindows where argv
is built to append "/st", schtasks_params.start_time.ptr and, for WEEKLY
schedules, "/d", schtasks_params.day_spec.ptr (only when non-null) alongside the
existing "/sc" and "/mo" so the schtasks command uses the intended start time
and day.
- Around line 256-260: Replace uses of bun.getenvZ with the type-safe cached
accessors: where the code currently calls bun.getenvZ("HOME") (producing the
local variable home and following this.setErr/this.finish error handling),
change it to bun.env_var.HOME.get() and adjust the nil/optional check
accordingly; likewise replace bun.getenvZ("HOME") at the other occurrence and
replace bun.getenvZ("PATH") with bun.env_var.PATH.get(); keep the existing error
handling logic (this.setErr and this.finish) but use the new accessor's
return/optional semantics.
- Around line 894-905: The resolvePath function currently checks POSIX-only
absolute paths and concatenates with "/" and std APIs; change it to use bun.path
utilities: replace the leading-slash check with bun.path.isAbsolute(.auto,
path), get the caller directory via bun.path.dirname(caller_path.slice(),
.auto), allocate a bun.PathBuffer instead of a std.fs.max_path_bytes buffer, and
produce the joined absolute string using bun.path.joinAbsStringBufZ(&pathBuf,
caller_dir, path, bun.default_allocator) (or the exact bun.path.join API
available) then dup the resulting Z string with bun.default_allocator.dupeZ;
update any error handling to return error.PathTooLong as before.
- Around line 877-884: The code uses std.mem string ops (std.mem.splitScalar,
std.mem.eql, std.mem.trim, std.mem.startsWith) on the lines loop that compares
against marker and elsewhere; replace these with the bun.strings equivalents for
SIMD acceleration: use bun.strings.splitScalar (or bun.strings.split if
available) for splitting, bun.strings.trim for trimming, bun.strings.eql for
equality checks, and bun.strings.startsWith for prefix checks; update usages
around the variables lines, line, marker, and skip_next and in the later block
(previously lines ~981-999) to call bun.strings.* so the signatures/types match
(convert to the expected slice/array types if needed) and keep the same control
flow behavior.
In `@src/cli/Arguments.zig`:
- Around line 828-833: Ensure --cron-title and --cron-period are provided
together: after reading args.option for both (the current assignments to
ctx.runtime_options.cron_title and ctx.runtime_options.cron_period), add a
validation that if one of ctx.runtime_options.cron_title or
ctx.runtime_options.cron_period is set and the other is not, emit a clear error
and exit/fail-fast (use the same error reporting mechanism used elsewhere in
this module). Update the logic around args.option(...) handling so you check
both values after parsing and return a descriptive error when only one flag is
present.
In `@test/js/bun/cron/cron.test.ts`:
- Around line 13-43: The clearTestCrontabs helper currently requires fs
dynamically and only removes bun-marked entries, leaving manually injected
crontab lines behind; move the fs import (writeFileSync, unlinkSync) to module
scope, modify clearTestCrontabs to snapshot the original crontab via
readCrontab() at test start and restore it in a finally block, and extract the
temporary-file + crontab-write logic into a shared helper (e.g.,
writeCrontabTempAndInstall) used by clearTestCrontabs so the original crontab
(including non-bun entries like the test's "0 0 * * * /usr/bin/some-other-job")
is always restored; update references to readCrontab and
Bun.spawnSync("/usr/bin/crontab", tmpFile) accordingly.
da28213 to
e46fe50
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@docs/runtime/cron.mdx`:
- Around line 116-121: The example's inline comment contradicts the POSIX OR
behavior: update the comment next to Bun.cron.parse("0 0 15 * FRI") to reflect
OR semantics (e.g., "Fires on the 15th of every month OR every Friday") and
ensure surrounding explanatory text explicitly states that when both
day-of-month and day-of-week are specified the expression matches if either
condition is true; keep the symbol Bun.cron.parse and the sample cron string
unchanged.
In `@packages/bun-types/bun.d.ts`:
- Around line 7259-7262: The JSDoc example for the parameter named "expression"
contains an unescaped "*/" which prematurely closes the comment; update the
example string in the JSDoc (the `"*/15 * * * *"` part) to escape the slash
(e.g. change `*/` to `*\/`) so the comment doesn't terminate early and
TypeScript can parse the declaration for the function documented by these params
(`expression`, `relativeDate`).
In `@src/bun.js.zig`:
- Around line 217-223: The generated JS string for cron_script currently
interpolates ctx.runtime_options.cron_period raw into std.fmt.allocPrint, which
can break the script if the period contains quotes, backslashes or line
terminators; modify the allocPrint call that builds cron_script to run
ctx.runtime_options.cron_period through escapeForJSString (the same helper used
for escaped_path) and pass that escaped_period into the format args so the
embedded "{s}" for the period is safe; update any variable name used (e.g.,
escaped_period) and ensure the call site inside the cron_script format uses that
escaped value.
- Around line 230-233: The Source stored in vm.module_loader.eval_source is
holding slices into the stack buffer (eval_entry_path from eval_path_buf) so
they become dangling after boot() returns; to fix, ensure a heap-owned path is
passed into logger.Source.initPathString (or fs.Path.init) instead of the stack
buffer: allocate a dupe/copy of eval_entry_path on the heap (using
bun.default_allocator.dupe or equivalent) before calling
logger.Source.initPathString (or add/use a variant that accepts/assumes
heap-owned buffers), assign that heap buffer to script_source and only then set
vm.module_loader.eval_source, and keep the existing run.entry_path dupe logic
consistent so both run.entry_path and script_source.path reference heap-owned
memory.
In `@src/bun.js/api/BunObject.zig`:
- Around line 71-73: The inline `@import` call used in the cron property
initializer should be moved to a file-level constant: add a bottom-of-file
import binding (e.g., const cronModule = `@import`("./cron.zig");) and change the
property initializer from
toJSLazyPropertyCallback(`@import`("./cron.zig").getCronObject) to
toJSLazyPropertyCallback(cronModule.getCronObject); do the same for the other
inline imports referenced around lines 141-144 so all `@import` usages are
declared as file-level constants and never used inline inside initializers or
functions.
In `@src/bun.js/api/cron.zig`:
- Around line 989-994: The current use of a thread-local static buffer
(static_min in cronToSchtasks) is fragile because it returns a pointer into
mutable static storage that startWindows and spawnCmd currently consume
immediately; change cronToSchtasks to return an owned allocated string (e.g.,
[]u8 or []const u8 from the allocator) instead of &static_min.buf so each call
gets its own buffer, copy n into that allocation using `@memcpy`, and update
callers (startWindows / spawnCmd) to accept and free (or let allocator-managed
scope drop) the returned allocation; alternatively, if you prefer minimal
change, add a clear comment on cronToSchtasks that documents the requirement
that the returned pointer must be consumed synchronously and may be corrupted by
subsequent calls.
- Around line 1028-1032: The allocPrintZ helper does an extra allocation by
calling std.fmt.allocPrint to create slice, copying it with allocator.dupeZ (to
add a NUL) and freeing the original; replace this with a single allocation
approach — either call std.fmt.allocPrintZ(allocator, fmt, args) if available,
or allocate a buffer sized slice.len + 1 up front and format directly into it so
you can return the NUL-terminated buffer without the intermediate alloc/free
(update the function allocPrintZ and remove the unnecessary allocator.free of
the temporary slice and allocator.dupeZ usage).
Code ReviewNewest first 🔴 c0bec — 3 issue(s) found
Previous reviews🔴 b3629 — 3 issue(s) found
🔴 317a4 — 1 issue(s) found
🔴 4bbbd — 3 issue(s) found
Powered by Claude Code Review |
1f6dbe8 to
f0f5614
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/bun.js.zig`:
- Around line 592-613: The function escapeForJSString creates a
std.array_list.Managed (result) and may leak its allocation if an append fails;
fix by adding errdefer result.deinit() immediately after the Managed init (e.g.,
after var result = try std.array_list.Managed(u8).initCapacity(...)) so the list
is cleaned up on error, and keep the existing return result.toOwnedSlice() on
success (errdefer ensures no deinit runs on success). Ensure the early
non-escaping path still uses allocator.dupe as before.
In `@test/js/bun/cron/cron.test.ts`:
- Around line 199-205: The test currently spawns a crontab process using "using
setup = Bun.spawn(...)" and writes to setup.stdin but doesn't verify the process
exit status; capture the process result by awaiting setup.exited into a variable
(e.g., result) and assert the exit code is zero (or the expected success code)
before proceeding with the rest of the test so a failing crontab call fails the
test; update the section around the Bun.spawn call and the subsequent
setup.exited usage to await and assert the exit code.
Code ReviewNewest first 🔴 3f80d — 2 issue(s) found
🔴 da282 — 3 issue(s) found
Powered by Claude Code Review |
Code ReviewNewest first 🔴 da282 — 2 issue(s) found
Powered by Claude Code Review |
3f80d03 to
a546e55
Compare
Code ReviewNewest first 🔴 1f6db — 3 issue(s) found
🔴 da282 — 2 issue(s) found
🔴 da282 — 3 issue(s) found
🔴 4bbbd — 3 issue(s) found
Powered by Claude Code Review |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@docs/runtime/cron.mdx`:
- Around line 172-175: The documentation incorrectly shows the crontab
invocation as "bun run --cron-title=... --cron-period='...' <bun-path>
<script-path>"; update the prose in docs/runtime/cron.mdx so the crontab entry
shows the schedule first and the bun executable quoted next: "<schedule>
'<bun-path>' run --cron-title=<title> --cron-period='<schedule>'
'<script-path>'". Locate the paragraph that mentions running the cron job (it
currently contains the "bun run --cron-title" snippet and the reference to the
scheduled() handler) and replace that command example with the corrected
pattern, preserving the explanation about importing the module and calling
scheduled().
In `@src/bun.js/api/cron.zig`:
- Around line 399-404: The normalization step is expanding step patterns (e.g.,
"*/15") into explicit lists via parsed.formatNumeric and normalized_schedule,
which breaks cronToSchtasks because it expects "*/N"; update the code to call
cronToSchtasks with the original raw expression (schedule_slice or the parsed
raw string) instead of normalized_schedule, or alternatively detect
evenly-spaced numeric lists produced by parsed.formatNumeric and convert them
back into the "*/N" form before passing to cronToSchtasks; adjust references in
the code paths that use CronExpression.parse, parsed.formatNumeric,
normalized_schedule, and cronToSchtasks so Windows conversion preserves step
patterns.
- Around line 1051-1054: Replace the use of std.mem.startsWith for the minute
prefix check with the SIMD-accelerated helper: change std.mem.startsWith(u8,
minute, "*/") to bun.strings.startsWith(minute, "*/") in the cron matching
conditional (keeping the other bun.strings.eql checks for hour, dom, month, dow
unchanged) so the code uses bun.strings.* utilities consistently.
5530ffd to
b9b56ed
Compare
Code ReviewNewest first 🔴 3f80d — 4 issue(s) found
Powered by Claude Code Review |
Code ReviewNewest first 🔴 164c1 — 3 issue(s) found
🔴 a546e — 2 issue(s) found
🔴 5530f — 2 issue(s) found
Powered by Claude Code Review |
164c126 to
c674cc6
Compare
|
when can we expect this feature to be bundled? :D |
|
I can't wait for this either, hopefully it makes it to the next release! 👀 |
|
@robobun Three tasks:
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (11)
src/bun.js.zig (2)
611-622:⚠️ Potential issue | 🟡 MinorGuard
Managedallocation witherrdeferinescapeForJSString.If any
append/appendSlicefails,resultleaks. Adderrdefer result.deinit();right afterinitCapacity.🔧 Proposed fix
var result = try std.array_list.Managed(u8).initCapacity(allocator, input.len + 16); + errdefer result.deinit(); for (input) |c| { switch (c) { '\\' => try result.appendSlice("\\\\"), '"' => try result.appendSlice("\\\""), '\n' => try result.appendSlice("\\n"), '\r' => try result.appendSlice("\\r"), '\t' => try result.appendSlice("\\t"), else => try result.append(c), } }As per coding guidelines: "In Zig code, be careful with allocators and use defer for cleanup."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js.zig` around lines 611 - 622, The Managed array_list allocation in escapeForJSString (var result = try std.array_list.Managed(u8).initCapacity(allocator, input.len + 16)) is not cleaned up on error, causing a leak if any append/appendSlice fails; add an errdefer result.deinit(); immediately after the initCapacity call (before the for loop) so that result is deinitialized on error, keeping the existing return result.toOwnedSlice() behavior on success.
219-242:⚠️ Potential issue | 🟡 MinorAdd error-path cleanup for cron allocations.
cron_script(Line 219) andheap_entry_path(Line 238) can leak if later fallible ops fail (e.g., Line 229getcwd, Line 238dupe, Line 239create). Adderrdeferimmediately after each allocation.🔧 Proposed fix
const cron_script = try std.fmt.allocPrint(bun.default_allocator, \\const mod = await import("{s}"); \\const scheduled = (mod.default || mod).scheduled; \\if (typeof scheduled !== "function") throw new Error("Module does not export default.scheduled()"); \\const controller = {{ cron: "{s}", type: "scheduled", scheduledTime: Date.now() }}; \\await scheduled(controller); , .{ escaped_path, escaped_period }); + errdefer bun.default_allocator.free(cron_script); // entry_path must end with /[eval] for the transpiler to use eval_source const trigger = bun.pathLiteral("/[eval]"); var cwd_buf: bun.PathBuffer = undefined; const cwd_slice = switch (bun.sys.getcwd(&cwd_buf)) { .result => |cwd| cwd, .err => return error.SystemResources, }; var eval_path_buf: [bun.MAX_PATH_BYTES + trigger.len]u8 = undefined; `@memcpy`(eval_path_buf[0..cwd_slice.len], cwd_slice); `@memcpy`(eval_path_buf[cwd_slice.len..][0..trigger.len], trigger); const eval_entry_path = eval_path_buf[0 .. cwd_slice.len + trigger.len]; // Heap-allocate the path so it outlives this stack frame const heap_entry_path = try bun.default_allocator.dupe(u8, eval_entry_path); + errdefer bun.default_allocator.free(heap_entry_path); const script_source = try bun.default_allocator.create(logger.Source);As per coding guidelines: "In Zig code, be careful with allocators and use defer for cleanup."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js.zig` around lines 219 - 242, cron_script and heap_entry_path are allocated but not cleaned up on subsequent fallible operations (e.g., bun.sys.getcwd, bun.default_allocator.dupe, bun.default_allocator.create); add errdefer blocks immediately after each allocation to free those allocations on error paths: after creating cron_script (the result of std.fmt.allocPrint) add an errdefer that frees cron_script via bun.default_allocator.free or the appropriate free/allocator method, and after duplicating eval_entry_path into heap_entry_path (bun.default_allocator.dupe) add an errdefer that frees heap_entry_path and also ensure any earlier cron_script errdefer is still effective; likewise, if create(logger.Source) fails, ensure heap_entry_path is freed in its errdefer so no heap leak remains.src/bun.js/api/BunObject.zig (1)
72-72: 🛠️ Refactor suggestion | 🟠 MajorMove inline
@importto file-level constant.The inline
@import("./cron.zig")violates the Zig import placement guideline. Move it to a file-level constant at the bottom of the file.- pub const cron = toJSLazyPropertyCallback(`@import`("./cron.zig").getCronObject); + pub const cron = toJSLazyPropertyCallback(cron_module.getCronObject);Add at the bottom of the file with other imports:
const cron_module = `@import`("./cron.zig");As per coding guidelines: "Place
@importstatements at the bottom of the file... Never use@import()inline inside of functions. Always put imports at the bottom of the file or containing struct."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/BunObject.zig` at line 72, The inline `@import` in the cron property should be moved to a file-level import: add a bottom-of-file constant like const cron_module = `@import`("./cron.zig"); alongside the other imports, and update the property declaration pub const cron = toJSLazyPropertyCallback(`@import`("./cron.zig").getCronObject); to use cron_module.getCronObject instead (i.e., pub const cron = toJSLazyPropertyCallback(cron_module.getCronObject);) so `@import` is not used inline.packages/bun-types/bun.d.ts (1)
7303-7305:⚠️ Potential issue | 🟡 MinorFix the parse-chaining example so it type-checks.
Line 7304 uses an undeclared
from, and Line 7305 passesfirsteven thoughBun.cron.parse()can returnnull.✏️ Proposed fix
- * const first = Bun.cron.parse("@hourly", from); - * const second = Bun.cron.parse("@hourly", first); + * const from = new Date(); + * const first = Bun.cron.parse("@hourly", from); + * const second = first ? Bun.cron.parse("@hourly", first) : null;#!/bin/bash # Verify the example currently references `from` without declaration # and chains a nullable result directly. sed -n '7299,7307p' packages/bun-types/bun.d.ts rg -n 'const from =|const first = Bun\.cron\.parse\("@hourly", from\)|const second = Bun\.cron\.parse\("@hourly", first\)' packages/bun-types/bun.d.ts🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bun-types/bun.d.ts` around lines 7303 - 7305, The example chains Bun.cron.parse incorrectly: declare a proper Date source (e.g., const from = new Date()) to replace the undeclared from, and guard the nullable return of Bun.cron.parse before passing it into the next call; specifically, update the example so you assign const from = new Date(), call const first = Bun.cron.parse("@hourly", from), then check first !== null (or use an if) before calling Bun.cron.parse("@hourly", first) to produce second — referencing Bun.cron.parse, from, first, and second to locate the lines to change.test/js/bun/cron/cron.test.ts (1)
17-22:⚠️ Potential issue | 🟠 MajorFail fast on crontab command failures in setup/restore paths.
Line 21 and Line 201 currently allow crontab command failures to pass silently, which can leave stale scheduler state and produce false-positive tests.
🔧 Proposed fix
function writeCrontab(content: string) { const tmpFile = `/tmp/bun-cron-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`; writeFileSync(tmpFile, content); try { - Bun.spawnSync({ cmd: [crontabPath!, tmpFile] }); + const result = Bun.spawnSync({ + cmd: [crontabPath!, tmpFile], + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(`crontab install failed (exit ${result.exitCode}): ${result.stderr.toString()}`); + } } finally { try { unlinkSync(tmpFile); } catch {} } } @@ setup.stdin.write("0 0 * * * /usr/bin/some-other-job\n"); setup.stdin.end(); - await setup.exited; + const setupExitCode = await setup.exited; + expect(setupExitCode).toBe(0);As per coding guidelines: "Always check exit codes and test error scenarios when spawning processes in tests."
Also applies to: 195-201
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/js/bun/cron/cron.test.ts` around lines 17 - 22, The crontab spawn calls in writeCrontab (and the corresponding restore/reset helper used around lines 195-201) ignore the child process exit status, allowing failures to be silent; update both places to capture the result of Bun.spawnSync, check result.status (or result.exitCode), and throw or fail the test when it is non-zero, including the command, exit code and result.stderr/stdout in the thrown error message so failures fail fast and surface diagnostics; locate the crontab invocations in writeCrontab and the restore helper and add the status check and informative error throw there.src/bun.js/api/cron.zig (6)
909-909: 🛠️ Refactor suggestion | 🟠 MajorUse
bun.path.dirnameinstead ofstd.fs.path.dirname.Per coding guidelines: "Use
bun.path.join/dirname/basenameinstead ofstd.fs.path.join/dirname/basenamefor path manipulation."🔧 Proposed fix
- const source_dir = std.fs.path.dirname(caller_utf8.slice()) orelse "."; + const source_dir = bun.path.dirname(caller_utf8.slice(), .auto) orelse ".";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` at line 909, Replace the use of std.fs.path.dirname with bun.path.dirname when computing source_dir: locate the assignment to source_dir (the line using std.fs.path.dirname(caller_utf8.slice()) orelse ".") and change it to call bun.path.dirname on the caller path (caller_utf8.slice()) so it follows the coding guideline to use bun.path.dirname instead of std.fs.path.dirname while preserving the fallback to ".".
757-758: 🛠️ Refactor suggestion | 🟠 MajorReplace
jsc.ZigString.staticwithbun.String.staticfor property names.Per coding guidelines, prefer
bun.StringoverZigStringin new code for JS interop.🔧 Proposed fix
- cron_fn.put(globalThis, jsc.ZigString.static("remove"), remove_fn); - cron_fn.put(globalThis, jsc.ZigString.static("parse"), parse_fn); + cron_fn.put(globalThis, bun.String.static("remove"), remove_fn); + cron_fn.put(globalThis, bun.String.static("parse"), parse_fn);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 757 - 758, The property-name creation uses jsc.ZigString.static; change both calls to use bun.String.static instead (update the two lines where cron_fn.put(globalThis, jsc.ZigString.static("remove"), remove_fn) and cron_fn.put(globalThis, jsc.ZigString.static("parse"), parse_fn) so they call bun.String.static("remove") and bun.String.static("parse") respectively) to follow the JS interop guideline preferring bun.String over jsc.ZigString.
1079-1085:⚠️ Potential issue | 🟠 MajorWindows HOURLY schedule loses minute specification.
The HOURLY case parses the minute value but discards it. Without
/st, the task runs at an undefined time each hour, making15 * * * *(run at minute 15) indistinguishable from0 * * * *(run at minute 0).🔧 Proposed fix
// Hourly: 0 * * * * or N * * * * if (bun.strings.eql(hour, "*") and bun.strings.eql(dom, "*") and bun.strings.eql(month, "*") and bun.strings.eql(dow, "*")) { - _ = std.fmt.parseInt(u32, minute, 10) catch return error.UnsupportedSchedule; - return .{ .sc = "HOURLY", .mo = "1" }; + const m = std.fmt.parseInt(u32, minute, 10) catch return error.UnsupportedSchedule; + const static_st = struct { + var buf: [6:0]u8 = undefined; + }; + _ = std.fmt.bufPrintZ(&static_st.buf, "00:{d:0>2}", .{m}) catch return error.UnsupportedSchedule; + return .{ .sc = "HOURLY", .mo = "1", .start_time = &static_st.buf }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 1079 - 1085, The HOURLY branch in cron.zig currently parses the minute but discards it and returns .mo = "1"; update the branch that checks hour/dom/month/dow == "*" to capture the parsed minute value (from parseInt(u32, minute, 10) or use the original minute string) and set the returned schedule object's mo field to that minute so e.g. "15 * * * *" preserves minute 15; modify the return in that block (refer to variables minute and the returned struct .sc and .mo) to include the actual minute instead of the hardcoded "1".
264-269:⚠️ Potential issue | 🟠 MajormacOS: Missing LaunchAgents directory creation and memory leak.
Two issues remain unaddressed:
Directory may not exist:
~/Library/LaunchAgentsmight not exist on fresh macOS installs. Theopenat()withO.CREATonly creates the file, not parent directories, so registration will fail.Memory leak: At line 369,
this.tmp_path = nullprevents deletion of the installed plist, but the allocatedplist_pathstring is never freed.🔧 Proposed fix
this.tmp_path = plist_path; + + // Ensure ~/Library/LaunchAgents exists + const agents_dir = bun.path.dirname(plist_path, .posix) orelse { + this.setErr("Invalid plist path", .{}); + this.finish(); + return; + }; + bun.makePath(bun.FD.cwd(), agents_dir) catch { + this.setErr("Failed to create LaunchAgents directory", .{}); + this.finish(); + return; + };And in
spawnBootstrap:var argv = [_:null]?[*:0]const u8{ "/bin/launchctl", "bootstrap", uid_str.ptr, plist_path.ptr, null }; + bun.default_allocator.free(plist_path); this.tmp_path = null; // don't delete the installed plistAlso applies to: 328-331, 369-370
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 264 - 269, The plist allocation and assignment leak memory and can fail if the LaunchAgents parent directory doesn't exist: before calling allocPrintZ/creating the plist file ensure the ~/Library/LaunchAgents directory exists (create it with recursive/mode 0700 using the default allocator or POSIX mkdir/mkdirat) so openat with O_CREAT won't fail due to missing parents; after you create and install the plist ensure you free the allocPrintZ buffer (plist_path) when you assign it to this.tmp_path or when you later set this.tmp_path = null so the allocated string is released (apply the same fix around the other allocPrintZ usages at the referenced blocks and inside spawnBootstrap).
809-812:⚠️ Potential issue | 🔴 CriticalCritical: Windows subprocess runs with empty environment.
The code creates
null_envpas an array containing only a null sentinel, then passes@ptrCast(&null_envp)tospawnProcess. This is a non-null pointer to an empty environment block, not a null pointer.In libuv,
env = nullmeans "inherit parent environment", while a non-null pointer means "use this environment block." Passing a pointer to an empty array causes the subprocess to run with zero environment variables, breaking PATH, HOME, USERPROFILE, etc.🔧 Proposed fix
Either pass the actual parent environment on Windows:
- var null_envp = [_:null]?[*:0]const u8{null}; - const envp: [*:null]?[*:0]const u8 = if (comptime bun.Environment.isPosix) `@ptrCast`(`@constCast`(std.c.environ)) else `@ptrCast`(&null_envp); + const envp: [*:null]?[*:0]const u8 = `@ptrCast`(`@constCast`(std.c.environ));Or modify
spawnProcessto accept an optional envp and pass true null for inheritance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 809 - 812, The Windows branch is creating a non-null pointer to an empty env block (null_envp) causing child processes to run with zero environment; change the envp assignment so that when bun.Environment.isPosix is false you pass a true null pointer to spawnProcess instead of `@ptrCast`(&null_envp). Locate null_envp and the envp definition in the file (symbols: null_envp, envp, bun.Environment.isPosix) and ensure the value passed into bun.spawn.spawnProcess is a null pointer for inheritance on Windows (or adjust spawnProcess to accept an optional env and map a null optional to a true null pointer) so children inherit the parent environment.
806-806: 🧹 Nitpick | 🔵 TrivialConsider capturing stderr for debugging spawn failures.
Setting
.stderr = .ignoresuppresses all error output from spawned processes, making it harder to diagnose failures. Consider using.inheritor piping to a log during development/debugging.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` at line 806, The spawn configuration currently sets ".stderr = .ignore" which discards all error output; locate the spawn call in api/cron.zig where ".stderr = .ignore" is set and change it to either ".stderr = .inherit" during development or set ".stderr = .piped" (or equivalent) and read the pipe to forward stderr into your logging system so spawn failures are captured; ensure you flush/close the pipe and handle errors when reading so logs always include stderr content for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/Arguments.zig`:
- Around line 834-837: The current XOR check only ensures both or neither are
non-empty but doesn't reject the case where both arguments were passed as empty
strings; update the validation around ctx.runtime_options.cron_title and
ctx.runtime_options.cron_period to explicitly reject empty values: after the
existing condition, add a check that if either
ctx.runtime_options.cron_title.len == 0 or ctx.runtime_options.cron_period.len
== 0 then call Output.errGeneric with a clear message (e.g., "--cron-title and
--cron-period cannot be empty") and Global.exit(1), so empty-provided values are
treated as invalid.
---
Duplicate comments:
In `@packages/bun-types/bun.d.ts`:
- Around line 7303-7305: The example chains Bun.cron.parse incorrectly: declare
a proper Date source (e.g., const from = new Date()) to replace the undeclared
from, and guard the nullable return of Bun.cron.parse before passing it into the
next call; specifically, update the example so you assign const from = new
Date(), call const first = Bun.cron.parse("@hourly", from), then check first !==
null (or use an if) before calling Bun.cron.parse("@hourly", first) to produce
second — referencing Bun.cron.parse, from, first, and second to locate the lines
to change.
In `@src/bun.js.zig`:
- Around line 611-622: The Managed array_list allocation in escapeForJSString
(var result = try std.array_list.Managed(u8).initCapacity(allocator, input.len +
16)) is not cleaned up on error, causing a leak if any append/appendSlice fails;
add an errdefer result.deinit(); immediately after the initCapacity call (before
the for loop) so that result is deinitialized on error, keeping the existing
return result.toOwnedSlice() behavior on success.
- Around line 219-242: cron_script and heap_entry_path are allocated but not
cleaned up on subsequent fallible operations (e.g., bun.sys.getcwd,
bun.default_allocator.dupe, bun.default_allocator.create); add errdefer blocks
immediately after each allocation to free those allocations on error paths:
after creating cron_script (the result of std.fmt.allocPrint) add an errdefer
that frees cron_script via bun.default_allocator.free or the appropriate
free/allocator method, and after duplicating eval_entry_path into
heap_entry_path (bun.default_allocator.dupe) add an errdefer that frees
heap_entry_path and also ensure any earlier cron_script errdefer is still
effective; likewise, if create(logger.Source) fails, ensure heap_entry_path is
freed in its errdefer so no heap leak remains.
In `@src/bun.js/api/BunObject.zig`:
- Line 72: The inline `@import` in the cron property should be moved to a
file-level import: add a bottom-of-file constant like const cron_module =
`@import`("./cron.zig"); alongside the other imports, and update the property
declaration pub const cron =
toJSLazyPropertyCallback(`@import`("./cron.zig").getCronObject); to use
cron_module.getCronObject instead (i.e., pub const cron =
toJSLazyPropertyCallback(cron_module.getCronObject);) so `@import` is not used
inline.
In `@src/bun.js/api/cron.zig`:
- Line 909: Replace the use of std.fs.path.dirname with bun.path.dirname when
computing source_dir: locate the assignment to source_dir (the line using
std.fs.path.dirname(caller_utf8.slice()) orelse ".") and change it to call
bun.path.dirname on the caller path (caller_utf8.slice()) so it follows the
coding guideline to use bun.path.dirname instead of std.fs.path.dirname while
preserving the fallback to ".".
- Around line 757-758: The property-name creation uses jsc.ZigString.static;
change both calls to use bun.String.static instead (update the two lines where
cron_fn.put(globalThis, jsc.ZigString.static("remove"), remove_fn) and
cron_fn.put(globalThis, jsc.ZigString.static("parse"), parse_fn) so they call
bun.String.static("remove") and bun.String.static("parse") respectively) to
follow the JS interop guideline preferring bun.String over jsc.ZigString.
- Around line 1079-1085: The HOURLY branch in cron.zig currently parses the
minute but discards it and returns .mo = "1"; update the branch that checks
hour/dom/month/dow == "*" to capture the parsed minute value (from parseInt(u32,
minute, 10) or use the original minute string) and set the returned schedule
object's mo field to that minute so e.g. "15 * * * *" preserves minute 15;
modify the return in that block (refer to variables minute and the returned
struct .sc and .mo) to include the actual minute instead of the hardcoded "1".
- Around line 264-269: The plist allocation and assignment leak memory and can
fail if the LaunchAgents parent directory doesn't exist: before calling
allocPrintZ/creating the plist file ensure the ~/Library/LaunchAgents directory
exists (create it with recursive/mode 0700 using the default allocator or POSIX
mkdir/mkdirat) so openat with O_CREAT won't fail due to missing parents; after
you create and install the plist ensure you free the allocPrintZ buffer
(plist_path) when you assign it to this.tmp_path or when you later set
this.tmp_path = null so the allocated string is released (apply the same fix
around the other allocPrintZ usages at the referenced blocks and inside
spawnBootstrap).
- Around line 809-812: The Windows branch is creating a non-null pointer to an
empty env block (null_envp) causing child processes to run with zero
environment; change the envp assignment so that when bun.Environment.isPosix is
false you pass a true null pointer to spawnProcess instead of
`@ptrCast`(&null_envp). Locate null_envp and the envp definition in the file
(symbols: null_envp, envp, bun.Environment.isPosix) and ensure the value passed
into bun.spawn.spawnProcess is a null pointer for inheritance on Windows (or
adjust spawnProcess to accept an optional env and map a null optional to a true
null pointer) so children inherit the parent environment.
- Line 806: The spawn configuration currently sets ".stderr = .ignore" which
discards all error output; locate the spawn call in api/cron.zig where ".stderr
= .ignore" is set and change it to either ".stderr = .inherit" during
development or set ".stderr = .piped" (or equivalent) and read the pipe to
forward stderr into your logging system so spawn failures are captured; ensure
you flush/close the pipe and handle errors when reading so logs always include
stderr content for debugging.
In `@test/js/bun/cron/cron.test.ts`:
- Around line 17-22: The crontab spawn calls in writeCrontab (and the
corresponding restore/reset helper used around lines 195-201) ignore the child
process exit status, allowing failures to be silent; update both places to
capture the result of Bun.spawnSync, check result.status (or result.exitCode),
and throw or fail the test when it is non-zero, including the command, exit code
and result.stderr/stdout in the thrown error message so failures fail fast and
surface diagnostics; locate the crontab invocations in writeCrontab and the
restore helper and add the status check and informative error throw there.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c556e653-8ce3-417f-8360-7ff160a60e61
📒 Files selected for processing (16)
docs/docs.jsondocs/runtime/cron.mdxpackages/bun-types/bun.d.tssrc/bun.js.zigsrc/bun.js/api/BunObject.zigsrc/bun.js/api/bun/process.zigsrc/bun.js/api/cron.zigsrc/bun.js/api/cron_parser.zigsrc/bun.js/bindings/BunObject+exports.hsrc/bun.js/bindings/BunObject.cppsrc/bun.js/bindings/JSGlobalObject.zigsrc/bun.js/bindings/bindings.cppsrc/cli.zigsrc/cli/Arguments.zigtest/integration/bun-types/fixture/cron.tstest/js/bun/cron/cron.test.ts
robobun
left a comment
There was a problem hiding this comment.
Review — test changes and CI status
@alii's three tasks ✅
All three tasks from the comment are addressed in commit 5ab0ab1:
-
Smoke test: Verified the test file runs. On a machine without
crontab, the crontab-dependent tests are correctly skipped (11 skip) while the expression parsing tests pass (7 pass). TheBun.cron.parsetests fail on system bun as expected (it's a new API). -
Skip on Linux when no
crontabin PATH:describe.skipIf(!hasCrontab)usingBun.which("crontab")replaces the old!isLinuxcheck. This correctly skips on any platform where crontab isn't available. -
Cleanup with
using: All tests now useusing _restore = saveCrontabState()(disposable pattern) instead ofbeforeEach/afterEach. Even if a test throws, cleanup runs.
CI: ✅ Green
Build #38698 passed — all checks completed successfully on HEAD 5ab0ab1.
Remaining unresolved review threads
There are ~15 unresolved threads from coderabbitai and claude[bot] about the core cron.zig implementation (not the test changes). Key concerns:
schtasksargv null entries (line 498): Null sentinel array has gaps when optional args are absentcronToSchtasksonly handles simple cron patterns (line 485): Called withraw_schedulebut only parses single-value fieldscronToCalendarIntervalproduct expansion (line 1021): When fields have multiple values, the product count logic may be incorrect- HOURLY discards minute spec (line 1083): Minute value is parsed then discarded in schtasks conversion
SpawnOptionsmissingwindows.loop(line 806): Uninitialized fieldbun.d.tstype/doc issues: Cron chaining example won't compile, schedule types could be improved
These are for the PR author to address — they're about the implementation, not the test scaffolding.
|
@robobun fix it. all 11. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/bun.js/api/cron.zig (4)
782-783: 🧹 Nitpick | 🔵 TrivialUse
bun.String.staticfor property keys in new Zig JS interop code.Swap
jsc.ZigString.static(...)withbun.String.static(...)forremove/parsekeys.As per coding guidelines, "Prefer
bun.StringoverZigStringin new Zig code."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 782 - 783, The property key creation uses jsc.ZigString.static; update the two calls so cron_fn.put(globalThis, jsc.ZigString.static("remove"), remove_fn) and cron_fn.put(globalThis, jsc.ZigString.static("parse"), parse_fn) use bun.String.static instead of jsc.ZigString.static for the "remove" and "parse" keys so the calls become cron_fn.put(globalThis, bun.String.static("remove"), remove_fn) and cron_fn.put(globalThis, bun.String.static("parse"), parse_fn).
831-840:⚠️ Potential issue | 🔴 CriticalWindows child process env inheritance is likely broken.
The Windows branch passes a pointer to
{ null }instead of a true null env pointer, which can yield an empty environment rather than inheriting parent vars. This can break scheduler invocation reliability.#!/bin/bash # Verify spawnProcess env semantics and compare with existing patterns rg -n "fn spawnProcess|pub fn spawnProcess" src --type zig -A30 -B5 rg -n "options\\.envp orelse|std\\.c\\.environ|\\.env\\s*=" src/bun.js/api/bun/process.zig -A8 -B8 rg -n "spawnProcess\\(&spawn_options, `@ptrCast`\\(argv\\), envp\\)" src/bun.js/api/cron.zig -A8 -B8🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 831 - 840, The child process env pointer logic is incorrect for Windows: instead of passing a pointer to a one-element array containing null (null_envp) you must pass a true null env pointer to spawnProcess so the child inherits the parent environment; update the envp assignment used in spawnProcess(&spawn_options, `@ptrCast`(argv), envp) to yield a null pointer on Windows (when bun.Environment.isWindows) and std.c.environ (or `@ptrCast`(`@constCast`(std.c.environ))) on POSIX; refer to the symbols envp, null_envp, spawn_options, bun.spawn.spawnProcess and bun.Environment.isPosix/isWindows to locate and change the branch so Windows gets a real null rather than &null_envp.
1156-1156: 🧹 Nitpick | 🔵 TrivialUse
bun.strings.eqlinstead ofstd.mem.eqlfor string comparison.This keeps string ops aligned with the project’s SIMD-optimized helpers.
As per coding guidelines, "Use
bun.strings.eql/indexOf/startsWithinstead ofstd.mem.eql/indexOf/startsWithfor string operations."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` at line 1156, Replace the std.mem.eql call with the project's SIMD-optimized helper: change the comparison in cron.zig (the line using std.mem.eql(u8, &upper, prefix) inside the relevant loop/function) to use bun.strings.eql with the same operands; ensure bun.strings is imported/available in this module and keep the argument order/values identical so the behavior is unchanged (use bun.strings.eql(&upper, prefix) or the appropriate slice forms used elsewhere in this file).
264-270:⚠️ Potential issue | 🟠 MajorCreate
LaunchAgentsdirectory and fix retained plist path leak.
openatwon’t create missing parent directories, so first-run setup can fail if~/Library/LaunchAgentsis absent. Also, settingthis.tmp_path = nulldrops the ownedplist_pathallocation without freeing it.As per coding guidelines, "Use
bun.makePath()for creating directories recursively in Zig."Also applies to: 328-332, 368-370
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 264 - 270, allocPrintZ is building a plist path into ~/Library/LaunchAgents but openat won't create the parent directory and assigning this.tmp_path = plist_path can leak the allocation later when this.tmp_path is dropped; before calling allocPrintZ, call bun.makePath(home, "Library/LaunchAgents") (or bun.makePath("{s}/Library/LaunchAgents", .{home})) to create the directory recursively, then set this.tmp_path = plist_path only after successfully creating it; ensure any previous this.tmp_path is freed (or transferred ownership) instead of overwriting with null to avoid leaks — apply the same fix pattern around the other occurrences involving allocPrintZ / this.tmp_path noted in the comment (the blocks around the other ranges).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/bun.js/api/cron.zig`:
- Around line 484-486: cron registration currently passes this.raw_schedule into
cronToSchtasks which uses a 5-field tokenizer, causing valid nicknames (e.g.,
`@daily`) that were validated by CronExpression.parse in cronRegister to fail;
change startWindows (and the other occurrence around cronToSchtasks at lines
~1070-1079) to pass the normalized/expanded schedule (use the parsed/normalized
field, e.g., this.schedule or the output of CronExpression.parse) into
cronToSchtasks instead of this.raw_schedule so nicknames are resolved before
Windows conversion.
- Around line 1134-1141: The weekly schedule branch currently rejects dow=7;
update the handler around the parseInt/resolveWeekdayName result (the variable d
returned by std.fmt.parseInt or resolveWeekdayName) to treat 7 as Sunday by
mapping d==7 to 0 before the bounds check and indexing into day_names;
specifically, after computing d in the routine that produces the u32 `d`, add a
small normalization step (if d == 7 then d = 0) and then proceed with the
existing if (d > 6) validation and the return that uses day_names[d] and
start_time building.
---
Duplicate comments:
In `@src/bun.js/api/cron.zig`:
- Around line 782-783: The property key creation uses jsc.ZigString.static;
update the two calls so cron_fn.put(globalThis, jsc.ZigString.static("remove"),
remove_fn) and cron_fn.put(globalThis, jsc.ZigString.static("parse"), parse_fn)
use bun.String.static instead of jsc.ZigString.static for the "remove" and
"parse" keys so the calls become cron_fn.put(globalThis,
bun.String.static("remove"), remove_fn) and cron_fn.put(globalThis,
bun.String.static("parse"), parse_fn).
- Around line 831-840: The child process env pointer logic is incorrect for
Windows: instead of passing a pointer to a one-element array containing null
(null_envp) you must pass a true null env pointer to spawnProcess so the child
inherits the parent environment; update the envp assignment used in
spawnProcess(&spawn_options, `@ptrCast`(argv), envp) to yield a null pointer on
Windows (when bun.Environment.isWindows) and std.c.environ (or
`@ptrCast`(`@constCast`(std.c.environ))) on POSIX; refer to the symbols envp,
null_envp, spawn_options, bun.spawn.spawnProcess and
bun.Environment.isPosix/isWindows to locate and change the branch so Windows
gets a real null rather than &null_envp.
- Line 1156: Replace the std.mem.eql call with the project's SIMD-optimized
helper: change the comparison in cron.zig (the line using std.mem.eql(u8,
&upper, prefix) inside the relevant loop/function) to use bun.strings.eql with
the same operands; ensure bun.strings is imported/available in this module and
keep the argument order/values identical so the behavior is unchanged (use
bun.strings.eql(&upper, prefix) or the appropriate slice forms used elsewhere in
this file).
- Around line 264-270: allocPrintZ is building a plist path into
~/Library/LaunchAgents but openat won't create the parent directory and
assigning this.tmp_path = plist_path can leak the allocation later when
this.tmp_path is dropped; before calling allocPrintZ, call bun.makePath(home,
"Library/LaunchAgents") (or bun.makePath("{s}/Library/LaunchAgents", .{home}))
to create the directory recursively, then set this.tmp_path = plist_path only
after successfully creating it; ensure any previous this.tmp_path is freed (or
transferred ownership) instead of overwriting with null to avoid leaks — apply
the same fix pattern around the other occurrences involving allocPrintZ /
this.tmp_path noted in the comment (the blocks around the other ranges).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a1960ed3-e6ef-4b9d-b55a-af3635b3ba65
📒 Files selected for processing (3)
packages/bun-types/bun.d.tssrc/bun.js/api/cron.zigsrc/cli/Arguments.zig
|
@robobun fix every issue from the PR review comments. |
|
@robobun fix every issue from the PR review comments. Maybe you forgot to push |
|
All 11 review issues addressed in 4584a29: cron.zig fixes (1-9):
docs/runtime/cron.mdx fixes (10-11): Cross-platform compilation verified: |
…rsing Adds `Bun.cron()`, `Bun.cron.remove()`, and `Bun.cron.parse()` — a complete API for registering OS-level cron jobs and parsing cron expressions from JavaScript. **Bun.cron(path, schedule, title)** registers a cron job that runs a JS/TS module on a schedule, using the platform's native scheduler: - Linux: crontab - macOS: launchd (plist + StartCalendarInterval) - Windows: Task Scheduler (schtasks) The target module exports a `scheduled(controller)` handler following the Cloudflare Workers Cron Triggers API. **Bun.cron.parse(expression, relativeDate?)** parses a cron expression and returns the next matching UTC Date. Supports: - Standard 5-field expressions (minute hour day month weekday) - Named days: SUN-SAT, Sunday-Saturday (case-insensitive) - Named months: JAN-DEC, January-December (case-insensitive) - Sunday as 7 (normalized to 0) - Predefined nicknames: @Yearly, @annually, @monthly, @Weekly, @daily, @midnight, @hourly - POSIX OR logic: when both day-of-month and day-of-week are specified (neither is *), either matching is sufficient The parser uses a bitset representation (u64/u32/u16/u8 per field) for efficient matching. The next-occurrence algorithm advances by the largest non-matching unit (month > day > hour > minute) and normalizes via JSC's UTC date functions on each iteration. Expressions are normalized to numeric form before platform registration, so named values like "Monday" or "@daily" produce valid crontab entries. Also fixes a pre-existing bug in filterCrontab where substring matching (indexOf) could cause removing job "test" to also remove "test-cleanup". Changed to exact line matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace beforeEach/afterEach with `using _restore = saveCrontabState()`
so crontab is always restored even if a test crashes
- Use Bun.which("crontab") instead of hardcoded /usr/bin/crontab
- Skip cron tests based on crontab availability, not just isLinux
- Build schtasks argv dynamically to avoid null gaps truncating before /f - Initialize windows.loop in SpawnOptions for Windows spawn support - Wrap multiple calendar interval dicts in <array> for valid launchd plist - Use minute value for /st in HOURLY schtasks schedule - Resolve named weekdays (MON, TUE, etc.) in cronToSchtasks - Use bun.path.dirname instead of std.fs.path.dirname in resolvePath - Fix JSDoc chaining example to handle null return - Add common cron patterns and template literal to CronWithAutocomplete type - Reject empty --cron-title/--cron-period values in argument validation https://claude.ai/code/session_01YZgjSQgUz2Xt7SZKNHv5jv
cron.zig: 1. Use this.schedule (not raw_schedule) in cronToSchtasks so nicknames like @daily get the normalized 5-field form 2. Accept dow=7 as Sunday (POSIX cron allows both 0 and 7) 3. Use bun.strings.eql instead of std.mem.eql in resolveWeekdayName 4. Use bun.String.static instead of jsc.ZigString.static in getCronObject 5. Create ~/Library/LaunchAgents dir before writing plist file 6. Fix buffer overflow in MINUTE case of cronToSchtasks — use std.fmt.bufPrintZ instead of raw @memcpy of user input 7. Exempt Windows schtasks /delete non-zero exit in CronRemoveJob so removing a non-existent task resolves without error 8. Use std.c.environ for env inheritance instead of null pointer that gave child processes an empty environment 9. Implement POSIX cron OR semantics for launchd: when both day-of-month and day-of-week are specified, emit separate dict sets so launchd fires when either matches docs/runtime/cron.mdx: 10. Fix grep pattern: bun.cron not sh.bun.cron 11. Document that complex cron patterns are supported on macOS via Cartesian product expansion
robobun
left a comment
There was a problem hiding this comment.
CI is red — two fixes needed
1. test/internal/ban-words.test.ts — failing on ALL 17 platforms
src/bun.js/api/cron.zig line 270 uses std.fs.cwd() which is a banned word in the Bun codebase. The ban-words test enforces: "Prefer bun.FD.cwd()".
Fix: Change line 270 from:
bun.makePath(std.fs.cwd(), launch_agents_dir) catch {Look at how bun.makePath is called elsewhere — it takes a bun.FD or a dir fd. The correct fix is likely something like:
bun.makePath(bun.FD.cwd(), launch_agents_dir) catch {Verify by checking the bun.makePath signature.
2. test/js/bun/cron/cron.test.ts — failing on macOS only (4 platforms)
This is a new test file. It fails on all 4 Darwin CI runners (13 aarch64, 14 aarch64, 14 x64, 13 x64). Investigate the specific test failure on macOS — it could be the cron registration tests interacting with crontab differently on macOS, or the execution mode tests failing.
Other notes
test/integration/next-pages/test/dev-server.test.tsfailure on macOS 13 aarch64 looks like an infra flake (single platform).- The code itself looks well-structured. The cron parser, OS-level registration (crontab/launchd/schtasks), and POSIX OR semantics for day-of-month/day-of-week are all solid.
- Zero unresolved review threads.
|
@robobun fix the red ci |
Summary
Adds a complete cron API to Bun: register OS-level cron jobs, remove them, and parse cron expressions to compute the next matching time.
New APIs
How a cron job runs
When registered, the OS scheduler invokes
bun run --cron-title=<title> --cron-period='<schedule>' <script>at each scheduled time. Bun imports the module, findsdefault.scheduled(), and calls it:This follows the Cloudflare Workers Cron Triggers API. The handler can be
async— Bun waits for the promise to settle before exiting.--cron-titleand--cron-periodmust be provided together (validated at startup).Cron expression features
minute hour day-of-month month day-of-weekSUN–SAT,Sunday–Saturday(case-insensitive)JAN–DEC,January–December(case-insensitive)7as alias for0@yearly,@annually,@monthly,@weekly,@daily,@midnight,@hourly*), either matching fires the job — standard POSIX cron behavior*,,(list),-(range),/(step)How
Bun.cron()works per platformjournalctl -u cronStartCalendarInterval/tmp/bun.cron.<title>.{stdout,stderr}.log/ststart time and/ddayRe-registering with the same title overwrites the existing job in-place.
Implementation details
cron_parser.zig): Bitset representation —u64for minutes,u32for hours/days,u16for months,u8for weekdays. Usesbun.ComptimeStringMapwithgetASCIIICaseInsensitive()for name lookups andbun.stringsthroughout.GregorianDateTimefunctions each iteration. Searches up to ~4 years.Bun__gregorianDateTimeToMSUTCandBun__msToGregorianDateTimeUTCfor UTC time decomposition/recomposition.Monday,@daily) are converted to numeric form (1,0 0 * * *) viaformatNumeric()before platform registration.vm.transpiler.resolver.resolve()relative to the caller's source directory for full module resolution (TypeScript, package.json exports, node_modules).cronToCalendarInterval: Now expands comma-separated values into cartesian-product<dict>entries, supporting complex expressions.cronToSchtasks: Now passes/st HH:MMstart time and/d DAYfor DAILY/WEEKLY schedules.--cron-title/--cron-periodvalidation: Must be provided together (fails fast at startup otherwise).bun.sys.getcwd: Uses bun's syscall wrapper instead ofstd.posix.getcwd.bun.env_var: Uses type-safe cached env var accessors (HOME,PATH) instead ofbun.getenvZ.Bug fixes
filterCrontabsubstring matching: Fixed pre-existing bug where removing job"test"could also remove"test-cleanup"due toindexOfsubstring matching. Changed to exact line match withbun.strings.eql.Test plan
bun bd test test/js/bun/cron/cron.test.ts)bun run zig:check-all)describe.skipIf(!isLinux)and save/restore original crontabparse()calls verify patterns produce correct sequences (not just single results)0 0 15 * FRIproduces both 15th-of-month and Friday matches, with assertion that Jan 15 (Wednesday) appears in resultsgetUTCDay()/getUTCMonth()MON-FRI/2verified to produce Mon, Wed, Fri sequence🤖 Generated with Claude Code