DeploymentJobService.run_job dispatched on a hardcoded switch over three
core kinds, so a plugin could not own work that behaves like a
deployment. Its only option was the generic job system, which gives no
step plan, no streaming console and no place in Deploy Activity — the
reason promoting a WordPress environment, which is deploy-shaped in every
respect, is invisible in the one page you open to see what ran.
plugins_sdk.deploys lets a plugin register a kind and start one. The job
is a real DeploymentJob, so it inherits the Deploy Console, live logs,
retry and a row in the feed with no frontend work at all — those surfaces
key off the job, not off any particular kind, and the feed already
renders an unknown kind by its id.
Three things the SDK guarantees, each pinned by a test:
- Core kinds cannot be hijacked. A plugin registering app_deploy would
silently take over real deployments.
- A handler that raises still ends the job visibly failed. Third-party
code must not be able to leave a job spinning at 'running' with no
explanation — the same promise _reconcile_interrupted_jobs makes for a
lost process, extended to a lost handler.
- A handler that finalises the job itself keeps that outcome, even if its
return value contradicts it.
deploys.steps() advances the job through its plan as the handler works,
so the console's pipeline strip and the feed's tick bar fill in, and a
failure records which step failed rather than only a stack trace.
The implementation is deploys_sdk.py, not deploys.py: a submodule of that
name is bound as a package attribute on import and would shadow the lazy
accessor, handing callers the module instead of the SDK.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>