feat(docker): scheduled reminders via supercronic#6
Merged
Conversation
Add in-container scheduling so the dockerized agent can fire reminders on a cron schedule and deliver results to Telegram — without a host crontab. Why supercronic over crond: classic crond scrubs the environment from its jobs (so env_file vars like ODEK_API_KEY / the bot token never reach a tick) and wants root to setuid, clashing with the non-root container user. supercronic runs as the normal user and passes its own environment to each job, so a scheduled `odek run --deliver` sees exactly what the bot sees. - Dockerfile: install supercronic v0.2.46, pinned by SHA-256 computed from the official release assets (arch-aware via TARGETARCH); add a cron-entrypoint.sh wrapper that starts supercronic only when a crontab is mounted, then execs odek (PID-1 semantics, signals, and the Telegram lock unchanged). - compose: mount ./crontab into both telegram profiles. - telegram config: read ODEK_TELEGRAM_DEFAULT_CHAT_ID from env (was config-file only), so --deliver's target chat can live in .env like everything else. - docs + .env.example: document reminders and the new env var. E2E verified (build + run): supercronic SHA-256 checks out, a cron job inside the container inherits an injected env var, and the no-crontab path still runs odek unchanged. Unit tests cover the new env-var parsing (incl. negative IDs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five findings from a post-merge review, all verified:
C1 — graceful restart (/restart) re-execs the bare odek binary, bypassing
cron-entrypoint.sh; supercronic was never restarted.
Fix: cron-entrypoint.sh exports ODEK_ENTRYPOINT=$0 and ODEK_SUPERCRONIC_PID.
spawnChild() uses ODEK_ENTRYPOINT when set so the wrapper is re-entered on
restart. The wrapper kills the old supercronic PID before starting a new one,
preventing duplicate scheduler instances.
C2+C3 — no init process: supercronic zombies on exit; SIGTERM from docker stop
not forwarded to supercronic (in-flight jobs killed abruptly at SIGKILL).
Fix: init: true on both telegram compose services. Docker's built-in init
becomes PID 1, reaping orphaned children and forwarding SIGTERM to the process
group.
C4 — arch="${TARGETARCH:-amd64}" silently installed the wrong-arch supercronic
binary on arm64 hosts building without BuildKit; the SHA check still passed.
Fix: change to ${TARGETARCH:?...} — a hard build failure with an actionable
error message rather than a silent wrong-arch download.
C5 — Docker creates a root-owned directory at ./crontab on the host when the
source path is missing from a bind mount; [ -f ] returned false silently.
Fix: add [ -d ] branch with an explicit warning explaining the cause and fix.
C6 — supercronic backgrounded with &; set -e does not apply to background
processes, so an immediate startup failure was silently swallowed.
Fix: sleep 1 + kill -0 liveness check after launch; emits a clear WARNING if
supercronic exits immediately, rather than proceeding as if cron is running.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The graceful-restart fix in the previous commit added an ODEK_ENTRYPOINT check to spawnChild — when set by cron-entrypoint.sh, the child is re-exeucted through the wrapper so supercronic is restarted. That branch was not covered. Add three targeted tests: - TestSpawnChild_UsesODEKENTRYPOINT: exercises the true branch (ODEK_ENTRYPOINT set) — spawnChild must call os.StartProcess with the wrapper path, not the odek binary. Uses /bin/sh as a universally present stand-in executable. - TestSpawnChild_ODEKENTRYPOINTEmpty_FallsBackToOdekBinary: empty env var must not override the executable (false branch). - TestSpawnChild_ResolvedAPIKeyInjected: API key is appended to childEnv only, not leaked into the current process environment. spawnChild coverage: 68.4% → 89.5%. Co-Authored-By: Claude Opus 4.8 (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.
Summary
odek run --deliverreminders can fire on a schedule without a host crontabODEK_TELEGRAM_DEFAULT_CHAT_IDenv var support added (was config-file-only) so the delivery target lives in.envWhy supercronic over crond
Classic
crondscrubs the environment —ODEK_API_KEYand the bot token never reach a cron tick. supercronic runs as a non-root user and passes its own environment to every job.Changes
docker/DockerfileTARGETARCH:?);cron-entrypoint.shwrapper as new ENTRYPOINTdocker/cron-entrypoint.shODEK_ENTRYPOINT+ODEK_SUPERCRONIC_PIDfor restart safety; liveness check + directory-mount warningdocker/docker-compose.yml./crontabinto telegram services;init: truefor zombie reaping and SIGTERM forwardingdocker/crontabdocker/.env.example+README.mdinternal/telegram/config.goODEK_TELEGRAM_DEFAULT_CHAT_IDfrom envcmd/odek/telegram.gospawnChildusesODEK_ENTRYPOINTwhen set — graceful/restartre-launches through the wrapper so supercronic is restarted*_test.goConfigFromEnv(DefaultChatID); 3 tests forspawnChild(ODEK_ENTRYPOINT branch, empty fallback, env isolation);spawnChildcoverage 68% → 89%Test plan
make testpasses (unit + telegram package)--build-arg TARGETARCH=arm64TARGETARCHproduces a hard build error (not a silent wrong-arch download)/restartin Telegram chat: supercronic restarts (no duplicate instances)docker stop: SIGTERM reaches supercronic (init: true forwards it)🤖 Generated with Claude Code