Repository navigation
Add ms and Temporal.Duration inputs for CI - #1782
Conversation
Jobs, commands and extra machines become spans, CI's steps are named for what they do and marked as CI's work. A job's `name` sets its span name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every span is an entry in the `spans` catalog with a required kind, step IDs are built by `joinId`, and starting a machine is a flat staged function in `machine/start.ts`. Folds the CI half of ci2-span-kinds: commands, sandboxes, saves and attempts carry kinds, each statement on a background process is a row of its own, and a snapshot's span is passed to an SDK that takes it (feature-detected by version) and opened by CI otherwise. Names the checkout steps. Adds a `ciTest` harness. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving it to its own file cost more diff than it saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The span API and a snapshot's own span are always there, so the stub, the version gate and the dual snapshot-span path are gone. The shared test harness had one user, so it is local to that test again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he lookups CI's step IDs and names move to one catalog in pipeline/names.ts, run through a thin ciRun helper that tags the step as before. Sandbox paging and snapshot deletes (machine/admin.ts), snapshot names (cache/names.ts), the cache lookup and the check-complete payload each have one implementation. No step ID, name or wire format changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cached snapshot older than cache.maxAge counts as a miss. One usableFor predicate decides whether a snapshot may be used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every duration reads through one durationToMs backed by the SDK's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… calls A later direct call has the path `id (n)`, which is not a registry key, so its span lost the configured name. The span now takes the name from the job's config and keeps the call number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adapt the job span name fix into the steps catalog refactor: names.ts keeps the catalog and takes traceName.job(config, path) from the base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al to main's helpers) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 6ff5af2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # packages/ci/src/pipeline/job.ts
There was a problem hiding this comment.
🔵 Needs a closer look
Sandbox finalization can alter retry behavior, skip cleanup, or share resources across unrelated runs when async context is unavailable.
0 open findings
What changed in this PR
Extends the SDK’s shared duration handling to @inngest/ci, building on #1777. The changes also include sandbox lifecycle APIs and release updates.
Changes:
- Accept milliseconds, duration strings, and
Temporal.Durationacross CI duration options. - Add scoped sandbox cleanup and middleware-supported sandbox groups.
- Expand tests and update documentation and package compatibility.
| File | Description |
|---|---|
| packages/inngest/tsdown.config.ts | Adds disposable declaration references. |
| packages/inngest/tsconfig.json | Enables disposable types. |
| packages/inngest/src/types.ts | Generalizes middleware context extensions. |
| packages/inngest/src/test/helpers.ts | Adds retry context to test executions. |
| packages/inngest/src/index.ts | Exports shared duration utilities. |
| packages/inngest/src/helpers/duration.ts | Implements shared duration parsing. |
| packages/inngest/src/helpers/duration.test.ts | Tests duration parsing and validation. |
| packages/inngest/src/experimental.ts | Exports sandbox group types. |
| packages/inngest/src/components/sandbox/validation.ts | Uses shared duration validation. |
| packages/inngest/src/components/sandbox/types.ts | Exposes disposable sandbox creation. |
| packages/inngest/src/components/sandbox/trace.test.ts | Reuses shared test fixtures. |
| packages/inngest/src/components/sandbox/test-helpers.ts | Adds sandbox lifecycle fixtures. |
| packages/inngest/src/components/sandbox/middleware.ts | Adds sandbox groups and cleanup warnings. |
| packages/inngest/src/components/sandbox/lifetime.test.ts | Tests disposal and cleanup failures. |
| packages/inngest/src/components/sandbox/group.ts | Implements callback-scoped sandbox lifetimes. |
| packages/inngest/src/components/sandbox/group.test.ts | Tests grouping and cleanup behavior. |
| packages/inngest/src/components/sandbox/durable.ts | Adds disposal and idempotent destruction. |
| packages/inngest/src/components/InngestSandbox.ts | Re-exports sandbox group types. |
| packages/inngest/src/components/InngestSandbox.test.ts | Tests sandbox duration limits. |
| packages/inngest/src/components/Inngest.ts | Types middleware group extensions. |
| packages/inngest/package.json | Updates SDK version. |
| packages/inngest/CHANGELOG.md | Documents SDK release changes. |
| packages/ci/src/version.ts | Updates CI runtime version. |
| packages/ci/src/util.ts | Wraps shared parsing with CI errors. |
| packages/ci/src/units.test.ts | Expands duration validation tests. |
| packages/ci/src/types.ts | Broadens the public duration type. |
| packages/ci/src/types.test.ts | Checks expanded duration types. |
| packages/ci/src/testing/temporal.ts | Adds a Temporal-shaped test fixture. |
| packages/ci/src/pipeline/pipeline.test.ts | Tests timeout inputs and retention disabling. |
| packages/ci/src/pipeline/job.ts | Validates job duration options. |
| packages/ci/src/pipeline/cacheGuarantees.test.ts | Tests job duration validation. |
| packages/ci/src/machine/command.ts | Normalizes command timeouts eagerly. |
| packages/ci/src/github/helpers.ts | Normalizes GitHub wait timeouts. |
| packages/ci/src/checkout/wait.ts | Uses normalized numeric timeouts. |
| packages/ci/src/cache/cache.ts | Labels cache duration errors. |
| packages/ci/README.md | Documents supported duration inputs. |
| packages/ci/package.json | Updates version and SDK requirement. |
| packages/ci/CHANGELOG.md | Adds CI release notes. |
| .changeset/sandbox-snapshot-span-kind.md | Removes released snapshot changeset. |
| .changeset/ci-trace-spans.md | Removes released tracing changeset. |
| .changeset/ci-durations.md | Adds duration support release notes. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
[Medium impact] Adds duration input formats to CI and sandbox APIs. Fix the sandbox cleanup retry path before merging; it can retry work after deleting the sandbox. Findings
Fix with agent prompt### Issue 1
packages/ci/src/machine/command.ts:393
`CommandTimeoutError` now uses `formatDuration()`, which rounds values of at least one second. For example, `.timeout(1500)` enforces 1,500 milliseconds but reports “timed out after 2s.” This makes timeout failures harder to explain. Keep the exact limit in the error with a formatter that preserves milliseconds.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds milliseconds and
The
|
Comments Outside DiffThese findings could not be posted inline.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @inngest/ci@0.4.0 ### Minor Changes - [#1779](#1779) [`314fb478`](314fb47) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Cached jobs can now expire with `cache.maxAge`: a snapshot older than it is rebuilt. ```ts ci.job({ id: "base", cache: { key: files("images/node-base/**"), maxAge: "1d" } }, …); ``` - [#1780](#1780) [`4491a641`](4491a64) Thanks [@jpwilliams](https://github.com/jpwilliams)! - `checkout()` and `files()` can now read another repository or ref, including in runs whose trigger has no repository. ```ts await checkout({ repo: "acme/shared-config", ref: "main" }); ``` - [#1782](#1782) [`c410e598`](c410e59) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Every duration option (`cache.maxAge`, `keepOnFailure`, `.timeout()`, `waitForPort`, `waitForHttp`, `github.waitForChecks`, `waitForWorkflow`) now takes milliseconds, a string like `"1d"` or `"1h30m"`, or a `Temporal.Duration`. A bad value throws a `CiUsageError` naming the field. A bare numeric string like `"10"` now means 10 milliseconds. ```ts await waitForPort(3000, { timeout: "1m30s" }); ``` - [#1803](#1803) [`0e733c99`](0e733c9) Thanks [@jpwilliams](https://github.com/jpwilliams)! - A job can now start from a job another app defines with `image.job()`. That app builds it once for every app that asks. ```ts import { image } from "@inngest/ci"; ci.job({ id: "test", from: image.job("platform/node-base") }, async () => { await # @inngest/cipnpm test`; }); ``` - [#1803](#1803) [`0e733c99`](0e733c9) Thanks [@jpwilliams](https://github.com/jpwilliams)! - You can now define jobs inside a pipeline, and start other jobs `from` them. They build in the run that defined them, and cached ones are still reused by later runs. ```ts ci.pipeline({ id: "pr", on }, async () => { const base = ci.job("base", async () => { await # @inngest/cipnpm install`; }); const lint = ci.job({ id: "lint", from: base }, async () => { await # @inngest/cipnpm lint`; }); await lint(); }); ``` Defining the same job ID twice in one run now throws a `CiUsageError`. ### Patch Changes - [#1803](#1803) [`0e733c99`](0e733c9) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Mistakes in a job's `from` now fail when your app boots, with a message naming the problem, instead of partway through a run. This covers a parent from another CI client, a value that isn't a job, input that doesn't match the parent's schema, and `from` cycles (which used to build forever). ```ts ci.job({ id: "test", from: notAJob }, …); // throws at boot ``` ## inngest@4.25.0 ### Minor Changes - [#1802](#1802) [`40af8b5b`](40af8b5) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Choose the image a sandbox starts from with `image.inngest()` or `image.custom()`, exported from `inngest/experimental`. ```ts import { image } from "inngest/experimental"; await step.sandbox.create("build", { name: "build", vcpu: 2, memoryMb: 2048, from: image.inngest("base"), }); await step.sandbox.create("custom", { name: "custom", vcpu: 2, memoryMb: 2048, from: image.custom("my-image:v1"), }); ``` - [#1756](#1756) [`5dc1279d`](5dc1279) Thanks [@jakobevangelista](https://github.com/jakobevangelista)! - Select an already-published image with `image` in `inngest.sandboxes.create()` or `step.sandbox.create()`. Names, tags, and artifact SHA-256 references are supported. Omission keeps the default base image; snapshot clones retain their source image and cannot select another. Sandbox resources expose an optional `imageDigest` containing the pinned artifact digest. ### Patch Changes - [#1801](#1801) [`023ebfa3`](023ebfa) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Functions using `optimizeParallelism: false` once again complete the run when the function returns, as they did in v3. In v4 this changed unintentionally: the run waited for steps you started but never awaited. If you don't use `optimizeParallelism: false`, nothing changes. ```ts inngest.createFunction( { id: "example", optimizeParallelism: false, triggers: [{ event: "app/example" }], }, async ({ step }) => { step.sleep("unused", "7d"); return "done"; }, ); ``` - [#1791](#1791) [`b112a09e`](b112a09) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: code running in a sandbox can now call functions in your app. Pass `tools` to `commands.run`, and each call the code makes runs your function as its own durable step, so it's retried and memoized like any other step. ```ts await sandbox.commands.run("agent", "node agent.js", { tools: { getCustomer: (id: string) => db.customers.find(id), }, }); ``` ```js // agent.js, inside the sandbox const { tools } = require("inngest_tools"); const customer = await tools.getCustomer("cus_1"); ``` In Python, use `from inngest_tools import tools` and `await tools.getCustomer("cus_1")`. A tool that fails raises a `ToolError` the code can catch. - [#1783](#1783) [`167709c6`](167709c) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: you can now run Python in a sandbox with `step.sandbox.python`, available with `sandboxMiddleware()`. It creates a sandbox, runs your code, and destroys it, and a top-level `return` gives the result back. Pass data in with `input`: each key becomes a variable. A non-zero exit fails the step with the error output. Code and input of any size work, and results can be as large as a step can return. Pass a schema as `schema.output` to validate the result: it's typed from the schema (as its JSON form, like `step.run`), and a result that doesn't match fails the step with the schema's issues. ```ts const stats = await step.sandbox.python( "stats", `import statistics return {"mean": statistics.mean(values)}`, { input: { values: [1, 2, 3, 4] }, schema: { output: z.object({ mean: z.number() }) }, }, ); ``` Like any other step, it takes a step ID or step options, such as `{ id: "stats", name: "Compute the mean" }`, as its first argument. - [#1787](#1787) [`dc120418`](dc12041) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: inline sandbox runner code can be indented to match your source; it's dedented before it runs. - [#1793](#1793) [`27b3631c`](27b3631) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: `step.sandbox.python` and `step.sandbox.typescript` now take `tools`. The code calls them as `tools.name(...)` with no import, and each call runs your function as a durable step. ```ts const total = await step.sandbox.python( "total", `customer = await tools.getCustomer("cus_1") return customer["seats"] * 10`, { tools: { getCustomer: (id: string) => db.customers.find(id) }, schema: { output: z.number() }, }, ); ``` - [#1795](#1795) [`e308aaa3`](e308aaa) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: sandbox code can now call `tool.group()` tools, whose body is a group of real steps. Run steps in parallel, invoke other functions and use their result, send events, or wait for one, all from a single tool call. ```ts import { step } from "inngest"; import { tool } from "inngest/experimental"; const refund = tool.group({ input: z.object({ orderId: z.string() }), run: async ({ orderId }) => { const result = await step.invoke("refund", { function: refundFn, data: { orderId }, }); await step.sendEvent("notify", { name: "order/refunded", data: result }); return result; }, }); ``` - [#1796](#1796) [`0cc19e58`](0cc19e5) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: an Inngest function, or a `referenceFunction()` to one, can now be a sandbox tool. The code calls it with named arguments, which become the event data, and gets the function's result back. The invoke is a durable step. ```ts import { eventType } from "inngest"; const refund = inngest.createFunction( { id: "refund", triggers: [ eventType("order/refund", { schema: z.object({ orderId: z.string() }), }), ], }, async ({ event }) => issueRefund(event.data.orderId), ); await sandbox.commands.run("agent", "node agent.js", { tools: { refund }, }); ``` The arguments are checked against the function's trigger schema when it has one. Without a schema, any named arguments are sent; wrap the invoke in `tool.group()` to declare the input you want. - [#1794](#1794) [`80016cf0`](80016cf) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: declare a sandbox tool with `tool.step()` to give it typed, named arguments. The `input` schema is checked before anything runs, so a bad call gets a `ToolError` back in the sandbox, and `output` checks the result. A plain function in `tools` still works, and takes positional arguments. ```ts import { tool } from "inngest/experimental"; const getCustomer = tool.step({ input: z.object({ id: z.string() }), output: z.object({ name: z.string() }), run: async ({ id }) => db.customers.find(id), }); await sandbox.commands.run("agent", "node agent.js", { tools: { getCustomer }, }); ``` ```js // agent.js, inside the sandbox const { tools } = require("inngest_tools"); const customer = await tools.getCustomer({ id: "cus_1" }); ``` `tool.step()` works with `commands.run` and with the Python and TypeScript runners. Its body also receives an idempotency key that stays the same across retries, for APIs that deduplicate writes. - [#1784](#1784) [`f5b71099`](f5b7109) Thanks [@jpwilliams](https://github.com/jpwilliams)! - Experimental: you can now run TypeScript in a sandbox with `step.sandbox.typescript`, available with `sandboxMiddleware()`. `input` keys become variables, a top-level `return` gives the result back, and top-level `await` works. It takes the same `schema.output` as `step.sandbox.python` to validate and type the result. The sandbox image needs Node 22.13 or later. ```ts const total = await step.sandbox.typescript( "sum", `return numbers.reduce((a: number, b: number) => a + b, 0)`, { input: { numbers: [1, 2, 3] } }, ); ``` Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
Brings #1777's time inputs (
numberof milliseconds, ms-compatiblestring, orTemporal.Duration) to@inngest/ci's:maxAgekeepOnFailure.timeout()waitForPortwaitForHttpgithub.waitForCheckswaitForWorkflowChecklist
Added a docs PR that references this PRN/A Later bumpRelated
msandTemporal.Durationtimeouts for Sandboxes #1777