PR #245 merged: feat: add run_once action to scheduled_tasks (#233)
Summary
Adds action: "run_once" to the scheduled_tasks tool — a composite that runs a one-off command in an app/service container and returns its output. There is no upstream trigger/run-now endpoint for scheduled tasks (verified: no such route in docs/coolify-openapi.yaml, no such client method), so this composes the existing create / list_executions / delete client methods rather than adding a new endpoint.
Params: resource ('application'|'service'), uuid, command, container, optional timeout, optional wait_seconds (poll budget, default 90).
Behaviour:
- Creates a task with a generated
oneoff-<random suffix>name,frequency: "* * * * *", enabled. - Polls
list_executionsevery ~5s (attempt-counted, not wall-clock-gated, so it stays testable) for up towait_secondsuntil the first execution reaches a terminal (non-running) status. - Always deletes the task afterwards — success, timeout, or a polling error all fall through to the same cleanup step (Rust-
finally-equivalent structure). If the delete itself fails, the response says so loudly, includes the task UUID, and warns that the cron will keep firing every minute until it's removed by hand. - Returns the execution's
status+message(the API returns the command's stdout inmessage).
Idempotency caveat (carried in the tool description): the underlying cron can fire more than once before cleanup lands — deleting right after the first observed execution shrinks the window but doesn't close it. Commands must be idempotent or tolerant of re-execution.
Command length: noted in the tool description that Coolify silently 500s on scheduled-task commands over ~255 chars (#234, still open/unfixed on this branch) — not implemented as a hard .max() validation here since that's #234's scope, just documented so run_once callers aren't surprised.
Type modelling (#237 item 2): turns out ScheduledTaskExecution.message already existed in src/types/coolify.ts (added back in #172) — the issue's premise was slightly stale. What was actually missing was the list_executions tool description not mentioning that stdout lives in message; fixed that instead.
Docs: added the every-minute/idempotency gotcha (plus the "no run-now endpoint" context and the message-is-stdout note) to site/concepts/coolify-api-gotchas.md.
Closes #233 Closes #208
Test plan
-
npm test— 369 tests passing, coverage 98.1%/86.36%/99.53%/98.91% (all above threshold) - New
scheduled_tasks tool handler - run_oncesuite covers: success path (create→poll→return output→delete called), timeout path (no execution appears, delete still called, message includes task UUID), poll-error path (list_executions throws, delete still called, error surfaced), delete-failure path (message warns loudly with UUID + underlying error), input validation, and the service-resource variant -
npm run lint— 0 errors (4 pre-existing warnings, none introduced by this change) -
npx prettier --check .— clean -
npm run build— clean
Notes / uncertainties
- The poll loop counts attempts (
wait_seconds / 5) rather than gating on wall-clock time, specifically so the "instant sleep" test stub doesn't turn timeout tests into real multi-second waits. This is a slight approximation of "poll every ~5s for wait_seconds" but keeps behaviour deterministic and tests fast. sleepis a private class method overridden directly on the instance in tests (server.sleep = () => Promise.resolve()) rather than viajest.spyOn, becausejest.spyOn's generic inference reduces toneverwhen intersecting an object type with a class that already declares the member asprivate. Flagging in case there's a preferred pattern already in use elsewhere I missed.- Did not touch
package.json/CHANGELOG.mdper the task constraints — happy to add a changelog entry if that's expected before merge.
🤖 Generated with Claude Code