Skip to content

feat(docker): scheduled reminders via supercronic#6

Merged
jkyberneees merged 3 commits into
mainfrom
feat/docker-supercronic-reminders
Jun 3, 2026
Merged

feat(docker): scheduled reminders via supercronic#6
jkyberneees merged 3 commits into
mainfrom
feat/docker-supercronic-reminders

Conversation

@jkyberneees
Copy link
Copy Markdown
Contributor

Summary

  • Adds in-container cron scheduling using supercronic (pinned to v0.2.46, SHA-256 verified) so odek run --deliver reminders can fire on a schedule without a host crontab
  • ODEK_TELEGRAM_DEFAULT_CHAT_ID env var support added (was config-file-only) so the delivery target lives in .env
  • All 6 post-merge code review findings addressed (zombie reaping, SIGTERM forwarding, graceful-restart bypass, wrong-arch fallback, silent startup failure, directory-mount confusion)

Why supercronic over crond

Classic crond scrubs the environment — ODEK_API_KEY and the bot token never reach a cron tick. supercronic runs as a non-root user and passes its own environment to every job.

Changes

File What
docker/Dockerfile Install supercronic (SHA-256 pinned, arch-aware via TARGETARCH:?); cron-entrypoint.sh wrapper as new ENTRYPOINT
docker/cron-entrypoint.sh Starts supercronic if crontab mounted; exports ODEK_ENTRYPOINT + ODEK_SUPERCRONIC_PID for restart safety; liveness check + directory-mount warning
docker/docker-compose.yml Mount ./crontab into telegram services; init: true for zombie reaping and SIGTERM forwarding
docker/crontab Example reminders file (all commented out)
docker/.env.example + README.md Document the new env var and cron workflow
internal/telegram/config.go Read ODEK_TELEGRAM_DEFAULT_CHAT_ID from env
cmd/odek/telegram.go spawnChild uses ODEK_ENTRYPOINT when set — graceful /restart re-launches through the wrapper so supercronic is restarted
*_test.go 4 tests for ConfigFromEnv (DefaultChatID); 3 tests for spawnChild (ODEK_ENTRYPOINT branch, empty fallback, env isolation); spawnChild coverage 68% → 89%

Test plan

  • make test passes (unit + telegram package)
  • Docker build succeeds with --build-arg TARGETARCH=arm64
  • Empty TARGETARCH produces a hard build error (not a silent wrong-arch download)
  • Container with mounted crontab: supercronic fires job and env vars are visible
  • /restart in Telegram chat: supercronic restarts (no duplicate instances)
  • docker stop: SIGTERM reaches supercronic (init: true forwards it)

🤖 Generated with Claude Code

jkyberneees and others added 3 commits June 3, 2026 16:35
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>
@jkyberneees jkyberneees merged commit a60e658 into main Jun 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant