Repository navigation
cronjob: idempotent CRUD over a user crontab - #104
Merged
Merged
Conversation
A crontab has no identity: nothing in the file says which line is the nightly
summary. So the obvious way to install a job is to append, and the obvious
consequence is that a deploy step which reruns gives you the job once per run,
firing N times for N deploys, with nothing afterwards to say which copy was
meant to be there.
Identity comes from a marker comment. A managed job is exactly two lines:
# cli-tools:cronjob nightly-summary
0 9 * * * /usr/bin/whatever
`set` replaces those two lines IN PLACE when the id already exists, so the
first run and the thousandth leave a byte-identical file and a clean diff.
Appending would still converge, but it reorders the file on every changed job
and makes the diff unreadable. `remove` on an absent id changes nothing and
exits 0, because the requested end state already holds. Duplicate ids left by
a hand edit collapse onto the first, since leaving them means the job still
fires twice after a "fix".
Everything outside a marker block is preserved exactly -- hand-written
entries, MAILTO, PATH, blank lines, other people's comments. This is a shared
file and we are a guest in it.
Three details that are silent failures rather than errors:
- `%` is not an ordinary character in a crontab command. cron reads the first
unescaped one as end-of-command and feeds the rest to the job as stdin, so
`date +%Y-%m-%d` runs as `date +` and quietly does the wrong thing. Escaped
on write, and not double-escaped on rewrite.
- A crontab must end in a newline or some crons ignore the last line, which is
a job that looks installed and never runs.
- `crontab -` replaces the whole file, so this is read-modify-write over
shared state. The write holds an flock, or two deploys landing together let
the later read win and drop the earlier job.
Verified end to end against a real crontab carrying 17 unmanaged lines: three
`set` runs produced byte-identical output with one copy of the job and `%`
escaped, two `remove` runs were idempotent, and the file came back identical
to the pre-test backup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan23 finding(s) HIGH/CRITICAL: 4 | MEDIUM: 10 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
`registry.test.ts` reads the real bin directory, so adding bin/cronjob.ts alone made "gives every command a summary" fail. That test is doing its job: a tool with no summary is invisible in `cli-tools list`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cronjobinstalls, lists and removes scheduled jobs without ever installing one twice.A crontab has no identity: nothing in the file says which line is the nightly summary. So the obvious way to install a job is to append, and the obvious consequence is the job once per deploy.
Identity comes from a marker comment — a managed job is exactly two lines:
setreplaces in place, so run 1 and run 1000 leave a byte-identical file and a clean diff.removeon an absent id changes nothing and exits 0.MAILTO,PATH, comments.Three silent failures it handles
%in a commanddate +%Y-%m-%druns asdate +and quietly does the wrong thingcrontab -replaces the whole fileflockso two deploys landing together cannot drop each other's jobVerification
21 unit tests, plus an end-to-end run against a real crontab carrying 17 unmanaged lines: three
setruns produced byte-identical output with one copy of the job and%escaped to\%, tworemoveruns were idempotent, and the file came back identical to the pre-test backup.tsc --noEmitclean.Usage
First use is the coinpayportal nightly summary, which has never had a caller — no Railway service in the project has a
cronSchedule(checked all 72) and dev2 has no host cron or systemd timer for it.🤖 Generated with Claude Code