@render-lab/tasks-render
v0.2.6
Published
Durable Render control-plane tasks for Render Workflows: deploys, provisioning, convergent deletion, diagnostics, and scaling.
Downloads
506
Readme
@render-lab/tasks-render
⚠️ Experimental: proof of concept. This package is part of the Render Tasks POC and is published for testing only. It is not fully tested or production ready. Task names, inputs, outputs, and behavior can change or break in any release. Pin exact versions and expect breaking changes.
Durable Render control-plane tasks for Render Workflows. A first-party pack (ADR-0015) — the workflow runs on Render, so orchestrating Render itself is first-party, not "one cloud provider among many."
Scope is deliberately narrow: the async orchestration verbs that benefit from
durability — deploy, scale, suspend/resume, provision, convergent deletion, and bounded
deploy diagnostics. This is not an API mirror. Low-level lookup, pagination, log
filtering, and resource CRUD stay inside the package's internal RenderPort; they are not
registered as one Workflow task per API endpoint.
import {
triggerDeploy,
awaitDeploy,
provisionService,
deleteResource,
getDeployDiagnostics,
} from "@render-lab/tasks-render";| Task | Input | Output |
| --- | --- | --- |
| render.getService | { serviceId } | ServiceDTO |
| render.triggerDeploy | { serviceId, clearCache?, commitId? } | { serviceId, deployId, status } |
| render.awaitDeploy | { serviceId, deployId } | { serviceId, deployId, status, live } |
| render.scaleService | { serviceId, numInstances?, plan? } | { serviceId, numInstances, plan } |
| render.suspendService | { serviceId } | { serviceId, suspended } |
| render.resumeService | { serviceId } | { serviceId, suspended } |
| render.provisionPostgres | { name, ownerId?, plan?, region?, version?, … } | PostgresDTO |
| render.provisionKeyValue | { name, ownerId?, plan?, region?, maxmemoryPolicy? } | KeyValueDTO |
| render.createPrivateService | { name, repo? \| image?, branch?, ownerId?, … } | { serviceId, name, status, live, url } |
| render.provisionService | discriminated union for web_service, private_service, background_worker, or static_site | { service, created, deployId, status, live } |
| render.createProject | { name, ownerId?, environmentName? } | { project: { id, name }, environmentId } |
| render.deleteProject | { projectId } | { projectId, deleted, alreadyMissing } |
| render.deleteResource | { kind: "service" \| "postgres" \| "keyValue" \| "disk", id } | { kind, id, deleted, alreadyMissing } |
| render.getDeployDiagnostics | { serviceId, deployId, maxBytes?, maxLines? } | { deploy, logs, failureReason, truncated, nextStartTime } |
Deploy, then durably wait — the headline pattern
render.triggerDeploy kicks a deploy and returns its deployId; render.awaitDeploy
durably polls until it is live (return) or hits a failure terminal (throw a distinct
error a human should see). Splitting them keeps the non-idempotent trigger out of the poll
loop — the CI/CD "promote, then gate" shape, mirroring github.mergeWhenGreen.
const { deployId } = await triggerDeploy({ serviceId });
await awaitDeploy({ serviceId, deployId }); // throws until live / failedProvision + wait ready — idempotent by name
provisionPostgres, provisionKeyValue, createPrivateService, and provisionService
create a resource and then durably poll until it is ready. Because the durable poll
re-runs the whole task, the create is made idempotent by name (find-or-create):
a re-run finds the in-flight resource and keeps waiting rather than provisioning a
duplicate. Use unique, stable names per environment (e.g. orders-db-pr-42).
provisionService is the general service entry point. Its discriminated union supports
web services, private services, background workers, and static sites. Existing services are
reused only when immutable fields such as type and runtime are compatible; incompatible
matches fail rather than silently changing the resource.
Group resources under a project + environment
createProject is find-or-create by name: it lists the owner's projects, reuses a match,
and otherwise creates the project with a single environment (environmentName, default
"production"). Because a retried or resumed deploy re-runs the task, matching by name keeps
it from duplicating the project. It returns the project plus that environment's id, which you
thread into every provision so the resources land inside the environment rather than floating
in the workspace:
const { project, environmentId } = await createProject({ name: "acme-pr-42" });
await provisionService({ type: "web_service", name: "api-pr-42", environmentId, /* … */ });
await provisionPostgres({ name: "orders-db-pr-42", environmentId });environmentId is optional on provisionService, provisionPostgres, and provisionKeyValue;
omit it to create a floating workspace resource as before.
deleteProject deletes an empty project and treats a missing one as alreadyMissing: true.
It does not cascade — the Render API rejects the delete while any environment still holds
resources — so delete the contained services and datastores (via deleteResource) first, then
the project last.
Convergent deletion
deleteResource deletes services, Render Postgres instances, Render Key Value instances,
and disks by ID. Missing resources are treated as already deleted and returned with
alreadyMissing: true, which makes cleanup workflows safe to retry.
Bounded deploy diagnostics
getDeployDiagnostics fetches deploy metadata plus bounded build/runtime log context.
The port owns log pagination, filtering, control-character cleanup, and byte/line limits,
so the task returns a compact DTO with truncation metadata instead of unbounded logs.
Install
pnpm add @render-lab/tasks-render @renderinc/sdk@renderinc/sdk is a peer dependency. HTTP goes through @render-lab/tasks-core's shared
client — no vendor SDK.
Environment contract
| Variable | Required | Purpose |
| --- | --- | --- |
| RENDER_API_KEY | yes (at first use) | Render API key. Read lazily on the first call (ADR-0007). |
| RENDER_OWNER_ID | for provision* / createPrivateService | Default owner (team/user) id for created resources. Overridable per-input via { ownerId }. |
Durability notes
- The durable retry re-runs the whole task.
triggerDeployis not idempotent — a retry after a network failure can kick a second deploy. Where exactness matters, trigger once and rely onawaitDeployto wait rather than on the retry to re-fire the trigger.scaleService,suspendService,resumeServiceset an absolute target, so retrying is safe.provision*,provisionService, andcreatePrivateServiceare idempotent byname(see above), so their create-then-poll retry does not duplicate resources.deleteResourceis convergent, so retrying cleanup does not fail on a resource that is already gone.getDeployDiagnosticsis read-only and bounded. Use the Dashboard, CLI, or Render MCP for exploratory browsing and unbounded log analysis.
