Commit Graph

1208 Commits

Author SHA1 Message Date
Nathan Rajlich 45d1eb2340 web: configure vercelPreset() for Vercel deployments (#1815)
* web: configure vercelPreset() for Vercel deployments

Enables per-route bundle splitting, function-level configuration, and an
accurate Deployment Summary for the packages/web Vercel deployment.

Gated on WORKFLOW_WEB_VERCEL_BUILD (rather than the ambient VERCEL var)
so that the standard build layout consumed by server.js (self-hosted
deployments and the CLI's in-process server via @workflow/web/server) is
still produced when the package is packed as a tarball by the docs
Vercel deployment. Existing VERCEL-based checks in vite.config.ts have
been migrated to this same variable for consistency.

* web: allow WORKFLOW_WEB_VERCEL_BUILD through Turborepo

Turborepo strips environment variables not declared in turbo.json, which
prevented WORKFLOW_WEB_VERCEL_BUILD (set on the Vercel project) from
reaching the build. Declare it in the env list and also include the
.vercel/ directory (where the preset writes react-router-build-result.json)
in the build outputs.

* web: narrow Turbo output to react-router-build-result.json

* web: address PR review feedback

- Check WORKFLOW_WEB_VERCEL_BUILD === '1' explicitly (rejects '0', 'false',
  etc.) in both react-router.config.ts and vite.config.ts
- Use double-quoted package name in changeset frontmatter to match the
  repo's dominant convention
2026-05-04 10:04:21 +00:00
Peter Wielander 8ea1532e48 [core] Combine flow+step bundle and process steps eagerly (#1338) 2026-05-04 09:53:02 +00:00
Peter Wielander 9d94cf45cc [world-vercel] Revert stream close control framing (#1891)
This reverts commit 340c085681.
2026-05-04 06:29:08 +00:00
Peter Wielander ff336a0f73 [tarballs] Use turbo to build workspace deps before packing (#1908) 2026-05-04 06:11:59 +00:00
Pranay Prakash 1203dae70c Friendlier workflow errors (consolidated) (#1849)
* Introduce structured context-violation errors + Ansi renderer

Phase 1: Add Ansi rendering helpers (frame, hint, note, help, code, inline)
to @workflow/errors, and a chalk mock for readable snapshot tests.

Phase 2: Add four context-violation error classes to @workflow/core
(NotInWorkflowContextError, NotInStepContextError,
NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError)
and apply them to all twelve user-facing throw sites so errors now
include docs links and a structured "what/why/fix" frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review: tighten changeset, implement ansifyName, harden Ansi

- Tighten phase 1 changeset to a single sentence (per pranaygp review) and switch to double-quoted frontmatter (per Copilot + repo convention).
- Implement `ansifyName` to actually apply dim styling to workflow/ / step/ prefixes; add an `Ansi.dim` helper to `@workflow/errors` so callers don't need to import chalk directly.
- Remove the `void getWorkflowMetadata;` workaround in context-errors.ts by dropping the unused value import (we only needed the type and symbol).
- Render the plain-Error throw in `workflow/get-workflow-metadata.ts` with `Ansi.frame` + docs link so the VM path matches the structured-class styling from the sibling step path (still uses a plain Error to avoid the module-init cycle).
- Guard `buildUnderline` against zero-length markers so a stray empty token can't produce a negative `String.repeat` count.

* Structured runtime logger metadata + fold in replay-timeout logging

Adds a `.child()` and `.forRun(runId, workflowName)` child-logger API to
the structured logger so runtime/step code doesn't have to repeat
`workflowRunId`/`workflowName`/`stepId` on every call. Normalizes error
metadata to structured `errorName` / `errorMessage` / `errorStack` fields
instead of ad-hoc `error: err.message` strings, and adds comments to
silent catches that swallow expected idempotency conflicts.

Also folds in the pending changes from #1812 so that PR can be closed:

- Standardize the console prefix to `[workflow-sdk]`.
- Split the replay-timeout log into a warn-while-retrying vs.
  error-when-giving-up, and surface the underlying error when we can't
  mark a timed-out run as failed.
- Include the error stack in the "Fatal runtime error during workflow
  setup" log and in the top-level user-code workflow error log so the
  stack surfaces in flattened log drains.
- Drop the `[Workflows] "<runId>" - ` prefix from
  `buildWorkflowSuspensionMessage` — the structured logger now attaches
  run context.

Supersedes #1812.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Add SerializationError + apply to user-facing serialization sites

Phase 4 of friendlier errors: introduce a `SerializationError` class with
an optional `hint` and a docs link (workflow-sdk.dev/err/serialization-failed),
and adopt it at every user-facing serialization boundary in @workflow/core:

- Locked ReadableStream at a workflow boundary
- Unregistered class / missing `classId` / missing `WORKFLOW_DESERIALIZE`
- Attempting to return step functions to clients or call workflow functions
  directly
- Webhook `respondWith()` called outside a step
- `dehydrate*` / `getSerializeStream` failures (workflow args/return, step
  args/return, stream chunks)

Internal invariants (format prefix length checks, unknown format bytes,
missing `STREAM_NAME_SYMBOL`, encryption key/size guards, etc.) now throw
`WorkflowRuntimeError` instead of plain `Error` so the classifier and logger
treat them consistently.

`formatSerializationError` now returns `{ message, hint }` so the hint
fragment can be rendered with the standard SerializationError framing
instead of being baked into the message string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Presentation-only user vs SDK error attribution

Add describeError() that derives attribution and class-aware hints from
existing error classes + RUN_ERROR_CODES — no event data changes. Wire into
step failures, max-delivery exhaustion, run failures, and fatal setup errors
so terminal logs include errorAttribution and a hint for known error types.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review: describeError accepts precomputed errorCode + instanceof

- `describeError(err, errorCode?)` now accepts an optional precomputed
  `RunErrorCode`. `classifyRunError(err)` only narrows to USER_ERROR /
  RUNTIME_ERROR, so the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches
  were previously unreachable from the step / run failure log sites.
  Callers that know the failure category (runtime.ts for replay timeout and
  max-deliveries exhaustion) now pass the code in.
- Context-violation checks use `instanceof` against the actual classes from
  context-errors.ts instead of a name-string set. Type-safe + survives
  class renames.
- Wire the new hints through to the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED
  log sites so those branches actually render a hint now.
- 3 new tests cover the reachable code paths + precomputed-code override.
- Changeset frontmatter switched to double quotes per repo convention.

* Cosmetic consistency pass on remaining bare throws

Internal invariants now use WorkflowRuntimeError so describeError attributes
them to the SDK: missing startedAt, VM generateKey, closure-vars outside
step context, ENOTSUP. defineHook().resume() formats schema validation
failures as a readable list instead of a JSON blob.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use double-quoted changeset frontmatter per repo convention

* Data-driven describeRunError + expose via @workflow/core/describe-error

Observability renderers read persisted run_failed / step_failed event data,
not live Error instances. describeRunError takes { errorCode, errorName }
and returns the same { attribution, hint } shape as describeError, so the
CLI and web UI can derive user-vs-SDK framing from the event log directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Friendlier build-time errors: WorkflowBuildError class + applications

Add `WorkflowBuildError` class in `@workflow/errors` with optional `hint`
for an actionable next step, and apply it in `@workflow/builders` at
user-facing sites: failed esbuild phases, unresolved built-in steps, and
empty esbuild output now throw `WorkflowBuildError` with a hint pointing
at the likely fix. Runtime invariants remain plain `Error`.

* Polish friendlier-errors rendering: drop functionName leak, simplify docs link, redirect stack

- Drop the readonly `functionName` param-property on context-error classes so
  util.inspect no longer prints a trailing `{ functionName: 'foo()' }` block.
- Replace the `DocLink` ("label: https://…") shape with a plain `DocsUrl`
  template-literal type. Error output now renders a single clean line:
  `docs: https://…` (new `Ansi.docs` helper) instead of the noisier
  "note: Read more about foo(): https://…".
- Add throw helpers (`throwNotInWorkflowContext`, etc.) that call
  `Error.captureStackTrace(err, stackStartFn)` on V8 engines so the top frame
  of the thrown error points at the user's call site instead of at the gate
  function inside the framework. Callers pass themselves as the boundary.
- Refactor `defineHook()` (both root and `/workflow`) to use named function
  closures rather than `this.create`/`this.resume`, since the stack redirect
  relies on a stable function identity that survives destructuring.
- Update context-errors.test.ts to snapshot the new `docs:` framing and to
  add a regression test asserting the top stack frame is the user call site.

* Consolidate friendlier-errors stack: fix ANSI leak + non-retry semantics

Addresses PR review feedback across the 8-phase friendlier-errors stack and
fixes issues surfaced by manual testing (createHook() inside a step):

- ANSI no longer leaks into .message / .stack. Context-violation errors
  now store plain text on .message and render the colored framed form
  lazily via [util.inspect.custom] / toString(). Structured logs, log
  drains, CBOR-serialized events, and JSON payloads no longer contain
  raw \x1B[...m bytes.

- Context violations are now fatal. ContextViolationError sets
  fatal = true; FatalError.is(err) recognizes any error with a
  fatal: true own property. Calling createHook() from a step no longer
  burns three retry attempts on a guaranteed-to-fail context violation.

- Ansi helpers moved to @workflow/errors/ansi subpath so imports from
  @workflow/errors no longer pull chalk into consumers that only want
  error classes (addresses reviewer VaguelySerious).

- Shared redirectStackToCaller helper in packages/core/src/capture-stack.ts,
  used by both context-errors.ts and workflow/get-workflow-metadata.ts
  (addresses Copilot review on #1849).

- Structured framed content: ContextViolationError now takes a structured
  FramedContent (title segments + detail branches) and renders plain/pretty
  from the same source of truth.

Tightens the eight existing phase changesets to 1-2 sentences each and adds
four new scoped changesets (errors-ansi-subpath, context-errors-plain-message,
context-errors-fatal, capture-stack-shared) for the followup fixes, so the
final changelog history stays readable.

* test: update step-handler mocks for scoped forRun() logger

The runtime logger now uses .forRun(runId, name, {stepId, stepName})
to attach scope context, so 409-handling log calls no longer repeat
{workflowRunId, stepId} in every metadata bag — those live on the
scoped logger instance. Update the mock to return itself from forRun()
and tighten assertions to check both the log args (errorName/errorMessage)
and the forRun() scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Mark SerializationError fatal + route dehydration through step-failure path

SerializationError now carries readonly fatal = true. Step-return
dehydration is wrapped inside the user-code try/catch so that the
resulting error flows through userCodeFailed → step_failed →
FatalError.is() short-circuit instead of bubbling up as HTTP 500 and
triggering a queue retry loop. Retrying a step that returned a non-POJO
is guaranteed to fail the same way, so this saves ~20s and 3 near-
identical error blocks per serialization failure.

* Add logging snapshot tests + manual-test artifacts

Snapshot tests lock in the exact shape of:
- describeError() payloads (attribution, errorCode, hint) for every
  classification — plain Error, SerializationError, context-violation,
  WorkflowRuntimeError, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED.
- The scoped-logger call signature for the two canonical runtime
  failure paths (fatal-bubble and hit-max-retries), so refactors of
  forRun() / child() metadata merging can't silently change what users
  see in their log drains.

SerializationError now also has a direct test for readonly fatal=true
+ FatalError.is() recognition.

pr-artifacts/ contains real log-output snapshots from running the
nextjs-turbopack workbench against five error scenarios. These are
reference material for reviewers and are flagged to be removed before
merge.

* Readable step-fatal logs: inline stack + friendly step/workflow names

The step-level fatal-error log used to embed the full stack trace inside
an `errorStack` string field in the metadata object, so util.inspect
rendered it as a quote-escaped, line-continuation blob when the log
hit the terminal — unreadable in practice. Move framing + stack into
the log *message* (matching the workflow-level log in runtime.ts) and
keep the metadata object compact with only the indexable structured
fields (`errorAttribution`, `errorName`, `errorMessage`, `hint`,
IDs). Log drains still get the same keys; humans now see a readable
stack trace.

Also introduce `formatStepName` / `formatWorkflowName` in
`@workflow/utils` that render machine names
(`step//./workflows/1_simple//add`) as `add (./workflows/1_simple)` in
log framings, using the existing `parseStepName` / `parseWorkflowName`
parsers. Applied to step-fatal, hit-max-retries, exceeded-max-retries,
and workflow-threw log sites.

Artifacts in pr-artifacts/ updated to show the new output shape, and
renamed .log → .md since they're Markdown and IDE previews are nicer
that way.

* Opinionated pretty formatter for runtime structured-log metadata

Replace util.inspect's default object dump (which quote-escapes
multi-line stacks and paragraph hints into a single-line JSON-y blob)
with a workflow-aware formatter that composes the entire log line
into a single string passed to console.error / console.warn.

Highlights of the new output:
- Per-run / per-step IDs render with their parsed friendly names so
  users see `wrun_… · simple (./workflows/1_simple)` instead of just
  the raw `workflowName: 'workflow//./workflows/1_simple//simple'`.
- Color-coded attribution badge (user error red / sdk error magenta)
  paired with the error class in bold.
- Hints render as a paragraph under `hint:` rather than a backslash-
  `\n`-escaped string.
- Drops redundant fields (errorStack always; errorMessage when it's
  already in the parent message) to avoid double-printing.
- Unknown fields fall through as a sorted `key  value` tail so we
  never silently drop log information.

@workflow/errors/ansi gains bold/red/magenta helpers used by the
formatter. The web / web-shared packages don't consume stderr — they
read structured event payloads from the World event log — so this is
presentation-only at the runtime layer.

* ci(benchmarks): disable pnpm cache for getCommunityWorldsMatrix

The job never runs `pnpm install` (it just calls `node` against a
checked-in script), so the pnpm store path never exists. The post-job
`actions/setup-node@v4` cache-save then fails with `Path Validation
Error: Path(s) specified in the action for caching do(es) not exist`
and red-X's the entire job even though the matrix step succeeded.

The setup-workflow-dev composite already has a `cache-pnpm` opt-out
input for this exact case — wire it through here.

* Address PR review comments: inspect dedup, cause leak, retry-loop tests

- ContextViolationError: util.inspect(err) duplicated every framed detail
  line because the stack-tail strip only sliced the first message line.
  V8's Error.stack reads `Name: messageLine1\n  messageLine2\n  at ...`,
  so for our multi-line `title\n╰▶ docs: …` messages every detail line
  was getting prepended twice (once in the pretty form, once via the
  unsliced message tail). Count the actual message lines and slice past
  all of them. Repro test asserts `╰▶ docs:` appears exactly once.

- WorkflowError: stop assigning `cause: undefined` as an enumerable own
  property when no cause is provided. Subclasses (every error in this PR)
  inherit the parent constructor; the unconditional assignment polluted
  `util.inspect(err)` output with `{ cause: undefined, … }` on every
  no-cause instance. The `super(...)` call already conditionally sets
  `.cause` non-enumerably when `options.cause` is provided.

- step-handler.test.ts: add a regression-gate suite that exercises the
  fatal-vs-retryable retry-loop wiring directly. Asserts that an error
  with `fatal: true` produces exactly one `step_failed` event with no
  `step_retrying`, and that a non-fatal `Error` retries via
  `step_retrying` on early attempts and emits `step_failed` once the
  retry budget is exhausted. Catches the silent-regression case where
  `fatal = true` is removed from a context-violation error class but
  the `FatalError.is()` unit tests stay green.

* Consolidate changesets + remove pr-artifacts

Address review feedback to drastically shorten the changesets — fold
the 15 file-by-file entries into a single user-facing changeset for
@workflow/core / errors / builders / utils. Also drop the pr-artifacts/
folder (reviewer-only log captures, no longer needed).

* Polish runtime error logging: layout, stack trim, hint consolidation

Five user-driven fixes from manual smoke-testing of #1849:

1. Logger layout. composeLogLine() now puts the structured-fields block
   (attribution badge, run/step IDs, error code) **between** the framing
   line and the stack body, instead of after it where 30+ lines of stack
   buried the most useful information. The framing stays at the top,
   stack at the bottom, structured info readable at a glance.

2. Stack trim. Drops framework-internal frames (`node_modules/.pnpm/`,
   `node:internal/`, Turbopack-bundled `node_modules__pnpm_*` chunks,
   `_next_dist_*` chunks) and caps the surviving frame count at 6
   so the stack stays compact even on heavy async wrappers. Suppressed
   runs emit one summary line so users know the trim happened.

3. Wrapper-route noise. The nextjs-turbopack workbench's start route
   was catching `WorkflowRunFailedError` rejection on
   `Promise.race([readLoop(), run.returnValue])` and re-logging it via
   `console.error('Error in workflow stream:', error)` plus
   `controller.error(error)` — which then triggered Next.js's
   `⨯ failed to pipe response` overlay. The SDK already logs the
   failure cleanly upstream and the runId is on the response header, so
   the wrapper now closes the SSE stream cleanly on
   WorkflowRunFailedError.

4. Consistent framed `╰▶ hint:` / `╰▶ docs:` layout for all errors
   that carry a hint or docs slug. WorkflowError, SerializationError,
   and WorkflowBuildError now share one `appendFramedDetails` helper
   matching the box-drawing structure that ContextViolationError
   already used. Was: blank-line-separated `Learn more: <url>`. Now:
   one tree, indistinguishable from context-violation rendering.

5. Drop the duplicate logger-side `hint` field. Hints now live on the
   error message only — actionable hints get serialized into the event
   log, rehydrated on the workflow side, and shown in observability
   automatically. The previous logger-only hint duplicated stderr but
   never made it past the step boundary.

   Updated SerializationError hint to point at the foundations doc
   ("Ensure you're returning workflow serializable types. Check the
   serialization docs to see what's serializable:
   https://workflow-sdk.dev/docs/foundations/serialization") instead
   of the hardcoded `(plain objects, arrays, primitives, …)` list,
   which drifted out of sync as the supported types grew. Same hint
   reuses for step args, workflow args/return, stream messages, and
   any other site that goes through `formatSerializationError`.

Also retitled the retry summary `3 retries` → `3 max retries` since
"3 retries" next to "4 attempts" was ambiguous (already-happened vs.
budget).

* Trim error-card title + drop machine step name from persisted error

- ErrorStackBlock (web observability): show just the first non-empty
  trimmed line of the error message in the card title with single-line
  truncation. Multi-line messages (`Failed to serialize step return
  value\n╰▶ hint: …`) were rendering the entire framed body in the
  title, pushing the copy button off-screen and burying the
  scannability of the headline. Full message stays in the body via
  the stack (V8 prepends `Name: message` to `Error.stack`), so no
  information is lost; hover-tooltip exposes the full title text.

- Persisted error message: drop the `Step "step//./.../foo"` machine
  name from `Step failed after N retries: …` and `Step exceeded max
  retries (…)` strings. Observability already attributes the event
  to a specific step via the UI tree, and the CLI logger emits the
  friendly `Step foo (./...) hit max retries` framing on its own
  line. Embedding the raw `step//./...` machine name in the persisted
  message text was duplicate noise.

* Update .changeset/friendlier-errors.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* Update .changeset/pretty-log-format.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* Update SerializationError snapshot tests for slug-less message

The class no longer attaches a slug-based `╰▶ docs:` line — the
foundations URL is embedded directly in the hint via the
`formatSerializationError` helper in @workflow/core. Update the test
expectations accordingly:

- bare-title case is now a single line (no docs link)
- hint case renders one `╰▶ hint: …` branch (no second branch)

* Update serialization.test.ts hint assertions for foundations URL

Four `should throw error for an unsupported type` cases were still
asserting on the old hardcoded type list. Update to the new hint
phrasing that points at the foundations doc, matching the change in
`formatSerializationError` (`packages/core/src/serialization/errors.ts`).

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-05-04 05:04:24 +00:00
Peter Wielander a7071bf3d4 Fix pnpm type issue after tanstack PR (#1907) 2026-05-04 04:13:45 +00:00
Nathan Rajlich 059821cb39 ci: pass stale-banner via path: to sticky-pull-request-comment in tests + benchmarks workflows (#1887)
* Pass stale-banner via path: to sticky-pull-request-comment instead of message:

The 'Update existing test comment with stale warning' step inlined the
previous comment body via ${{ steps.get-comment.outputs.previous-results }}
into the action's `message:` input. As the test matrix grows, the
resulting argv can exceed ARG_MAX and the action fails with
'Argument list too long' — observed on a feature branch where the
matrix doubled.

Write the rendered stale-banner message to
$RUNNER_TEMP/stale-comment.md in the github-script step and pass the
path to sticky-pull-request-comment via its `path:` input instead.
This is robust to any future matrix size.

* Apply same fix to benchmarks.yml

Same ARG_MAX hazard exists in the benchmark workflow's stale-warning
step. Apply the identical `path:`-instead-of-`message:` refactor:

- The github-script step now writes the rendered stale-banner to
  $RUNNER_TEMP/stale-comment.md and exposes the path as a step output.
- The sticky-pull-request-comment 'Update existing benchmark comment
  with stale warning' step uses `path:` instead of inlining
  ${{ steps.get-comment.outputs.previous-results }} via `message:`.

The final 'Update PR comment with results' step in this workflow
already used `path: benchmark-summary.md`; only the stale-banner
update was inlined.

* Use `github.run_started_at` for stale-comment timestamps

The 'Started at:' label was sourced from `github.event.pull_request.updated_at`,
which is the PR metadata-update timestamp — not the workflow run start
time. That made the displayed timestamp:
- coupled to PR edits (label changes, description edits, etc.) rather
  than to the actual CI run, and
- stale on workflow re-runs (an empty re-run would still show the
  original PR-update time).

Switch all six occurrences across `tests.yml` and `benchmarks.yml` to
`github.run_started_at`, the canonical "this CI run started at"
timestamp.
2026-05-04 04:09:37 +00:00
Peter Wielander 8202663857 [workbench] Add TanStack Start workbench and tests (#1875) 2026-05-04 00:42:44 +00:00
Nathan Rajlich 92dc82608a Atomically dedupe duplicate step_created/wait_created events in world-local (#1877)
Concurrent invocations producing identical correlationIds (as the snapshot
runtime does by design across replays) previously both succeeded and
persisted duplicate events. step_created had no guard at all; wait_created
used a TOCTOU read-then-check that allowed both writers through under
concurrency. Both now claim a per-(runId, correlationId) constraint file
with O_CREAT|O_EXCL before writing, so the loser surfaces as
EntityConflictError — which the runtime's dedup catch path already
handles.
2026-05-04 00:21:53 +00:00
Peter Wielander 382cdf4f60 Split tarball hosting out of docs into its own project (#1893) 2026-05-04 09:10:54 +09:00
Nathan Rajlich 98ca60c318 Replace fixed-sleep hook waits with event-driven waitForHook helper (#1879)
Hook-related e2e tests (hookWorkflow, hookCleanupTestWorkflow,
hookDisposeTestWorkflow, hookWithSleepWorkflow, distributedAbortController)
previously slept a fixed 5 seconds before calling getHookByToken to wait
for the hook to be registered. On slower runtimes — notably the snapshot
runtime on Vercel where each workflow round-trip is several seconds longer
than replay — that fixed budget is too tight and the test fails with
HookNotFoundError. On faster runtimes it's unnecessarily slow.

Adds a waitForHook(token, { timeoutMs, intervalMs, runId }) helper that
polls until the hook resolves or the timeout (default 30s) expires, with
an optional runId filter for token-reuse tests where eventually-consistent
backends may briefly still report a stale hook. Each hook-wait site now
uses this helper. Non-hook fixed sleeps (workflow-progress polling for
sleepingWorkflow cancel tests, payload-processing waits in
hookWithSleepWorkflow) are unchanged.
2026-05-04 00:05:11 +00:00
Peter Wielander c1163eb146 [world-local] Reject run_failed on non-existent run (#1894) 2026-05-02 10:45:05 +00:00
Peter Wielander 2f52d14f38 [vitest] [world-local] Fix local-world data recovery isolation (#1895) 2026-05-02 19:30:40 +09:00
Nathan Rajlich cd50618d1f ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources (#1882)
* ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources

The e2e, benchmark, and docs-smoke CI jobs previously used the static
`VERCEL_AUTOMATION_BYPASS_SECRET` deployment-protection bypass token
to reach protected Vercel deployments. Switch them over to the new OIDC
Trusted Sources flow: the GitHub Actions runner mints a short-lived
OIDC token via `core.getIDToken()` and forwards it on requests in the
`x-vercel-trusted-oidc-idp-token` header.

Each workbench project (and `workflow-docs`) has been configured with a
matching trusted-source rule:
  aud=https://github.com/vercel, repository=vercel/workflow

The shared header helper now lives at `scripts/trusted-sources-headers.mjs`
and is imported by both the e2e/bench tests and the docs smoke script,
removing the previous duplication.

* rename to VERCEL_OIDC_TOKEN and wire through world-vercel

- Rename the env var from VERCEL_TRUSTED_OIDC_TOKEN to VERCEL_OIDC_TOKEN
  to match Vercel's convention (also read by @vercel/oidc's
  getVercelOidcToken()).
- In @workflow/world-vercel, replace the legacy
  VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS / x-vercel-protection-bypass
  flow with VERCEL_OIDC_TOKEN / x-vercel-trusted-oidc-idp-token. The
  trusted-source header is attached on every outbound workflow-server
  request (both proxied through api.vercel.com and direct).
- Drop the bypass header from the encryption-key and
  resolve-latest-deployment fetches: those go to api.vercel.com which
  is public.
- Drop VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS plumbing from tests.yml.
- Update the pending world-vercel changeset to describe the final
  trusted-sources flow.

* .

* .

* ci: add statuses:read permission for wait-for-vercel-project action

The action queries /commits/{sha}/status (Commit Statuses API) in addition
to the Deployments API, in order to extract the Vercel `dpl_...` ID. With
an explicit permissions block in place, GITHUB_TOKEN now needs
`statuses: read` or the action 403s when resolving the deployment ID.

Reported by Copilot review on #1882.

* ci(docs): log status code and body when waitForServer times out

Helps diagnose deployment-protection / OIDC-trusted-source bypass
failures (e.g. SSO redirects) on the workflow-docs preview.

* ci(docs): log OIDC token claims (aud, repository, etc.) for diagnostics

Helps determine whether the bypass is failing because of missing
trusted-source config, claim mismatch, or audience mismatch.

* ci(docs): add curl debug step to verify OIDC header reaches Vercel

* .

* ci: remove debug logging now that trusted-sources config is correct

The fetch-failure root cause was the trusted-sources rule format: the
labs workbench projects had been PATCHed with just `to.slugs` (no
`preset`), but Vercel's edge requires the dashboard-form-style
`to.preset: 'all-custom'` field plus `development` in the slug list to
match incoming requests. After re-PATCHing all projects with the
correct format, the bypass works end-to-end.

* ci(docs): debug — test trusted-sources bypass against docs and labs deployments

Trying repository_owner claim added to one labs project to see if that
fixes the bypass.

* ci(docs): revert curl debug step

The GitHub Actions OIDC trusted-sources bypass returns 401 on all tested
projects regardless of claim configuration (including workflow-docs which
was set up via the dashboard). This is not a per-project config issue.
Need to investigate with Vercel team before continuing.

* ci(docs): probe trusted-sources bypass and surface x-vercel-id

Adds a debug step that does two HEAD requests against the docs preview
deployment (with and without the OIDC trusted-sources header) and prints
the response status line plus `x-vercel-id` for each. The proxy-side
trusted-sources changes for GitHub Actions OIDC tokens are rolling out
gradually (~12+ hours), so the edge-node identifier in `x-vercel-id`
helps explain why a request might succeed or fail during the rollout
window.

Also includes `x-vercel-id` in the `waitForServer` timeout error so
post-mortem analysis of failing runs has the same edge-node info.

* ci(docs): drop trusted-sources curl probe — bypass works once proxy fix reaches the serving edge node

The probe served its purpose: confirmed the bypass is functional once
the request lands on a region that has the proxy-side trusted-sources
fix rolled out. The waitForServer error message still surfaces
x-vercel-id for any future rollout-window debugging.

* .

* world-vercel: log outbound OIDC token claims once per process

Adds a one-shot diagnostic that prints the non-sensitive claims of the
OIDC token (`iss`, `aud`, `owner_id`, `project_id`, `environment`,
`sub`, `scope`, `exp`) on the first request that uses bearer auth.

This is invaluable for debugging Vercel deployment-protection
trusted-source rule mismatches: a 401 from the edge tells you nothing
about why the rule didn't match, and the token's claims are the only
thing that determines that. The signature is never logged.

Gated to once per process — Vercel-issued tokens are process-stable for
the lambda's lifetime so further log lines would just be redundant
spam.

* world-vercel: route trusted-sources header through getVercelOidcToken()

The Authorization bearer correctly preferred config.token (a static
Vercel auth token from CLI / Actions runner) and fell back to
getVercelOidcToken() inside a Vercel function. But the trusted-sources
bypass header (x-vercel-trusted-oidc-idp-token) was being read directly
from process.env.VERCEL_OIDC_TOKEN inside getHeaders(). That env var is
the bake-time token, frozen at deployment-creation time — on a project
that has been redeployed after a settings change, it carries stale
claims (e.g. an iss from when the project was briefly in 'global' mode)
that no longer match the workflow-server's trusted-sources rule.

Move trusted-sources header attachment from getHeaders() (sync) to
getHttpConfig() (async) and source it from getVercelOidcToken(). That
function reads getContext().headers['x-vercel-oidc-token'] first — a
freshly minted per-request token that always reflects current project
settings — and only falls back to the env var when that header is
missing.

Bearer auth source remains config.token-first.

Also expand the diagnostic to log claims from BOTH the per-request OIDC
token AND the bake-time env var so the divergence is visible in logs
when debugging future trusted-source mismatches.

Removes the now-misleading getProtectionBypassHeader() helper (its
'read env var directly' semantics were exactly the bug).

* world-vercel: skip OIDC trusted-sources header on proxied path

The two outbound flows have different auth requirements:

  1. Proxied (usingProxy=true) — calls api.vercel.com/v1/workflow.
     Public endpoint, authenticated with a static Vercel auth token via
     config.token. The api-workflow proxy mints its own OIDC token
     before forwarding to workflow-server, so the trusted-sources
     bypass header on the SDK→proxy hop is meaningless. CLI, GitHub
     Actions, and other API-client callers take this path.

  2. Direct (usingProxy=false) — runs inside a Vercel deployment
     talking straight to workflow-server. workflow-server validates a
     Vercel OIDC bearer; Vercel's edge validates the trusted-sources
     header. Both must come from getVercelOidcToken() (the per-request
     fresh token), not process.env.VERCEL_OIDC_TOKEN (the bake-time
     token that can be stale after a project config change).

Previously getHttpConfig attached x-vercel-trusted-oidc-idp-token on
both paths whenever getVercelOidcToken() resolved. That accidentally
forwarded the GitHub Actions OIDC token (when wired into
VERCEL_OIDC_TOKEN by the test runner) onto every SDK→proxy request,
which is harmless but wrong-by-design — the proxy is public, doesn't
look at that header on its inbound side, and the GHA token isn't its
intended audience.

Bearer auth source rules:
  - Proxied: only config.token. (No fallback to OIDC; that auth
    pathway doesn't go through the proxy's auth checks.)
  - Direct: config.token (for tests / local dev), falling back to
    getVercelOidcToken() (for Vercel-runtime calls).

* world-vercel: throw if proxied path is hit without a Vercel auth token

The api-workflow proxy authenticates the caller with a regular Vercel
auth token (not OIDC), so reaching the proxied path with no
config.token is always wrong: the proxy will reject the request and
the SDK caller would see an opaque 401 with no actionable hint.

Throw at config-resolution time with a clear message that points to
the WORKFLOW_VERCEL_AUTH_TOKEN env var the SDK reads from. Adds tests
covering both the no-token-throws case and the with-token-attaches-
bearer-and-skips-trusted-sources case.

* test(e2e): include x-vercel-id in startWorkflowViaHttp error message

When the trusted-sources bypass returns 401, the error message now
surfaces the response's x-vercel-id header so we can identify which
edge node served the failure. Helps distinguish proxy-rollout
incompleteness from actual config errors during incremental
rollouts of edge-side changes.

* ci: mint GHA OIDC tokens on demand to survive 5-minute expiry

GitHub Actions OIDC tokens have a hard 5-minute lifetime that cannot be
extended (no API to ask for a longer TTL — exp is always iat + ~300s).
Pre-minting once at the start of the job and shipping the result down
to the test runner via env var means tests that run late in the suite
hit an expired token and 401 on /api/trigger-pages (and any other
trusted-sources protected endpoint).

Move minting into scripts/trusted-sources-headers.mjs:
  - getTrustedSourcesHeaders() is now async.
  - It calls the runner's ACTIONS_ID_TOKEN_REQUEST_URL endpoint directly
    (the env vars GHA exposes when permissions: id-token: write is on)
    and re-mints 60s before the cached token's exp.
  - Falls back to process.env.VERCEL_OIDC_TOKEN for non-GHA contexts
    (Vercel runtime, local dev).

Workflow files drop the now-redundant 'Mint OIDC token' step and the
VERCEL_OIDC_TOKEN env-var passthrough on the test step. The runner env
vars propagate to subsequent steps automatically.

Updates all 17 callers in e2e.test.ts / bench.bench.ts / utils.ts /
docs/scripts/check-docs-smoke.mjs to await the now-async call.

* address PR #1882 code review

- Drop `statuses: read` from the three workflow permission blocks (the
  wait-for-vercel-project action works without it on a public repo).
- Revert the `x-vercel-id` debug logging in `startWorkflowViaHttp`.
- Delete `packages/world-vercel/src/jwt-claims.ts` (debug-only helper).
- Drop the JWT claims diagnostic logging from `getHttpConfig`.
- Tighten the auth-flow comment in `getHttpConfig` and remove the
  historical 'no longer attaches' note from `getHeaders`/its test.
- Restore `.changeset/world-vercel-protection-bypass.md` (already
  shipped in a beta release per .changeset/pre.json).
- Trim the `.changeset/world-vercel-trusted-sources.md` description to
  one short paragraph.

* docs(AGENTS): document local VERCEL_OIDC_TOKEN via vercel env pull

Configured trustedSources.projects on all 11 workbench app projects so
each one accepts a Vercel-issued OIDC token from any of the others. A
developer running e2e locally can now do `vercel env pull` from any
workbench app's directory and use the resulting VERCEL_OIDC_TOKEN to
bypass Deployment Protection on any of the workbench preview/prod
deployments — no need to disable protection on the project just to run
the suite locally.
2026-05-02 19:21:52 +09:00
Pranay Prakash 072e86bbef Add additional tests for event consumer fixes for hook/sleep/step race conditions (#1528)
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-05-02 07:47:58 +00:00
Nathan Rajlich 74b13cd3ed Add first-class serialization for FatalError and RetryableError (#1513)
* Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE to FatalError and RetryableError

Add custom serialization methods to FatalError and RetryableError in
@workflow/errors, enabling the SWC plugin to discover and register them
through the standard class serialization pipeline. This preserves class
identity (instanceof), the fatal flag, and the retryAfter date when
these errors cross serialization boundaries.

- Add @workflow/serde dependency to @workflow/errors
- Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods to both classes
- Add unit tests verifying Instance-based round-trip serialization
- Add e2e workflow tests verifying class identity preservation end-to-end

* Address review feedback on FatalError/RetryableError serde

- FatalError: preserve cause property when present (Copilot feedback)
- RetryableError: preserve cause property when present
- RetryableError: serialize retryAfter as numeric timestamp for realm
  safety (the Date reducer uses instanceof global.Date which fails
  across VM realms; timestamps sidestep that issue)
- Replace e2e tests with step return value serialization (step throw
  path always reconstructs errors as FatalError, so those tests don't
  exercise the new serde code path)
- Add unit tests for cause preservation on both classes

* Bump fatal-retryable-error-serialization changeset to minor

Adding WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE hooks to FatalError
and RetryableError is a feature, not a bug fix.

* Switch FatalError/RetryableError to first-class serialization

Replace the WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods on
FatalError and RetryableError with dedicated reducers/revivers in the
common serialization module. The Instance/Class pipeline relies on the
SWC plugin discovering classes and registering them by classId, which
means values constructed in environments that don't run the plugin
(vitest e2e runner, ad-hoc Node scripts) can't be deserialized.
Treating FatalError/RetryableError as first-class serialization targets
makes them round-trip from any environment with no setup, matching the
behavior of TypeError, RangeError, etc. added in the previous commit.

- Drop @workflow/serde dependency on @workflow/errors
- Remove WORKFLOW_SERIALIZE/DESERIALIZE statics from FatalError/RetryableError
- Add FatalError/RetryableError reducers to serialization/reducers/common.ts
  with cached base-reducer factories for the subclasses that wrap the
  shared shape (RetryableError, AggregateError)
- Migrate unit tests off registerSerializationClass setup
- Extend the errorSubclassRoundTripWorkflow e2e test to cover FatalError
  and RetryableError, and drop the parallel errorFatalSerdeRoundTrip /
  errorRetryableSerdeRoundTrip tests

* Address review feedback on FatalError/RetryableError serde

- Soundness: split makeErrorSubclassReducer into a shared base helper
  (reduceErrorBase / reduceNamedErrorSubclassBase returning the
  BaseErrorPayload shape) plus a thin wrapper constrained to subclass
  keys whose serialized shape is exactly that base payload. The
  AggregateError and RetryableError reducers — which extend the base
  with extra fields — now consume reduceNamedErrorSubclassBase
  directly instead of calling makeErrorSubclassReducer with an
  unsound type cast. The compiler now rejects accidental misuse
  (SimpleErrorSubclassKey type guard).
- Realm safety: RetryableError reviver constructs retryAfter via
  new global.Date(...) to match the rest of the module and ensure
  the resulting Date passes instanceof global.Date checks in the
  target realm.
- Test strength: assert serialized payloads contain the literal
  devalue marker ["FatalError",N] / ["RetryableError",N] rather
  than the bare class name (which would also match a generic Error
  payload whose name happens to be "FatalError"). Also assert the
  generic ["Error",N] marker is absent.

* Match Error subclass reducers by `value.name`, not constructor name

Bundlers like Turbopack compile `export class FatalError extends Error
{...}` into a registration call like `e.s(["FatalError", 0, class
extends Error {...}])` — passing an anonymous class expression as a
function argument. The resulting constructor function has `name === ''`,
which broke the previous `value.constructor?.name === subclassName`
match: an instance of the bundled FatalError class no longer matched the
dedicated FatalError reducer and instead fell through to the generic
`Error` reducer, losing class identity across the workflow boundary.

This was caught by the local-prod CI matrix, where each Next.js route
gets its own bundled chunk: a real `new FatalError('fatal!')` returned
from a workflow was serialized as a plain Error and revived without
`instanceof FatalError` holding on the consumer side.

Switch the match in `reduceNamedErrorSubclassBase` to `value.name`,
which:
- works for built-in subclasses (TypeError/RangeError/… all set
  `name` automatically and aren't bundled, so behavior is unchanged
  in practice).
- works for FatalError/RetryableError, whose constructors set
  `this.name` explicitly — robust across realms AND bundlers.
- is consistent with how `FatalError.is()` / `RetryableError.is()`
  already identify their values.

Two existing cross-VM Error tests (added in #1164) used `name = 'FatalError'`
on a plain Error to stand in for any cross-realm error — which now hits
the dedicated FatalError reducer (returning a host-realm FatalError)
instead of the generic Error reducer (which constructs a VM-realm Error).
Renamed the stand-in to `'CustomError'` so they continue to exercise the
intended path.
2026-05-01 20:36:08 +00:00
Nathan Rajlich d5517a1814 docs: note WORKFLOW_PUBLIC_MANIFEST=1 requirement for local e2e dev server (#1890) 2026-05-01 12:37:41 -07:00
Nathan Rajlich e7ea0684f4 Add first-class serialization for built-in Error subclasses (#1511)
* Add first-class serialization for built-in Error subclasses

Replace the generic Error reducer/reviver with specific handlers for
TypeError, RangeError, SyntaxError, URIError, ReferenceError, EvalError,
and AggregateError. Preserve the cause property on all Error types,
only serializing it when present to maintain absence vs presence
semantics.

* Refactor Error subclass reducers/revivers with helper functions

Extract repetitive reducer/reviver logic into makeErrorSubclassReducer
and makeErrorSubclassReviver helpers. Reduces ~120 lines of duplicated
code to ~50 while preserving identical behavior. AggregateError remains
as a wrapping extension since it needs to preserve the errors array.

* Bump error-subclass-serialization changeset to minor

Adding new serialization support for built-in Error subclasses is a
feature, not a bug fix.
2026-05-01 18:39:01 +00:00
Nathan Rajlich 78301695ea fix(builders): avoid false-positive node-module errors for step-only usage in shared modules (#1821)
The workflow-node-module-error plugin records violations in onResolve, which
runs before esbuild's tree-shaker. A shared module with both a workflow-safe
export and a step-only export that imports node:* would be flagged even when
the workflow never reached the step-only export.

Mark node:/bun: imports as sideEffects: false so esbuild can tree-shake them
when unused in surviving code, enable metafile, and filter recorded violations
in onEnd against outputs[].imports so only builtins that actually survive in
the emitted bundle are reported.

Fixes #1817
2026-05-01 17:57:56 +00:00
Nathan Rajlich 9f3516ec28 Refactor: Extract serialization into modular architecture and wire into existing pipeline (#1299)
* Add serialization module foundation: types, codec interface, format prefix

Start of the serialization refactor (separate from snapshot-runtime).

New files:
- serialization/types.ts — SerializationFormat enum, SerializableSpecial
  interface, Reducers/Revivers types
- serialization/codec.ts — Codec interface with formatPrefix, serialize,
  deserialize, and optional deserializeLegacy
- serialization/format.ts — Format prefix encode/decode/peek, moved from
  the monolithic serialization.ts

The Codec interface enables future alternative formats (CBOR, JSON) while
keeping the devalue implementation as the current default.

* Add reducers, devalue codec, encryption, and mode-specific modules

Serialization refactor Phase 1: create the new module structure alongside
the existing monolithic serialization.ts (which continues to work).

New files:
- serialization/reducers/common.ts — Date, Error, Map, Set, URL, BigInt,
  typed arrays, Headers, Request, Response, RegExp, URLSearchParams
- serialization/reducers/class.ts — Class/Instance with WORKFLOW_SERIALIZE/
  DESERIALIZE support
- serialization/reducers/step-function.ts — StepFunction with closure vars
- serialization/codec-devalue.ts — devalue Codec implementation
- serialization/encryption.ts — composable encrypt/decrypt layer
- serialization/workflow.ts — synchronous, no encryption, for VM use
- serialization/step.ts — async with encryption, for step handler
- serialization/client.ts — async with encryption, for start() API
- serialization/index.ts — re-exports all public API
- serialization/serialization.test.ts — 25 focused tests

All modes compose their reducer/reviver sets from the shared building blocks.
Cross-mode compatibility verified: data serialized in any mode can be
deserialized in any other mode (for common types).

Existing 108 serialization tests continue to pass unchanged.

* Add sub-path exports for workflow serialization module

- Add ./serialization/workflow export to @workflow/core package.json
- Add ./internal/serialization re-export to workflow meta-package
- The workflow bundle can now import serialize/deserialize via:
  import { serialize, deserialize } from 'workflow/internal/serialization'

Full test suite passes: 493 tests across 22 files (including 25 new
serialization module tests).

* Address code review feedback

1. Fix reducer composition order: Class/Instance reducers now come BEFORE
   common reducers in all three modes (workflow, step, client). This ensures
   custom Error subclasses with WORKFLOW_SERIALIZE are handled by the
   Instance reducer before the generic Error reducer (devalue uses
   first-match-wins semantics).

2. Fix encryption decrypt() to fail fast when encrypted data is encountered
   without a decryption key, instead of silently returning encrypted bytes
   that would fail later with an unhelpful format error.

3. Remove Request/Response from common reducers — they don't have matching
   common revivers, so including them caused asymmetric behavior (serialize
   as Request, deserialize as plain object). Request/Response handling
   belongs in mode-specific modules that can provide proper revivers.

4. Document Node.js dependency in the workflow serialization re-export.
   The current implementation uses node:util and Buffer. For the QuickJS
   VM (snapshot runtime), these will need polyfills — tracked separately.

* Move reducer/reviver composition into the devalue codec

The Codec interface now takes a SerializationMode ('workflow', 'step',
'client') instead of raw reducers/revivers. The reducer/reviver
composition is internal to the devalue codec implementation.

This is the right abstraction because reducers/revivers are devalue-
specific concepts. A future CBOR codec would handle Date, typed arrays,
Map, Set natively via the CBOR type system — it wouldn't use reducers
at all. A JSON codec would only support standard JSON types.

The mode-specific modules (workflow.ts, step.ts, client.ts) are now
simpler — they just pass the mode string to the codec.

* Replace SerializationFormatType enum with open-ended FormatPrefix type

The format prefix is now a branded string type validated by
isFormatPrefix() — any 4-character [a-z0-9] string is valid.
This removes the hard-coded enum of known formats, making the system
truly open for extension:

  type FormatPrefix = string & { __brand: 'FormatPrefix' };
  function isFormatPrefix(value: string): value is FormatPrefix;

The SerializationFormat object still provides well-known constants
('devl', 'encr') but they're now just typed constants, not an
exhaustive enum.

peekFormatPrefix() and decodeFormatPrefix() use isFormatPrefix() for
validation instead of checking against a known list. Unknown but valid
prefixes (e.g. 'cbor', 'json', 'v2b1') are accepted — the caller
decides whether they can handle the format.

6 new isFormatPrefix tests covering: valid strings, too short, too long,
uppercase, special characters. 1 new test for unknown-but-valid prefixes.

* Wire modular serialization modules into serialization.ts, add 138 unit tests

Replace duplicate format prefix, reducer/reviver, and encryption helper
code in the monolithic serialization.ts with imports from the modular
serialization/ directory. This completes the refactoring started in the
earlier additive-only commits.

Key changes:
- serialization.ts now imports types, format prefix, common/class/step-function
  reducers and revivers, and encryption helpers from ./serialization/ modules
- Removed ~450 lines of duplicate code from serialization.ts
- Made encryption error messages consistent between old and new modules
- Added 138 comprehensive unit tests covering types, format prefix,
  encryption, codec, all three reducer modules, all three mode modules,
  cross-mode compatibility, and edge cases
- Updated one existing test assertion for new error message wording

* Address code review feedback

- encryption.ts: throw WorkflowRuntimeError instead of plain Error in
  decrypt() to preserve the error contract from legacy maybeDecrypt()
- format.ts: document that open-ended prefix validation ([a-z0-9]{4})
  is intentional for forward compatibility — callers check support
- errors.ts: extract duplicated formatSerializationError into shared
  utility, remove 4 copies from workflow.ts, step.ts, client.ts
- codec-devalue.ts: document that globalThis default is a known
  limitation; legacy dehydrate/hydrate path still supports custom global

* Fix codec-devalue.ts comment: clarify modular modules are not used in current runtime

The globalThis default is not a limitation for the current runtime —
all serialization goes through dehydrate*/hydrate* in serialization.ts
which passes the correct global. The modular modules are infrastructure
for the future snapshot runtime where serialization runs inside the VM.

* Wire dehydrate/hydrate functions through modular serialize/deserialize

The dehydrate*/hydrate* functions in serialization.ts now delegate to
the modular mode modules (workflowModule, stepModule, clientModule)
instead of directly calling devalue stringify/parse/unflatten.

Key changes:
- Extended Codec interface with CodecOptions (global, extraReducers,
  extraRevivers) so the codec can receive VM globals and mode-specific
  stream/Request/Response handlers
- devalueCodec threads global through to all reducer/reviver factories
  so instanceof checks work across VM boundaries
- Mode modules (workflow.ts, step.ts, client.ts) accept CodecOptions
  and pass them through to the codec
- dehydrate*/hydrate* functions now call module serialize/deserialize
  with stream and Request/Response reducers/revivers passed as extras
- v1Compat path remains inline (pre-codec, uses stringify + revive)
- Error context strings preserved via try/catch re-wrapping

* Bump changeset from patch to minor for serialization refactor

Return types of public get*Reducers/get*Revivers functions narrowed
from Reducers/Revivers to Partial<Reducers>/Partial<Revivers>, which
is a TypeScript-level breaking change. Also adds new sub-path exports
(@workflow/core/serialization/workflow, workflow/internal/serialization)
which is additive. Minor bump is the appropriate semver for both.

* Remove unused workflow/internal/serialization re-export and @workflow/core/serialization/workflow sub-path

Both exports had zero consumers in the repo. The workflow/internal/serialization
export was previously removed on main in #1082 for the same reason. The
modular workflow.serialize/deserialize is still reachable via
@workflow/core/serialization when needed. These exports can be reintroduced
by the snapshot runtime branch if/when it actually needs them.

Also updates the changeset to drop the 'new sub-path exports' bullet.

* Downgrade changeset from minor to patch

After auditing actual consumers of the narrowed return types
(getExternalReducers/getWorkflowReducers/getExternalRevivers/getWorkflowRevivers
now return Partial<Reducers>/Partial<Revivers>), no in-repo or external
consumer indexes specific keys on the returned object in a way that would
break. The only internal caller that did (runtime/run.ts) was updated in
this same PR. The narrowing is type-safer but effectively invisible at
runtime and for idiomatic callers that spread or forward the object.
Since the refactor is internally restructuring only, patch is the
appropriate semver bump.

* Trim serialization-refactor changeset

* Dedup formatSerializationError: import from serialization/errors.ts

The legacy serialization.ts had its own inlined copy of
formatSerializationError. Now that the helper is exported from
serialization/errors.ts (already consumed by workflow.ts/step.ts/client.ts),
import it here too to keep the single source of truth.
2026-05-01 09:54:08 +00:00
Nathan Rajlich e0ec429bb3 fix(builders): bundle aliased project-local helpers instead of externalizing (#1885)
Externalizing aliased helpers was unsafe: their source on disk could
contain further alias imports that Node's ESM loader doesn't know about,
causing 'Package subpath ... is not defined by "exports"' /
ERR_MODULE_NOT_FOUND at runtime.

Now bundle inline so all alias resolution happens at build time.
2026-05-01 02:20:31 -07:00
github-actions[bot] eb8562ddda Version Packages (beta) (#1884)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@5.0.0-beta.4
2026-04-30 14:25:43 -07:00
Nathan Rajlich 640a0505b8 Add missing changeset for #1872 (trace viewer polish) (#1883) 2026-04-30 14:22:01 -07:00
Mitul Shah aeb495bccc [trace viewer] Polish items (#1872)
* wip

* context

* cleanup

* wip

* wip

* Update trace-viewer.tsx

* adjust hovers

* time marker

* tweak

* Update event-list.tsx

* header

* Update utils.ts

* search

* Update timeline.tsx

* ship

* Update timeline.tsx

* Update event-list.tsx

* Create icons.tsx

* Update trace-viewer.tsx

* Update utils.ts

* new trace viewrr

* Update workflow-trace-view.tsx

* Fix: Runtime crash when `trace` is `undefined`: `NewTraceViewerComponent` receives `undefined` (cast as `Trace`) and immediately accesses `trace.spans`, causing "Cannot read properties of undefined".


This commit fixes the issue reported at packages/web-shared/src/components/trace-viewer-new.tsx:28

**Bug Analysis:**

In `trace-viewer-new.tsx`, the `buildTrace` function returns `undefined` when `!run?.runId`. The result is stored in `trace` which has type `TraceWithMeta | undefined`. However, on line 28, `trace` is passed directly to `NewTraceViewerComponent` with a type assertion `trace as Trace`, which silences the TypeScript error but does not prevent the runtime crash.

Inside `NewTraceViewerComponent` (in `new-trace-viewer/trace-viewer.tsx` line 98), the component immediately accesses `trace.spans`:

```tsx
<ActiveSpanProvider spans={trace.spans}>
```

When `trace` is `undefined`, this produces: `TypeError: Cannot read properties of undefined (reading 'spans')`.

This happens whenever the component renders before `run.runId` is available — a normal scenario during initial loading.

The old `WorkflowTraceViewer` component (in `workflow-trace-view.tsx` line 953) correctly handles this with a `if (!trace)` guard that renders a loading skeleton. The new component lacks this guard.

**Fix:**

Added a null guard in `trace-viewer-new.tsx` that checks `if (!trace)` before rendering `NewTraceViewerComponent`. When trace is undefined, a simple loading placeholder is rendered instead. This prevents the crash and follows the same pattern as the existing `WorkflowTraceViewer`. The `as Trace` cast on line 28 is now safe because `trace` is guaranteed to be defined after the guard.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: mitul-s <mitulxshah@gmail.com>

* fixed edge view

* Update timeline.tsx

* ship it

* cleanup

* Update trace-viewer.tsx

* wip

* wip

* Add detail pane to the new trace viewer + cleanup  (#1714)

* detail pane

* cleanup

* cleanup

* cleanu

* fix

* Update workflow-trace-view.tsx

* cleanup

* Update entity-detail-panel.tsx

* move sidebar provider into export

* dead code

* cleaning up more

* remove decorative indenting & output loader

* fix bars

* Update inspector-theme.ts

* cleanups

* Update copyable-data-block.tsx

* cleanup

* chonky

* Update timeline.tsx

* bug fxi

* marker lines

* wip

* changes

* Update event-list.tsx

* rounded

* Update event-list.tsx

* middle truncate component

* cleanup

* height updatres

* colour fixes

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-04-30 16:50:24 -04:00
github-actions[bot] 8f9ca384ec Version Packages (beta) (#1868) workflow@5.0.0-beta.3 2026-04-30 13:34:08 -07:00
Mitul Shah 03dfab35e7 Updated Workflow Trace viewer (#1454) 2026-04-30 13:46:54 -04:00
Nathan Rajlich 3ad8ee7e33 fix(world-local): prevent path traversal via request-supplied IDs (#1829)
* fix(world-local): prevent path traversal via request-supplied IDs

Request-supplied identifiers (runId, eventId, stepId, hookId, correlationId,
stream names, and tags) flowed directly into path.join() calls, allowing a
client to send values like '../../../package' and cause the backend to read
or write files outside the workflow data directory.

Add a centralized validator (assertSafeEntityId) that rejects IDs which are
empty, start with '.', or contain path separators or NUL bytes. Apply it at
each storage-layer entry point that composes IDs into filesystem paths:
fs.taggedPath / readJSONWithFallback / paginatedFileSystemQuery, the runs /
steps / events / hooks storage methods, and the streamer.

* address review feedback

- UnsafeEntityIdError now extends WorkflowWorldError for consistency with
  other storage-layer errors and the platform error-to-HTTP mapping.
- Add resolveWithinBase(basedir, ...segments) containment helper and
  apply it at every taggedPath / readJSONWithFallback / .locks path
  construction site in events-storage and legacy, so a forgotten
  assertSafeEntityId at a future call site can't silently regress.
- Truncate attacker-controlled values in the error message.
- Drop unused assertSafeEntityIds helper and the unreachable typeof
  check under the TS signature.
- Fix docstrings on assertSafeEntityId / taggedPath JSDoc example /
  filePrefix validation comment to match what the code actually does.
- handleLegacyEvent now re-asserts runId locally so the invariant is
  documented at the call site instead of implicitly inherited from
  events.create.

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2026-04-30 09:27:46 +00:00
Nathan Rajlich fe382021b9 docs: document full Vercel E2E env vars in AGENTS.md (#1819)
* docs: update AGENTS.md with complete Vercel E2E env vars

* docs: remove WORKFLOW_PUBLIC_MANIFEST from local E2E instructions
2026-04-30 08:16:45 +00:00
Nathan Rajlich 59ec3983b9 docs: tighten changeset description guidance in AGENTS.md (#1833)
* docs: tighten changeset description guidance in AGENTS.md

Specify that changeset descriptions should be one sentence (two at
most), and remove the redundant BREAKING CHANGE marker guidance since
breaking changes are already communicated via the major semver bump.

* docs: address Copilot review feedback

- Align command reference to `pnpm changeset add` (matching line 188)
- Drop the "see existing changesets for examples" parenthetical since
  existing changesets predate the new sentence-limit guidance
2026-04-30 07:46:45 +00:00
Nathan Rajlich 2d66c75ad0 ci: fail fast when Next.js dev server is wedged on Windows (#1871)
A recurring Turbopack-on-Windows bug causes the dev server to enter a
'MODULE_UNPARSABLE' state during HMR in dev.test.ts, after which every
request returns 500. The remaining e2e suite then polls stuck workflows
for 60s each, burning the full 30-minute job window before getting
cancelled (~50% of recent main runs).

Bail out of the Windows e2e job as soon as dev.test.ts fails, and
health-check the dev server before kicking off test:e2e so any other
silent breakage is surfaced quickly instead of via a 30-minute timeout.
2026-04-30 00:42:09 -07:00
Nathan Rajlich 906f7c1213 refactor(next): remove step file copy mechanism from deferred builder (#1796)
* refactor(next): remove step file copy mechanism from deferred builder

Step sources are now imported directly into the generated `step/route.js`
using the same `getImportPath`-based logic already used for serde files.
This is possible because the SWC plugin's client mode was merged into
step mode (#1686), so the workflow loader always runs in step mode and
transforms every file it sees, including those from packages.

Removed:
- `__workflow_step_files__/` per-file copies with hashed names, metadata
  comments, inline source maps, and bare-specifier rewriting via
  `enhanced-resolve`
- `createResponseBuiltinsStepFile`; builtins are now imported via
  `require.resolve('workflow/internal/builtins')` at build time
- `step-copy-utils.ts` and all copy-specific branches in `loader.ts`
- `enhanced-resolve` dependency

The deferred builder still removes the legacy `__workflow_step_files__/`
directory on boot so upgrades leave no stale artifacts behind.

Dev tests that inspected copied step file contents now inspect the
manifest.json entries (which list every discovered step keyed by source
path).

* refactor(next): scrub historical phrasing from code comments

* test: update next step stack expectations

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2026-04-29 21:23:08 +00:00
Karthik Kalyan 0ec805a999 fix(e2e): increase utf8StreamWorkflow test timeout to 120s (#1869)
Matches the convention used by readableStreamWorkflow and webhookWorkflow.
The 60s budget is too tight when a single Vercel Queue delivery retry can
take 30-60s, causing intermittent flakiness on the nitro workbench.

Made-with: Cursor
2026-04-29 12:27:57 -07:00
Nathan Rajlich 787bb15df0 ci: use GitHub API commit mode for changesets action (#1867)
The repo enforces "Commits must have verified signatures" via an
org/enterprise-level ruleset, which blocks unsigned commits pushed via
the Git CLI by GITHUB_TOKEN. Switching the changesets action to
commitMode: github-api makes commits GPG-signed by GitHub.
2026-04-29 10:34:24 -07:00
Nathan Rajlich 89738e5236 ci: stop using Release App token in release workflows (#1866)
The Release App has been temporarily removed. Switch the Release and
Backport workflows to use the default GITHUB_TOKEN, and disable the
cross-repo Front dispatch workflow until the App is restored.

Also add a workflow_dispatch trigger to release.yml so the Version
Packages PR can be created/updated manually (since pushes made by
GITHUB_TOKEN do not trigger downstream workflow runs).
2026-04-29 17:25:36 +00:00
Karthik Kalyan 9ea125427f Decode UTF-8 stream chunks (#1852)
* Decode typed array stream chunks

* Render decoded stream bytes with raw view

* Render decoded bytes in data inspector

* Use generic byte inspector for streams

* review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests

- Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay
  exports; keep only the formatArrayBufferViewForDisplay path actually used
  by DataInspector.
- Replace broken role=tablist/role=tab on the Decoded/Bytes switcher
  with aria-pressed toggle-button semantics.
- Export collapseRefs/isBytesDisplay and add regression tests covering
  typed-array detection (top-level, nested in object/array/Map/Set,
  DataView exclusion).

* Replace eval with JSON.parse in serialization revive helper (#1848)

* Replace eval with JSON.parse in serialization revive helper

devalue.stringify() always produces valid JSON — special values
(undefined, NaN, Infinity, -0) are encoded as negative integer
sentinels. JSON.parse yields the same flattened array form that
unflatten() expects, without the eval anti-pattern (VULN-918).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop redundant workflow package from changeset

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* Add e2e test for UTF-8 parseable stream chunks

Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended,
CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and
asserts each chunk round-trips through TextDecoder({ fatal: true }).
Exercises the same decode path the web inspector relies on for
typed-array stream values.

Made-with: Cursor

---------

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 09:22:05 -07:00
Nathan Rajlich cbecbaa5fe feat(nitro): forward externals.external to workflow builder (#1844)
Nitro users can declare packages as externals via
`externals: { external: ['fsevents'] }` in `nitro.config.ts`. Previously
the workflow SDK's step/workflow/webhook bundles were built in isolation
and ignored that config, forcing users to either patch the Nitro adapter
or ship a custom builder (as in the 250MB bundle-size workaround gist).

Forward string entries from `nitro.options.externals.external` to the
builder's esbuild `external` option. Only string entries are forwarded
— RegExp and function matchers aren't supported by esbuild's
`external` API, so those are skipped.
2026-04-28 23:54:18 +00:00
Karthik Kalyan 64c66d211e docs(cookbook): rewrite core recipes, dedupe, and reorder sidebar (#1850)
* fix(docs): highlight active top-bar nav item and drop duplicate keys

Match the current pathname (accounting for the hidden default locale)
and forward an `active` prop to the Radix NavigationMenuLink so the
selected top-bar item stays highlighted. Also remove module-level
`key` props from the intro Loading/Success/Error indicators — they
fired React's duplicate-key warning whenever two lines shared a state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* update saga cookbook docs with adaptation guidance and streaming example

Made-with: Cursor

* docs(cookbook): rewrite core recipes, dedupe, reorder sidebar

Pass over the cookbook focused on correctness, concrete examples, and
tighter information architecture.

Rewrites:
- Saga / Batching / Scheduling: concrete use cases (order fulfillment,
  CSV import, cancellable drip campaign); added "how it works" +
  "adapting" sections.
- Durable Agent / Human-in-the-Loop / Stop Workflow: fixed broken
  patterns (tool invocations don't stream before return; prepareStep
  flag checks never fire); canonical single example per page.
- AI SDK integration: refocused from DurableAgent showcase to
  streamText(); added multi-turn session pattern with durable stream
  slicing, ai-sdk.dev deep links, and a pitfalls section.
- Sandbox integration: rewrote around the real @vercel/sandbox SDK;
  flagship pattern is one workflow run = one persistent sandbox
  session that outlives the 5-hour VM cap via snapshot rotation.
- Chat SDK integration: full rewrite — previous page documented AI
  SDK's useChat, not chat-sdk.dev. New page covers durable chat
  sessions, Mermaid flowchart, and cross-SDK serialization.
- Publishing Libraries: swapped ASCII tree for Fumadocs <Files>
  (font-independent); corrected isomorphic guidance ("use workflow" /
  "use step" are safe no-ops when SWC plugin doesn't process them);
  added Mux AI and World ID adopter examples with canonical links.

Deletions (redundant or flawed):
- Fan-Out, Conditional Routing (covered by Batching / not a pattern).
- Tool Streaming, Tool Orchestration (covered by foundations/streaming
  and ai/defining-tools).
- Durable Objects (hook-in-loop bug; framing doesn't hold up).
- Custom Serialization, Isomorphic Packages (merged into
  foundations/serialization and publishing-libraries respectively).

Sidebar restructure:
- Reordered: Overview -> Agent Patterns -> Common Patterns ->
  Integrations -> Advanced (Agent Patterns leads since it drives most
  new-visitor traffic).
- Moved Child Workflows + Distributed Abort Controller from Common
  Patterns to Advanced (composition / coordination concerns, not
  common reliability recipes); permanent redirects added in
  next.config.ts.

Made-with: Cursor

* docs(cookbook): rename Agent Stop Signal → Agent Cancellation, migrate common patterns

The agent cancellation page now covers two named patterns — Hard
Cancellation via getRun(runId).cancel() and Stop Signal via a hook +
Promise.race — so the prior "Agent Stop Signal" title biased toward only
one of them. Rename to the broader noun phrase and update the intro,
description, and section heading to frame both approaches equally.

Also migrate all patterns from foundations/common-patterns into dedicated
cookbook recipes (timeouts, sequential-and-parallel, workflow-composition)
and remove the foundations page. Permanent redirects added for the prior
URLs (stop-workflow, agent-stop-signal, foundations/common-patterns) so
no inbound link breaks.

Made-with: Cursor

* docs(cookbook): fix CI typecheck failures on three snippets

- publishing-libraries.mdx: the second processPayment snippet uses
  getWorkflowRunId() defined in the prior block. Each block is type-checked
  independently, so add @skip-typecheck (matches the existing pattern in
  this file).
- ai-sdk.mdx: the "snapshot tailIndex first" pitfall is a fragment
  referencing probe / turnHook / run / text from the surrounding multi-turn
  pattern. Add @skip-typecheck.
- chat-sdk.mdx: the [platform] webhook route used Next.js 15 typegen
  (RouteContext<"/api/webhooks/[platform]">), which isn't part of the
  docs-typecheck environment. Switch to the manually-typed
  { params }: { params: Promise<{ platform: string }> } form — still fully
  supported by Next.js 15+ and doesn't depend on .next/types.

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:52:54 -07:00
Pranay Prakash 7d07fab692 Replace eval with JSON.parse in serialization revive helper (#1848)
* Replace eval with JSON.parse in serialization revive helper

devalue.stringify() always produces valid JSON — special values
(undefined, NaN, Infinity, -0) are encoded as negative integer
sentinels. JSON.parse yields the same flattened array form that
unflatten() expects, without the eval anti-pattern (VULN-918).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop redundant workflow package from changeset

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:58:24 +00:00
JJ Kasper baba580794 Write Next diagnostics to dist dir (#1857)
* Write Next diagnostics to dist dir

* Simplify Next dist dir config access
2026-04-28 22:02:03 +00:00
JJ Kasper a38f140ce3 Make TypeScript peer dependency optional (#1830)
* Make TypeScript peer dependency optional

* Handle missing TypeScript more gracefully

* Fix TypeScript plugin build after optional peer change
2026-04-28 21:48:13 +00:00
Nathan Rajlich 3a08eaa0a1 ci: refactor wait-for-vercel-project to use GitHub Deployments API (#1861)
* ci: refactor wait-for-vercel-project to use GitHub Deployments API

Replaces the Vercel SDK / Vercel API token-based implementation with one
that resolves the deployment URL via the GitHub Deployments API:

- Find the GitHub Deployment for (target SHA, environment) where
  environment matches the Vercel-app-created "Preview \u2013 <slug>" or
  "Production \u2013 <slug>" naming pattern.
- Wait for the latest deployment status to be `success` (or `inactive`
  when Vercel skips a duplicate build, in which case its environment_url
  still points at the live deployment).
- Probe the URL to confirm the edge can route to it (any non-5xx
  response counts as live, including 401/403 from Deployment Protection
  and 404/405 from the app). Manual redirect handling treats redirects
  to vercel.com as "still building".
- Resolve the dpl_xxx deployment ID from the matching commit status
  (Vercel posts `Vercel \u2013 <slug>` statuses where target_url's last
  path segment is the inspector ID == deployment ID without the prefix).

Inputs change: project-slug + bypass-secret + github-token (with
GITHUB_TOKEN default) replace team-id + project-id + vercel-token.

Removes the @vercel/sdk dependency, shrinking the bundled dist from
5.4MB to 829KB. The VERCEL_DOCS_TOKEN secret is no longer referenced
anywhere in the repo and can be deleted from GH after this lands.

* ci(wait-for-vercel-project): drop URL probe and bypass-secret input

The GitHub Deployment status transitions to `success` only after the
Vercel app finishes building and routing is live, so an extra HTTP
liveness probe of the deployment URL was redundant. Removing it lets
us also drop the bypass-secret input \u2014 protected deployments don't
need a workaround anymore because we never make the request.

Reduces the action surface area and eliminates a runtime fetch.

* ci(wait-for-vercel-project): address PR review

- Fail loudly when the dpl_xxx deployment ID can't be resolved instead
  of returning an empty string. Consumers wire this into
  VERCEL_DEPLOYMENT_ID, which world-target uses to pick between the
  vercel and local worlds (packages/utils/src/world-target.ts), so an
  empty value would silently flip execution mode.
- Pass the GitHub App token to wait-for-vercel-project in the dispatch
  release workflow. The job sets `permissions: contents: read`, which
  blocks the default GITHUB_TOKEN from reading the Deployments API.
  The App token (already generated for workflow,front) has the
  necessary scopes.
2026-04-28 20:20:47 +00:00
Nathan Rajlich d6f7eb620b test: enable step source-map assertions for vite local dev (#1862)
* test: enable step source-map assertions for vite local dev

Vite ^7.3.2 (bumped in #1827) preserves step bundle source maps in
dev mode, so stack traces now contain original file paths. Update
hasStepSourceMaps() so vite returns true in local dev and stays false
only in local prod, fixing the consistently failing 'basic step error'
and 'cross-file step error' e2e tests.

* chore: drop body from empty changeset

* address review: drop redundant vite local-prod guard

The default `!DEV_TEST_CONFIG` fall-through already returns false for
vite local prod, so the vite-specific guard is dead code. Just remove
the vite block entirely now that vite local dev matches the default
'has source maps' behavior.
2026-04-28 19:26:09 +00:00
Nathan Rajlich 28dc089eef ci: fix VERCEL_WORKFLOW_SERVER_* env var ternary on main (#1859)
* ci: fix VERCEL_WORKFLOW_SERVER_* ternary so main actually unsets them

In GitHub Actions expressions, '' is falsy, so the original
`cond && '' || secrets.X` pattern always fell through to the secret
regardless of branch. The result was that pushes to main were sending
preview workflow-server values to production, causing 'invalid_url'
errors on `x-vercel-workflow-api-url` across all e2e jobs.

Flip the condition so the secret sits in the truthy branch and ||
correctly selects '' on main.

* ci: skip pnpm cache in matrix-generation jobs

The Get Test Matrix and Get Community Worlds Matrix jobs only run a
small Node script to emit a JSON matrix; they never run `pnpm install`.
With `cache: 'pnpm'` set on actions/setup-node, the post-job cache save
step fails with 'Path Validation Error' because the pnpm store path was
never created, marking the whole job as failed.

Add a cache-pnpm input to setup-workflow-dev (default true) and opt out
in the two matrix-generation jobs.
2026-04-28 10:26:04 -07:00
JJ Kasper 393ffd86fc Lock next@canary test version (#1860) 2026-04-28 10:25:34 -07:00
Karthik Kalyan 56ba32feb8 Bump vite (#1827) 2026-04-22 14:05:38 -07:00
Pranay Prakash e2ef3568a5 CI script improvements (#1826)
* CI script improvements

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* address codex feedback: harden remaining workflows

- add allowlist regex in prepare-workbench-path to block path traversal
- move matrix/input values to env vars across e2e-vercel-prod,
  benchmarks (local/postgres/vercel), and the reusable community-world
  workflows
- validate app-name/world-id/world-package inputs in the reusable
  community-world workflows
- pipe getCommunityWorldsMatrix script output through jq -c to prevent
  \$GITHUB_OUTPUT injection

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 23:00:27 -07:00
Nathan Rajlich 354840e93b feat(world-vercel): support new env vars for Vercel Deployment Protection (#1824)
* feat(world-vercel): support WORKFLOW_VERCEL_PROTECTION_BYPASS env var

Allows sending a Vercel Deployment Protection bypass secret via the
`x-vercel-protection-bypass` header on all outbound requests made by
the Vercel world, enabling use against protected deployments (e.g.
previews, or workflow-server once protection is enabled).

* feat(world-vercel): support VERCEL_WORKFLOW_SERVER_URL env var

Replace hard-coded WORKFLOW_SERVER_URL_OVERRIDE constant with a function
that reads from the VERCEL_WORKFLOW_SERVER_URL env var. Allows configuring
the workflow-server URL per-deployment (e.g. workbench Preview envs
pointing to a branch deployment) without editing source.

* fix(world-vercel): preserve inline WORKFLOW_SERVER_URL_OVERRIDE const

Keep the inline const as an empty-string literal so external CI rewrite
tooling continues to work unmodified; the env var is a fallback when the
inline value is empty.

* refactor(world-vercel): rename to VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS

Align env var naming with VERCEL_WORKFLOW_SERVER_URL.

* ci: expose workflow-server protection bypass env vars to e2e-vercel-prod

Set VERCEL_WORKFLOW_SERVER_URL and VERCEL_WORKFLOW_SERVER_PROTECTION_BYPASS
on PR runs so e2e tests hit the protected workflow-server preview; leave
unset on main so production runs use the public default URL.

* refactor(world-vercel): address PR review comments

- Consolidate bypass header logic in getHeaders() to reuse
  getProtectionBypassHeader() instead of duplicating env lookup.
- Use consistent 'Authorization' casing in direct fetch() calls.
- Add unit tests for getProtectionBypassHeader, getHttpUrl, and getHeaders
  covering env var toggling and proxy/override combinations.
2026-04-21 17:43:30 -07:00
Nathan Rajlich b163860fe4 Guard fibonacciWorkflow against non-finite n (#1814)
Throw a FatalError at the top of fibonacciWorkflow if n is not a finite
number. Prevents a runaway recursion if the workflow is ever invoked
without its numeric argument — NaN - 1 === NaN, NaN <= 1 === false, so
the base case would never fire and every descendant would spawn two
more children.
2026-04-21 00:44:49 -07:00
Pranay Prakash b27921985c Add distributed abort controller guide and implementation (#1811)
* feat: add distributed abort controller guide and implementation

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: implement abort signal step function and refactor workflows to use it

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: enhance distributed abort controller with user-provided ID

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: enhance distributed abort controller with TTL and reconnection logic

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: add grace period to abort controller workflow

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: add distributed abort controller workflow and tests

Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* add the pattern to the sidebar

* fix: correct start() call signature and run.id → run.runId

start() takes args as a positional second argument, not an options
object property. The Run object exposes runId, not id.

Made-with: Cursor

* fix: address PR review comments on distributed abort controller

- Make abort() idempotent by catching hook-not-found/expired errors
- Add error handling to signal getter's stream reader IIFE
- Fix tests: use instance methods instead of non-existent static
  methods, pass required ttlMs/graceMs args, include expired field
  in assertions

Made-with: Cursor

* fix: add missing import to Custom TTL docs code sample

The docs typecheck runs each code block in isolation. The Custom TTL
example was missing the DistributedAbortController import.

Made-with: Cursor

* fix: only sleep grace period on TTL expiration, not manual abort

Manual aborts now complete immediately instead of sleeping through
the full TTL + grace period. This fixes vitest timeouts where all
3 distributed-abort-controller tests exceeded the 30s limit.

Made-with: Cursor

* fix: wait for hook registration before aborting in test

The DistributedAbortController instance test was timing out because
abort() was called before the workflow had registered the hook. The
idempotent catch silently swallowed the "not found" error, leaving the
workflow running forever.

- Make runId readonly (was private) so tests can access it
- Add waitForHook(getRun(controller.runId)) before controller.abort()

Made-with: Cursor

* fix: apply conditional grace period to E2E workflow copy

The distributedAbortControllerWorkflow in 99_e2e.ts had the same
unconditional grace sleep bug, causing the reconnect test to timeout
at 60s while waiting ~66s for the grace period after manual abort.

Made-with: Cursor

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
2026-04-20 13:27:48 -07:00
Nathan Rajlich 5889d84aef ci: auto-resolve skills/ conflicts in backport to stable (#1798)
* ci: auto-resolve skills/ conflicts in backport to stable

The skills/ directory is not maintained on the stable branch (skill
files are unrelated to npm packaging). Extend the backport workflow's
auto-resolution — and the AI-assisted fallback prompt — to treat
skills/ conflicts the same way docs app conflicts are handled: keep
the stable side and drop the incoming change from main.

Also updates AGENTS.md to document which directories are stable-only
placeholders and how the backport action handles them.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-17 11:10:17 -07:00