From b56f5af3cf1c36811f784d0902422fece7a57632 Mon Sep 17 00:00:00 2001 From: Lu Zhang Date: Sun, 19 Jul 2026 07:39:15 +0800 Subject: [PATCH 1/3] Adapt CLI to wdl.20260718.1 Expose workflow-definition lifecycle state and align compatibility, D1, Durable Object, R2, secret, JSRPC, and Workflows contracts. Pin the packaged Wrangler fallback to 4.112.0, reject newly unmapped config fields, and disable update checks and telemetry for CLI-launched Wrangler processes. Refresh bilingual user and agent documentation and add focused regression coverage. Signed-off-by: Lu Zhang --- .claude/skills/wdl-deploy/SKILL.md | 25 +++-- CHANGELOG.md | 19 ++++ GUIDE-zh.md | 22 +++- GUIDE.md | 65 +++++++++--- commands/delete.js | 2 +- commands/deploy.js | 3 + commands/workers.js | 2 +- docs/README-zh.md | 1 + docs/README.md | 1 + docs/d1-zh.md | 2 + docs/d1.md | 5 + docs/deploy-zh.md | 15 ++- docs/deploy.md | 33 +++++- docs/durable-objects-zh.md | 4 + docs/durable-objects.md | 11 ++ docs/r2-zh.md | 2 + docs/r2.md | 4 + docs/secrets-zh.md | 2 +- docs/secrets.md | 5 +- docs/workflows-zh.md | 5 +- docs/workflows.md | 8 ++ lib/delete-format.js | 6 +- lib/workers-format.js | 12 ++- lib/wrangler/command.js | 9 +- lib/wrangler/config.js | 2 + package-lock.json | 123 ++++++++++----------- package.json | 12 +-- templates/AGENTS.md | 17 +-- tests/unit/cli-config-doctor.test.js | 19 ++++ tests/unit/cli-deploy.test.js | 67 +++++++++++- tests/unit/cli-lifecycle.test.js | 153 ++++++++++++++++++++++----- 31 files changed, 511 insertions(+), 145 deletions(-) diff --git a/.claude/skills/wdl-deploy/SKILL.md b/.claude/skills/wdl-deploy/SKILL.md index 4117f2b..7a45037 100644 --- a/.claude/skills/wdl-deploy/SKILL.md +++ b/.claude/skills/wdl-deploy/SKILL.md @@ -46,20 +46,29 @@ authoritative, and agent-facing references use the English set. New Wrangler configs should use `compatibility_date = "2026-06-17"` unless a project feature requires a newer target or the operator gives a different -target. WDL follows Wrangler config priority (`wrangler.json`, then -`wrangler.jsonc`, then `wrangler.toml`); both JSON filenames use Wrangler's -JSONC syntax, including comments and trailing commas. The control plane is -canonical for unsupported runtime shapes such as unsupported workerd -experimental compatibility flags and WDL-reserved injected module names; the -CLI still fails fast for cheap local cases such as Python Workers modules, -unmapped top-level or selected-env Wrangler runtime/deploy keys (`[site]`, -`workers_dev`, `pages_build_output_dir`, `observability`, `limits`, +target. Control rejects explicit dates before `2026-04-01`, invalid or future +dates, dates newer than the bundled workerd supports, upstream experimental +enable flags, `legacy_error_serialization`, and +`allow_irrevocable_stub_storage`. WDL follows Wrangler config priority +(`wrangler.json`, then `wrangler.jsonc`, then `wrangler.toml`). Both JSON +filenames use Wrangler's JSONC syntax, including comments and trailing commas. +The control plane is canonical for unsupported runtime shapes such as +unsupported workerd compatibility flags and WDL-reserved injected module +names. The CLI still fails fast for cheap local cases such as Python Workers +modules, unmapped top-level or selected-env Wrangler runtime/deploy keys +(`[site]`, `workers_dev`, `pages_build_output_dir`, `observability`, `limits`, `placement`, etc.), and ambiguous runtime `env` name collisions between `[vars]`, explicit bindings, and the implicit `ASSETS` binding. WDL-only `[[exports]]`, `[[platform_bindings]]`, `[[triggers.schedules]]`, and `[[services]].ns` are parsed by the CLI and removed from Wrangler's temporary bundle config; other fields retain their existing Wrangler passthrough behavior. Wrangler's object-shaped declarative `exports` config is unsupported. +The dry-run child disables Wrangler's banner/update check and anonymous +telemetry; project build hooks retain their normal network access. +For `[[services]]` and `[[exports]]`, read `docs/deploy.md`: tenant JSRPC may +delegate service or Durable Object class stubs as opaque capabilities, but the +receiver cannot rewrite their host-authored caller properties. Keep delegated +stubs in memory; long-term irrevocable stub storage is unsupported. Never recommend setting `CONTROL_CONNECT_HOST` outside local development: it overrides the TCP target the admin token connects to (Host header + TLS SNI diff --git a/CHANGELOG.md b/CHANGELOG.md index c1421b5..82795b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +### Changed + +- `wdl workers` now reports workflow-definition state (`unknown` when an older + control omits it), and `wdl delete worker --dry-run` reports workflow + definition presence. +- Platform capability docs now cover the current compatibility-date, Durable + Object RPC, Workflows, R2 metadata, secret-key, D1 error, and JSRPC contracts. +- The packaged deploy toolchain now pins Wrangler 4.112.0 and uses + jsonc-parser 3.3.1 and smol-toml 1.7.0. Unmapped Wrangler `addresses` and + `dependencies_instrumentation` fields now fail loudly instead of being + silently dropped. +- `wdl deploy` disables Wrangler's update check and anonymous telemetry for its + dry-run subprocess; project build hooks retain their normal network access. + +### Fixed + +- `wdl deploy` now gives an actionable hint when control returns + `compatibility_flag_unsupported`. + ## 1.4.1 ### Fixed diff --git a/GUIDE-zh.md b/GUIDE-zh.md index b75bb58..f7140fe 100644 --- a/GUIDE-zh.md +++ b/GUIDE-zh.md @@ -93,7 +93,7 @@ CLI 只会从 `.env` 读取 WDL 平台变量:`ADMIN_TOKEN`、`CONTROL_URL`、` `wdl deploy` 在上传前会以你的 OS 用户身份运行项目本地的 Wrangler dry-run 和 build 钩子,这些代码能读到磁盘上的 store(env scrub 只把 WDL 变量挡在 Wrangler 子进程的环境外,挡不住文件),所以只部署你信任的项目。`--no-token-store`(或 `WDL_TOKEN_STORE=off`)让 CLI 只从 flag / shell / `.env` 解析凭据、完全不读 store —— 这是给不太信任的项目或 CI 用的解析 opt-out,不是对文件本身的保护。 -用 `wdl config explain` 查看最终 namespace、control URL、脱敏 token 以及每个值的来源。用 `wdl whoami` 调 control-plane `/whoami`,查看当前 authenticated principal、token id、platform version、最低支持 CLI version 和 URL hints。用 `wdl doctor` 做本地可用性检查,包括 Node.js、wdl-cli、Wrangler、配置文件是否存在、凭据是否能解析,以及 `/whoami` 是否可达;在 CI 里可加 `--strict`,命令仍会打印检查结果,但只要任一检查失败就以非零退出。当 control plane 暴露 `/whoami` 时,`doctor` 可以发现 token 是否有效、principal namespace、platform version 和 CLI compatibility;更细的 capability 检查仍需要额外的 control endpoint。 +用 `wdl config explain` 查看最终 namespace、control URL、脱敏 token 以及每个值的来源。用 `wdl whoami` 调 control-plane `/whoami`,查看当前 authenticated principal、token id、platform version、最低支持 CLI version 和 URL hints。用 `wdl doctor` 做本地可用性检查,包括 Node.js、wdl-cli、Wrangler、配置文件是否存在、凭据是否能解析,以及 `/whoami` 是否可达;在 CI 里可加 `--strict`,命令仍会打印检查结果,但只要任一检查失败就以非零退出。当 control plane 暴露 `/whoami` 时,`doctor` 可以发现 token 是否有效、principal namespace、platform version 和 CLI compatibility;更细的 capability 检查仍需要额外的 control endpoint。运维方没有配置公开 platform domain 时,namespace URL 可能显示为 `(unavailable)`;认证和其它 `/whoami` 字段仍然有效。 ## 脚手架新 Worker @@ -146,7 +146,9 @@ APP_NAME = "hello" 新项目建议使用 `compatibility_date = "2026-06-17"`;除非需要的功能或管理方明确要求更新的目标日期。 -你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。 +Control 会拒绝早于 `2026-04-01` 的显式日期、无效或未来日期,以及超出 bundled workerd 支持范围的日期。上游 experimental enable flags、`legacy_error_serialization` 和 `allow_irrevocable_stub_storage` 不受支持。这些校验由 control canonical 持有,CLI 不复制 workerd flag table。 + +你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。WDL 会关闭这个 dry-run 子进程的 Wrangler banner、更新检查和匿名遥测;项目 build hook 仍保留正常的网络访问能力。 如果同时存在多个 Wrangler 配置文件,WDL 跟随 Wrangler 的优先级:`wrangler.json`,然后 `wrangler.jsonc`,最后 `wrangler.toml`。 @@ -230,7 +232,7 @@ https://.// ## 支持的 wrangler 配置 -Wrangler 能打包、但 WDL 不能运行的形状由 control plane 作为 canonical validator 拒绝,包括不支持的 workerd experimental compatibility flags 和 WDL 保留注入模块名。CLI 仍会对低成本的本地问题 fail-fast,例如 Python Workers modules,以及 `[vars]`、显式 bindings、隐式 `ASSETS` binding 之间的 runtime `env` 名称冲突。Deploy 和 secret mutation 还会校验留有 headroom 的 workerd 1 MiB `workerLoader` env budget;过大的 `[vars]`、secrets、binding metadata 或 retained versions 可能触发 `worker_env_too_large`。 +Wrangler 能打包、但 WDL 不能运行的形状由 control plane 作为 canonical validator 拒绝,包括不支持的 workerd compatibility flags 和 WDL 保留注入模块名。CLI 仍会对低成本的本地问题 fail-fast,例如 Python Workers modules,以及 `[vars]`、显式 bindings、隐式 `ASSETS` binding 之间的 runtime `env` 名称冲突。Deploy 和 secret mutation 还会校验留有 headroom 的 workerd 1 MiB `workerLoader` env budget;过大的 `[vars]`、secrets、binding metadata 或 retained versions 可能触发 `worker_env_too_large`。 | 配置 | 支持情况 | | --- | --- | @@ -336,6 +338,8 @@ WDL 不会创建真实的 per-namespace S3 bucket。`bucket_name` 是部署时 当前支持 `head`、`get`、`put`、`delete`、`list` 等常用路径。需要时也可以使用条件读取、range GET 和 `list({ include })` metadata hydration;metadata hydration 会在并发上限内额外发起 HEAD。`put(stream, ...)` 目前会先 buffer,再做单次 S3 PUT,最大 25 MiB。multipart upload、SSE-C、checksum selection 暂不支持。 +当 `httpMetadata` 使用 `Headers` object 传入时,`Expires` header 必须是 canonical IMF-fixdate,例如 `Wed, 21 Oct 2015 07:28:00 GMT`。 + 用 `wdl r2` 命令查看或显式删除 namespace 内的 R2 数据: ```bash @@ -405,6 +409,8 @@ D1 运行时请求在执行前有边界:binary query body 最大 8 MiB;解 `wdl d1 migrations status/apply` 走 control-plane JSON request parser,所以请求体上限是 1 MiB。特别大的 migration 集合或 SQL 文件应拆成更小批次再 apply。 +Control-plane D1 lifecycle API 对外返回 5xx 时会把消息收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。对外 4xx SQL / migration failure 仍可保留可操作的诊断。 + `examples/d1-demo` 提供了一个最小 visitor counter 示例,包含 D1 binding 和 forward-only migration。 ### Durable Objects @@ -447,6 +453,8 @@ export default { 当前支持 `stub.fetch()`、JSON-structured `stub.method(...args)` RPC、native `ctx.storage`、同步 `ctx.storage.sql`、alarm、普通 WebSocket upgrade 以及 native WebSocket hibernation API surface。跨 script binding、rename/delete migration、平台级 WebSocket session/cursor 恢复暂未实现。 +DO fetch 请求体上限是 1 MiB。RPC method name 必须符合 JavaScript identifier grammar,且最多 256 ASCII bytes。RPC arguments 最多 1 MiB,只接受 structural JSON:finite value、dense array 和 plain object;序列化不会调用 `toJSON()`。Object name 和 id 必须是 well-formed Unicode;DO class name 使用 ASCII JavaScript class-name grammar,最多 468 bytes。 + 使用 `ctx.storage.sql` 时,不要使用以 `_cf_` 开头的应用表名;workerd 对这个前缀做大小写不敏感保留。`ctx.storage.deleteAll()` 也会保留平台自有的 `_cf_*` 表。 `examples/durable-objects-demo` 提供了最小的同 Worker Durable Object 计数器,使用 SQLite-backed storage 保存状态。 @@ -478,7 +486,9 @@ wdl workflows terminate api orders order-123 --yes 重要运行限制和编程规则: +- `createBatch()` 每次最多接受 100 个 entry。单个 workflow result 上限是 1 MiB;一次 runtime 到 Workflows backend 的 JSON 请求上限是 2 MiB。 - 每个 instance 的聚合 payload 上限是 16 MiB。超过上限的 step/event 写入会让请求失败;runtime 写入过大的 terminal result 时,会在同一个事务里把 instance 转为 failed。 +- 新创建的 completed、failed 和 terminated instance 默认保留 8 小时;需要时可通过 `create()` 的 `retention` option 覆盖 success/error retention。 - 一个 `step.do` 如果永久失败,本次 run 会进入 terminal failure,即使用户代码 catch 了这次抛错也一样。 - 单个 step 最多记录 1000 条 dependency edge。单次 dispatch turn 最多有 1000 个 in-flight workflow steps,并最多启动 1000 个 fresh backend steps。 - 用户代码必须 await 所有已启动的 `step.do`。run 在仍有 started step 未 settle 时返回会失败为 `workflow_invalid_step`。 @@ -514,7 +524,7 @@ printf '%s' "$DATABASE_URL" | wdl secret put --scope ns DATABASE_URL - `secret_encryption_unconfigured`、`secret_decrypt_failed`、`invalid_envelope`、`unsupported_envelope`、`unknown_kid` 或 `secret_not_encrypted` 这类 secret-envelope 错误表示 mutation 没有写入;等运维侧修复 envelope 配置或已存储数据后再重试。 - worker-level secret 可以在第一次部署前设置;第一次部署会读取这些 secret。 - namespace-level secret 会共享给 namespace 下的所有 Worker,但不会批量 bump 所有 Worker。它会在下一次自然 cold-load 时生效,例如新部署、runtime recycle 或 isolate eviction。 -- secret key 必须符合环境变量命名规则,例如 `STRIPE_KEY`;value 最大 64 KiB,并且和 `[vars]` 一样计入 workerLoader env budget。 +- secret key 必须符合 `[A-Za-z_][A-Za-z0-9_]*`,最多 128 个字符;runtime-reserved name 和保留的 `Object.prototype` key 会被拒绝。Value 最大 64 KiB,并且和 `[vars]` 一样计入 workerLoader env budget。 ### Queues @@ -682,6 +692,8 @@ Service binding 在部署时解析并绑定到目标当时的线上版本。目 `export default function(request, env, ctx)` 会被当作 fetch handler 简写,并通过 `.fetch(...)` 暴露;它不是可直接调用的 default RPC method。RPC method 应放在命名 `WorkerEntrypoint` 上,或放在 default object / class 的方法上。 +当前 tenant JSRPC 可以序列化 `Blob` value,并把 service 或 Durable Object class stub 作为 opaque capability argument 传递。接收方可以调用被委托的目标,但不能改写 stub 携带的 host-authored caller properties。Delegated stub 只应留在内存中;WDL 不支持长期 irrevocable stub storage。 + ### Platform bindings 如果平台提供了第一方共享能力,你只需要绑定平台给出的符号名: @@ -798,7 +810,7 @@ wdl delete worker hello --dry-run wdl delete worker hello ``` -`wdl delete worker` 默认会要求确认。建议先用 `--dry-run` 预览受影响的线上版本、保留版本、路由、worker secrets、queue consumers 和资产清理。自动化脚本里只有在已有独立安全检查后,才建议传 `--yes`。 +`wdl delete worker` 默认会要求确认。建议先用 `--dry-run` 预览受影响的线上版本、保留版本、路由、worker secrets、workflow definitions、queue consumers 和资产清理。即使没有 deployed version,`wdl workers` 也会用 `workflow-defs=yes` 显示仍有 workflow definitions 的 entry;旧 control 未上报该字段时,CLI 显示 `workflow-defs=unknown`,这不表示没有 workflow definitions。自动化脚本里只有在已有独立安全检查后,才建议传 `--yes`。 确认后删除 D1 数据库: diff --git a/GUIDE.md b/GUIDE.md index c09fb7f..fefecaa 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -158,6 +158,8 @@ gate; the command still prints the checks, then exits non-zero if any check fails. `doctor` can detect token validity, principal namespace, platform version, and CLI compatibility when the control plane exposes `/whoami`; deeper capability checks still require additional control endpoints. +The namespace URL can be `(unavailable)` when the operator has not configured a +public platform domain; authentication and other `/whoami` fields still work. ## Scaffolding a New Worker @@ -221,6 +223,11 @@ APP_NAME = "hello" For new projects, use `compatibility_date = "2026-06-17"` unless a required feature or your operator gives you a newer target. +Control rejects explicit dates before `2026-04-01`, invalid or future dates, +and dates newer than the bundled workerd supports. Upstream experimental enable +flags, `legacy_error_serialization`, and `allow_irrevocable_stub_storage` are +unsupported. These checks are control-owned; the CLI does not mirror workerd's +flag table. You can keep using `wrangler dev` for local development. To deploy to this platform, use `wdl deploy` instead. The deploy command runs @@ -229,6 +236,9 @@ platform, use `wdl deploy` instead. The deploy command runs wrangler, then `PATH`. TypeScript, module resolution, esbuild bundling, and related build behavior still follow Wrangler. +WDL disables Wrangler's banner/update check and anonymous telemetry for this +dry-run subprocess. Project build hooks retain their normal network access. + When several Wrangler config files exist, WDL follows Wrangler's priority: `wrangler.json`, then `wrangler.jsonc`, then `wrangler.toml`. Both JSON filenames use Wrangler's JSONC syntax, including comments and @@ -334,13 +344,13 @@ for you. ## Supported Wrangler Configuration The control plane is the canonical validator for shapes that Wrangler can -bundle but WDL cannot run, including unsupported workerd experimental -compatibility flags and WDL-reserved injected module names. The CLI still -fails fast for cheap local cases such as Python Worker modules and ambiguous -runtime `env` name collisions between `[vars]`, explicit bindings, and the -implicit `ASSETS` binding. Deploy and secret mutation also enforce the -headroomed 1 MiB workerd `workerLoader` env budget; large `[vars]`, secrets, -binding metadata, or retained versions can fail with `worker_env_too_large`. +bundle but WDL cannot run, including unsupported workerd compatibility flags +and WDL-reserved injected module names. The CLI still fails fast for cheap local +cases such as Python Worker modules and ambiguous runtime `env` name collisions +between `[vars]`, explicit bindings, and the implicit `ASSETS` binding. Deploy +and secret mutation also enforce the headroomed 1 MiB workerd `workerLoader` env +budget; large `[vars]`, secrets, binding metadata, or retained versions can fail +with `worker_env_too_large`. | Configuration | Support | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -501,6 +511,9 @@ also supported for Workers that need them; metadata hydration issues extra HEAD requests under a concurrency cap. `put(stream, ...)` currently buffers before sending a single S3 PUT and has a 25 MiB maximum. Multipart upload, SSE-C, and checksum selection are not supported. +When `httpMetadata` is supplied as a `Headers` object, an `Expires` header must +use canonical IMF-fixdate syntax, such as +`Wed, 21 Oct 2015 07:28:00 GMT`. Use `wdl r2` commands to inspect or explicitly delete namespace R2 data: @@ -600,6 +613,11 @@ call are rolled back. its request body is capped at 1 MiB. Split very large migration sets or SQL files into smaller batches before applying. +Control-plane D1 lifecycle APIs redact outward 5xx errors to `Internal error` +and retain only bounded machine classifiers (`upstreamCode`, +`upstreamCategory`, `upstreamRetryable`, and `upstreamStatus`). Outward 4xx SQL +and migration failures can retain actionable diagnostics. + See `examples/d1-demo` for a minimal visitor counter using D1 plus a forward-only migration. @@ -649,6 +667,13 @@ Supported DO surface includes `stub.fetch()`, JSON-structured hibernation API surface. Cross-script bindings, renamed/deleted migrations, and platform-level WebSocket session/cursor recovery are not currently available. +DO fetch request bodies are capped at 1 MiB. RPC method names must use +JavaScript identifier grammar and are capped at 256 ASCII bytes. RPC arguments +are capped at 1 MiB and must be structural JSON: finite values, dense arrays, +and plain objects only; serialization does not call `toJSON()`. Object names and +ids must be well-formed Unicode, and DO class names use ASCII JavaScript +class-name grammar with a 468-byte cap. + For `ctx.storage.sql`, avoid application table names starting with `_cf_`; workerd reserves that prefix case-insensitively. `ctx.storage.deleteAll()` also leaves platform-owned `_cf_*` tables alone. @@ -687,9 +712,15 @@ available. Important runtime limits and programming rules: +- `createBatch()` accepts at most 100 entries per call. A single workflow result + is capped at 1 MiB. One runtime-to-Workflows backend JSON request is capped at + 2 MiB. - Per-instance aggregate payload is capped at 16 MiB. Step/event writes over the cap fail the request; an over-cap runtime terminal result transitions the instance to failed in the same transaction. +- Newly created completed, failed, and terminated instances are retained for 8 + hours by default. Override success/error retention through the `retention` + option passed to `create()` when needed. - A permanently failed `step.do` makes the run terminal even if user code catches the thrown error. - One step may record at most 1000 dependency edges. One dispatch turn may have @@ -749,9 +780,10 @@ Effect timing: - Namespace-level secret changes are shared by every Worker in the namespace, but they do not bump all Workers. They take effect on the next natural cold-load, such as a new deploy, runtime recycle, or isolate eviction. -- Secret keys must use environment-variable grammar, for example `STRIPE_KEY`; - values are limited to 64 KiB and count toward the same workerLoader env budget - as `[vars]`. +- Secret keys must match `[A-Za-z_][A-Za-z0-9_]*` and are capped at 128 + characters; runtime-reserved names and reserved `Object.prototype` keys are + rejected. Values are limited to 64 KiB and count toward the same workerLoader + env budget as `[vars]`. ### Queues @@ -955,6 +987,12 @@ shorthand and is exposed through `.fetch(...)`; it is not a callable default RPC method. Put RPC methods on a named `WorkerEntrypoint` or on default object/class methods. +Current tenant JSRPC can serialize `Blob` values and pass service or Durable +Object class stubs as opaque capability arguments. The receiver may call the +delegated target but cannot rewrite the host-authored caller properties carried +by the stub. Keep delegated stubs in memory; long-term irrevocable stub storage +is not a supported WDL surface. + ### Platform Bindings If the platform provides a first-party shared capability, bind the @@ -1096,8 +1134,11 @@ wdl delete worker hello `wdl delete worker` asks for confirmation by default. Use `--dry-run` first to preview the affected active version, retained versions, routes, worker secrets, -queue consumers, and asset cleanup. In automation, pass `--yes` only after a -separate safety check. +workflow definitions, queue consumers, and asset cleanup. `wdl workers` reports +`workflow-defs=yes` even for entries that have no deployed version. When an +older control does not report this field, the CLI displays +`workflow-defs=unknown`; that does not mean no definitions exist. In automation, +pass `--yes` only after a separate safety check. Delete a D1 database after confirming: diff --git a/commands/delete.js b/commands/delete.js index 7fa263c..fa59cd1 100644 --- a/commands/delete.js +++ b/commands/delete.js @@ -96,7 +96,7 @@ function usageText() { description: "Delete retained versions or all worker-owned lifecycle state.", commands: [ "version Delete one retained non-active worker version.", - "worker Delete a worker, its versions, secrets, routes, and queue consumers.", + "worker Delete a worker, its versions, secrets, workflow definitions, routes, and queue consumers.", ], options: optionHelp(DELETE_OPTIONS), }); diff --git a/commands/deploy.js b/commands/deploy.js index f37a1b4..5f3cde4 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -208,6 +208,9 @@ function deployErrorHint(text) { if (error === "experimental_compat_flag_unsupported") { return "; remove the unsupported workerd experimental compatibility flag."; } + if (error === "compatibility_flag_unsupported") { + return "; remove the unsupported compatibility flag."; + } if (isSecretEnvelopeErrorCode(error)) { return "; secret-envelope configuration or stored secret data needs operator repair before retrying."; } diff --git a/commands/workers.js b/commands/workers.js index 63e9646..acb4d3a 100644 --- a/commands/workers.js +++ b/commands/workers.js @@ -42,7 +42,7 @@ async function printWorkersList(context, json) { function usageText() { return formatHelp({ usage: ["wdl workers [options]"], - description: "List workers, active versions, retained versions, and secret-only entries.", + description: "List active and retained versions, secret-only entries, and workflow-definition-only entries.", options: optionHelp(WORKERS_OPTIONS), }); } diff --git a/docs/README-zh.md b/docs/README-zh.md index b93b94c..7d162c7 100644 --- a/docs/README-zh.md +++ b/docs/README-zh.md @@ -24,6 +24,7 @@ | WDL 环境覆盖 `[env.]` 规则 | [env-overrides-zh.md](./env-overrides-zh.md) | | Worker / namespace 运行时 secrets | [secrets-zh.md](./secrets-zh.md) | | 本地存储控制面 token | [token-zh.md](./token-zh.md) | +| Worker-to-Worker service binding 与 JSRPC delegation | [deploy-zh.md](./deploy-zh.md) | 组合功能时读多个专题。例如:queue 消费后写状态,读 [queues-zh.md](./queues-zh.md) 和 [kv-zh.md](./kv-zh.md);上传文件后记录索引,读 [r2-zh.md](./r2-zh.md) 和 [d1-zh.md](./d1-zh.md);带静态页面的管理工具,读 [assets-zh.md](./assets-zh.md) 和实际绑定对应的专题。 diff --git a/docs/README.md b/docs/README.md index 6ea6471..edd4578 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,7 @@ English set. | WDL `[env.]` override rules | [env-overrides.md](./env-overrides.md) | | Worker / namespace runtime secrets | [secrets.md](./secrets.md) | | Storing control-plane tokens locally | [token.md](./token.md) | +| Worker-to-Worker service bindings and JSRPC delegation | [deploy.md](./deploy.md) | Combining features means reading several topics. For example: writing state after consuming a queue, read [queues.md](./queues.md) and [kv.md](./kv.md); diff --git a/docs/d1-zh.md b/docs/d1-zh.md index 8a3212c..3533cbe 100644 --- a/docs/d1-zh.md +++ b/docs/d1-zh.md @@ -113,6 +113,8 @@ D1 请求在执行前会被限流:binary query body 最大 8 MiB;解码后 D1 migration 管理走 control-plane JSON request parser,所以 `wdl d1 migrations status/apply` 请求体上限是 1 MiB。特别大的 migration 集合或 SQL 文件应拆成更小批次再 apply。`d1 execute --file` 的路径必须存在、可读,并且留在项目根目录内;文件缺失或不可读时 CLI 会在本地拒绝,不会联系 control。 +Control-plane D1 lifecycle API 对外返回 5xx 时会把消息收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。对外 4xx SQL / migration failure 仍可保留可操作的 D1 诊断。 + 以 `_cf_` 开头的 SQLite object name 是 workerd 保留名,大小写不敏感。不要创建或 `RENAME TO` 到 `_cf_*` 形式的应用 table、index、trigger 或 view;包含这类 DDL 的 migration 在新数据库上可能失败。已经 apply 的 migration 文件不要回改;需要修正时新增 forward migration,把应用数据迁到非保留名称。 ## 删除数据库 diff --git a/docs/d1.md b/docs/d1.md index 2dd0b24..0c0562b 100644 --- a/docs/d1.md +++ b/docs/d1.md @@ -138,6 +138,11 @@ large migration sets or large SQL files into smaller batches before applying. project root; the CLI rejects missing or unreadable files before contacting control. +Control-plane D1 lifecycle APIs redact outward 5xx failures to `Internal error` +and expose only bounded machine classifiers (`upstreamCode`, +`upstreamCategory`, `upstreamRetryable`, and `upstreamStatus`). Outward 4xx SQL +and migration failures can retain actionable D1 diagnostics. + SQLite object names beginning with `_cf_` are reserved by workerd, case-insensitively. Do not create or rename application tables, indexes, triggers, or views to `_cf_*`; a migration containing that DDL can fail on a new diff --git a/docs/deploy-zh.md b/docs/deploy-zh.md index 4f6e259..869d010 100644 --- a/docs/deploy-zh.md +++ b/docs/deploy-zh.md @@ -6,6 +6,8 @@ wrangler 解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。默认不会临时 `npx --yes wrangler` 拉包;只有设置 `WDL_ALLOW_NPX_WRANGLER=1` 时才允许这个 fallback。 +WDL 会关闭这个 dry-run 子进程的 Wrangler banner、更新检查和匿名遥测;项目 build hook 仍保留正常的网络访问能力。 + ## CLI 调用形式 按以下顺序选一种: @@ -38,7 +40,7 @@ CLI 需要三个值: **不可信项目:** `wdl deploy` 在上传前会以你的 OS 用户身份运行项目本地的 Wrangler dry-run 和 build 钩子,这些代码能读到磁盘上的 token store(凭证 scrub 只把 WDL 变量挡在 Wrangler 子进程环境外,挡不住文件)。只部署你信任的项目。对不可信 / 第三方项目,用临时的 `--token` / `--control-url` 加 `--no-token-store`(或 `WDL_TOKEN_STORE=off`)让 CLI 不读 store —— 而且根本别留全局 store,因为这个 flag 只是不**读**文件,挡不住文件本身在磁盘上。详见 [token-zh.md](./token-zh.md)。 -不确定最终取了哪个值时,运行 `wdl config explain`;要确认 token 实际连到哪个 control、principal、platform version 和 URL hints,运行 `wdl whoami`;本机与远端基础排查运行 `wdl doctor`。当 control 支持 `/whoami` 时,`doctor` 会验证远端 token、principal namespace、platform version 和 CLI compatibility。CI 里需要失败即挡住后续步骤时,用 `wdl doctor --strict`。 +不确定最终取了哪个值时,运行 `wdl config explain`;要确认 token 实际连到哪个 control、principal、platform version 和 URL hints,运行 `wdl whoami`;本机与远端基础排查运行 `wdl doctor`。当 control 支持 `/whoami` 时,`doctor` 会验证远端 token、principal namespace、platform version 和 CLI compatibility。CI 里需要失败即挡住后续步骤时,用 `wdl doctor --strict`。如果运维方没有配置公开 platform domain,namespace URL 可能显示为 `(unavailable)`;这不代表认证失败。 运行时密钥(与 `ADMIN_TOKEN` 不同)见 [secrets-zh.md](./secrets-zh.md)。 @@ -97,11 +99,17 @@ wdl deploy . --env production `wrangler.json` 和 `wrangler.jsonc` 都按 Wrangler 的 JSONC 语法解析,支持注释和尾随逗号。 +新项目应继续使用 `2026-06-17` compatibility date,除非具体功能需要更新日期。Control 会拒绝早于 `2026-04-01` 的显式日期、无效或未来日期,以及超出 bundled workerd 支持范围的日期。上游 experimental enable flags、`legacy_error_serialization` 和 `allow_irrevocable_stub_storage` 不受支持。 + **支持:** `name`、`main`、`compatibility_date` / `compatibility_flags`、`[vars]`、`[[kv_namespaces]]`、`[[d1_databases]]`、`[[durable_objects.bindings]]`、`[[workflows]]`、`[[r2_buckets]]`、`[assets] directory`、`[triggers] crons`、`[[triggers.schedules]]`(带 timezone,平台扩展)、`[[queues.producers]]` / `[[queues.consumers]]`、`[[services]]`、`[[platform_bindings]]`、`[[exports]]`、`[env.]`。 WDL 会自行解析 `[[exports]]`、`[[platform_bindings]]`、`[[triggers.schedules]]` 和 `[[services]].ns`,并从传给 Wrangler bundler 的临时配置中移除这些私有扩展;其它字段保持既有的 Wrangler 透传行为。WDL 不支持 Wrangler 对象形态的 declarative `exports` 配置。 -**不支持(部署失败):** Analytics Engine。Durable Objects 仅支持同 worker class;`script_name`、rename/delete migration 暂未实现。WDL Workflows 仅支持当前 Worker 内定义的 workflow class,不是完整 Cloudflare Workflows parity;`script_name`、跨 worker workflow、跨 worker callback、service-binding callback 和 Cloudflare source-AST visualizer 暂不支持。`route` / `routes` 仅在运维方启用时支持。Python Workers modules、workerd experimental compatibility flags 和 WDL 保留注入模块名会在部署时被拒绝:CLI 会对本地 `.py` module fail-fast,workerd compatibility 与 bundle-shape policy 由 control plane canonical 判断。WDL 会忽略、且无法映射进 manifest 的顶层或所选 env Wrangler runtime/deploy 配置字段和 section 也会由 CLI 直接拒绝,包括 legacy `[site]` Workers Sites、`workers_dev`、`pages_build_output_dir`、`observability`、`limits`、`placement`,以及错误信息点名的其它 unsupported binding/config field 或 section。`assets.run_worker_first` 会被静默忽略。 +### Service bindings 与 capability delegation + +Tenant JSRPC 可以序列化 `Blob` value,并把 service 或 Durable Object class stub 作为 opaque capability argument 传递。接收方可以调用被委托的目标,但不能改写 stub 携带的 host-authored caller properties。Delegated stub 只应留在内存中;WDL 不支持长期 irrevocable stub storage。 + +**不支持(部署失败):** Analytics Engine。Durable Objects 仅支持同 worker class;`script_name`、rename/delete migration 暂未实现。WDL Workflows 仅支持当前 Worker 内定义的 workflow class,不是完整 Cloudflare Workflows parity;`script_name`、跨 worker workflow、跨 worker callback、service-binding callback 和 Cloudflare source-AST visualizer 暂不支持。`route` / `routes` 仅在运维方启用时支持。Python Workers modules、不支持的 workerd compatibility flags 和 WDL 保留注入模块名会在部署时被拒绝:CLI 会对本地 `.py` module fail-fast,workerd compatibility 与 bundle-shape policy 由 control plane canonical 判断。WDL 会忽略、且无法映射进 manifest 的顶层或所选 env Wrangler runtime/deploy 配置字段和 section 也会由 CLI 直接拒绝,包括 legacy `[site]` Workers Sites、`workers_dev`、`pages_build_output_dir`、`observability`、`limits`、`placement`,以及错误信息点名的其它 unsupported binding/config field 或 section。`assets.run_worker_first` 会被静默忽略。 Cron triggers 和 queue consumers 是 runtime dispatch 能力,只应声明在可路由的 tenant Worker 上。通过 `[[platform_bindings]]` 选择的 Worker 是冷加载的平台能力,不是 public/runtime dispatch 目标,不能声明 cron triggers 或 queue consumers。 @@ -109,6 +117,8 @@ Cron triggers 和 queue consumers 是 runtime dispatch 能力,只应声明在 `wdl delete worker`、`wdl delete version`、`wdl d1 delete`、`wdl secret delete` 默认会提示确认。如果有 `--dry-run`,先跑一遍(或先做只读检查),然后跟用户确认了再加 `--yes`。**不要**主动加 `--yes`。 +`wdl workers` 会显示 `workflow-defs=yes` 或 `workflow-defs=no`;`unknown` 表示旧 control 没有返回该字段,不表示没有 workflow definitions。即使 blocker 使 `wouldDelete=no`,worker delete dry-run 仍会报告 secret 和 workflow-definition 是否存在。 + 删除 worker **不会**删除 R2 数据 —— 见 [r2-zh.md](./r2-zh.md)。 ## 常见错误 @@ -122,6 +132,7 @@ Cron triggers 和 queue consumers 是 runtime dispatch 能力,只应声明在 | `[vars] : only string/number/boolean values are supported` | 移除嵌套值;敏感字符串改用 secret。 | | `binding name collision: ` | `[vars]`、显式 bindings 或隐式 `ASSETS` binding 复用了 runtime env 名称;重命名其中一个。 | | `experimental_compat_flag_unsupported` | 移除 experimental workerd compatibility flag。 | +| `compatibility_flag_unsupported` | 移除 control 点名的不支持 compatibility flag。 | | `python_workers_unsupported` | WDL 不支持 Python Workers;移除 Python Worker modules。CLI 也会对本地 `.py` module fail-fast。 | | `worker_env_too_large` | 减少 `[vars]`、secrets 或 binding metadata;如果错误点名 retained version,redeploy/delete 该版本。 | | `worker_code_too_large` | 减少生成的 Worker code 大小,或拆分 worker。 | diff --git a/docs/deploy.md b/docs/deploy.md index 3208ebe..587d481 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -12,6 +12,9 @@ wrangler, the CLI package's local wrangler, then `PATH`. By default there is no transient `npx --yes wrangler` fetch; that fallback is allowed only when `WDL_ALLOW_NPX_WRANGLER=1` is set. +WDL disables Wrangler's banner/update check and anonymous telemetry for this +dry-run subprocess. Project build hooks retain their normal network access. + ## CLI invocation forms Pick one in this order: @@ -76,6 +79,8 @@ run `wdl whoami`; for baseline local and remote diagnostics, run `wdl doctor`. When the control plane supports `/whoami`, `doctor` verifies the remote token, principal namespace, platform version, and CLI compatibility. Use `wdl doctor --strict` in CI when a failed check should make the job fail. +The namespace URL may be `(unavailable)` when the operator has not configured a +public platform domain; that does not mean authentication failed. For runtime secrets (distinct from `ADMIN_TOKEN`), see [secrets.md](./secrets.md). @@ -162,6 +167,12 @@ When multiple Wrangler config files exist, the CLI follows Wrangler's priority: Both JSON filenames use Wrangler's JSONC syntax, including comments and trailing commas. +New projects should keep the `2026-06-17` compatibility date unless a feature +requires a newer one. Explicit dates before `2026-04-01`, invalid or future +dates, and dates newer than the bundled workerd supports are rejected by +control. Upstream experimental enable flags, `legacy_error_serialization`, and +`allow_irrevocable_stub_storage` are unsupported. + **Supported:** `name`, `main`, `compatibility_date` / `compatibility_flags`, `[vars]`, `[[kv_namespaces]]`, `[[d1_databases]]`, `[[durable_objects.bindings]]`, `[[workflows]]`, `[[r2_buckets]]`, `[assets] directory`, `[triggers] crons`, @@ -175,16 +186,24 @@ extensions from the temporary config passed to the Wrangler bundler. Other fields retain their existing Wrangler passthrough behavior. Wrangler's object-shaped declarative `exports` configuration is not supported by WDL. +### Service bindings and delegated capabilities + +Tenant JSRPC can serialize `Blob` values and pass service or Durable Object +class stubs as opaque capability arguments. A receiver may call the delegated +target but cannot rewrite the host-authored caller properties carried by the +stub. Keep delegated stubs in memory; irrevocable persistence is unsupported. + **Unsupported (deploy fails):** Analytics Engine. Durable Objects supports same-worker classes only; `script_name` and rename/delete migrations are not implemented yet. WDL Workflows supports only workflow classes defined in the current Worker — not full Cloudflare Workflows parity; `script_name`, cross-worker workflows, cross-worker callbacks, service-binding callbacks, and the Cloudflare source-AST visualizer are not supported. `route` / `routes` are -supported only when the operator enables them. Python Workers modules, workerd -experimental compatibility flags, and WDL-reserved injected module names are -rejected during deploy: the CLI fails fast on local `.py` modules, and the -control plane is canonical for workerd compatibility and bundle-shape policy. +supported only when the operator enables them. Python Workers modules, +unsupported workerd compatibility flags, and WDL-reserved injected module +names are rejected during deploy: the CLI fails fast on local `.py` modules, +and the control plane is canonical for workerd compatibility and bundle-shape +policy. Top-level or selected-environment Wrangler runtime/deploy config fields and sections that WDL would otherwise ignore are also rejected by the CLI, including legacy `[site]` Workers Sites, `workers_dev`, `pages_build_output_dir`, @@ -205,6 +224,11 @@ consumers. run it first (or do a read-only check), then add `--yes` only after confirming with the user. Do **not** add `--yes` on your own. +`wdl workers` reports `workflow-defs=yes` or `workflow-defs=no`; `unknown` +means an older control omitted the field, not that no definitions exist. Worker +delete dry-runs report secret and workflow-definition presence even when a +blocker makes `wouldDelete=no`. + Deleting a worker does **not** delete R2 data — see [r2.md](./r2.md). ## Common errors @@ -218,6 +242,7 @@ Deleting a worker does **not** delete R2 data — see [r2.md](./r2.md). | `[vars] : only string/number/boolean values are supported` | Remove nested values; move sensitive strings to a secret. | | `binding name collision: ` | `[vars]`, explicit bindings, or the implicit `ASSETS` binding reused a runtime env name. Rename one of them. | | `experimental_compat_flag_unsupported` | Remove the experimental workerd compatibility flag. | +| `compatibility_flag_unsupported` | Remove the unsupported compatibility flag named by control. | | `python_workers_unsupported` | Python Workers are not supported by WDL; remove Python Worker modules. The CLI also fails fast on local `.py` modules. | | `worker_env_too_large` | Reduce `[vars]`, secrets, or binding metadata; redeploy/delete any retained version named in the error. | | `worker_code_too_large` | Reduce generated Worker code size or split the worker. | diff --git a/docs/durable-objects-zh.md b/docs/durable-objects-zh.md index f5cd641..d8b8b66 100644 --- a/docs/durable-objects-zh.md +++ b/docs/durable-objects-zh.md @@ -44,6 +44,10 @@ export default { 当前支持 `stub.fetch()`、JSON-structured `stub.method(...args)` RPC、native `ctx.storage`、同步 `ctx.storage.sql`、alarm、普通 WebSocket upgrade 以及 native WebSocket hibernation API surface。 +DO fetch 请求体上限是 1 MiB。RPC method name 必须符合 JavaScript identifier grammar,且最多 256 ASCII bytes。RPC arguments 最多 1 MiB,只接受 structural JSON:finite number、string、boolean、null、dense array 和 plain object。序列化不会调用 `toJSON()`;sparse array、循环结构、非 plain object 和其它非 JSON 值会在 dispatch 前被拒绝。 + +Object name 和 id 必须是 well-formed Unicode;lone UTF-16 surrogate 会被拒绝。DO class name 使用 ASCII JavaScript class-name grammar,最多 468 bytes。 + 使用 `ctx.storage.sql` 时,不要使用以 `_cf_` 开头的应用表名;workerd 对这个前缀做大小写不敏感保留。`ctx.storage.deleteAll()` 也会保留平台自有的 `_cf_*` 表。 ## 端到端示例 diff --git a/docs/durable-objects.md b/docs/durable-objects.md index d71f902..a070a66 100644 --- a/docs/durable-objects.md +++ b/docs/durable-objects.md @@ -50,6 +50,17 @@ Currently supported: `stub.fetch()`, JSON-structured `stub.method(...args)` RPC, native `ctx.storage`, synchronous `ctx.storage.sql`, alarms, ordinary WebSocket upgrade, and the native WebSocket hibernation API surface. +DO fetch request bodies are capped at 1 MiB. RPC method names must use +JavaScript identifier grammar and are capped at 256 ASCII bytes. RPC arguments +are capped at 1 MiB and must be structural JSON: finite numbers, strings, +booleans, null, dense arrays, and plain objects. Serialization does not call +`toJSON()`; sparse arrays, circular structures, non-plain objects, and other +non-JSON values are rejected before dispatch. + +Object names and ids must be well-formed Unicode; lone UTF-16 surrogates are +rejected. DO class names use ASCII JavaScript class-name grammar and are capped +at 468 bytes. + For `ctx.storage.sql`, avoid application table names beginning with `_cf_`; workerd reserves that prefix case-insensitively. `ctx.storage.deleteAll()` also leaves platform-owned `_cf_*` tables alone. diff --git a/docs/r2-zh.md b/docs/r2-zh.md index e8599d9..230fbd7 100644 --- a/docs/r2-zh.md +++ b/docs/r2-zh.md @@ -85,6 +85,8 @@ export default { `list({ include: ["httpMetadata", "customMetadata"] })` 会为列表里的对象额外发起 HEAD 来补齐 metadata,并带有并发上限。需要大范围扫描时,不要默认打开 metadata include;只在列表结果确实要展示或决策 metadata 时使用。 +当 `httpMetadata` 使用 `Headers` object 传入时,`Expires` header 必须是 canonical IMF-fixdate,例如 `Wed, 21 Oct 2015 07:28:00 GMT`。格式错误的值会在调用 host binding 前被拒绝。 + ### `put` 大小上限 `put(stream, ...)` 会先把流缓冲下来再发一次 S3 PUT,**上限 25 MiB**。当前平台还不支持 multipart upload;超过上限会报错,不会自动分片上传。 diff --git a/docs/r2.md b/docs/r2.md index 4a14386..9939465 100644 --- a/docs/r2.md +++ b/docs/r2.md @@ -99,6 +99,10 @@ requests to hydrate metadata for the listed objects, under a concurrency cap. For large scans, do not enable the metadata include by default; use it only when the list results genuinely need metadata for display or decisions. +When `httpMetadata` is supplied as a `Headers` object, an `Expires` header must +use canonical IMF-fixdate syntax, such as `Wed, 21 Oct 2015 07:28:00 GMT`. +Malformed values are rejected before the host binding call. + ### `put` size cap `put(stream, ...)` buffers the stream first, then sends a single S3 PUT, with a diff --git a/docs/secrets-zh.md b/docs/secrets-zh.md index 9b2f054..2e3f87e 100644 --- a/docs/secrets-zh.md +++ b/docs/secrets-zh.md @@ -54,7 +54,7 @@ Worker 级 secret mutation 是原子的:如果更新期间 active version 变 ## 约束 -- Key 必须符合环境变量命名规范:`[A-Z_][A-Z0-9_]*` —— 例如 `STRIPE_KEY`、`API_TOKEN`、`SIGNING_SECRET`。 +- Key 必须符合环境变量命名规范 `[A-Za-z_][A-Za-z0-9_]*`,最多 128 个字符。Runtime-reserved name 和保留的 `Object.prototype` key 会被拒绝。 - 值上限 64 KiB。 - Secrets 会和 `[vars]`、binding metadata 一起计入 workerLoader env budget。如果 mutation 返回 `worker_env_too_large`,减少 env payload;如果错误点名 retained version,redeploy/delete 该版本。 diff --git a/docs/secrets.md b/docs/secrets.md index 4e8c4d3..57a9e2e 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -75,8 +75,9 @@ retry after the operator reports the envelope issue repaired. ## Constraints -- Keys must follow environment-variable grammar: `[A-Z_][A-Z0-9_]*` — e.g. - `STRIPE_KEY`, `API_TOKEN`, `SIGNING_SECRET`. +- Keys must follow environment-variable grammar: `[A-Za-z_][A-Za-z0-9_]*`, + with a maximum length of 128 characters. Runtime-reserved names and reserved + `Object.prototype` keys are rejected. - Values are limited to 64 KiB. - Secrets count toward the workerLoader env budget together with `[vars]` and binding metadata. If a mutation returns `worker_env_too_large`, reduce the diff --git a/docs/workflows-zh.md b/docs/workflows-zh.md index a13a983..a9e8d56 100644 --- a/docs/workflows-zh.md +++ b/docs/workflows-zh.md @@ -21,7 +21,10 @@ Worker 代码遵循 Cloudflare Workflows 心智模型:`WorkflowEntrypoint`、 ## 编写限制 +- `createBatch()` 每次最多接受 100 个 entry。 +- 单个 workflow result 上限是 1 MiB;一次 runtime 到 Workflows backend 的 JSON 请求上限是 2 MiB。 - 每个 workflow instance 的聚合 payload 上限是 16 MiB。超过上限的 step/event 写入会让请求失败;runtime 写入过大的 terminal result 时,会在同一个事务里把 instance 转为 failed。 +- 新创建的 completed、failed 和 terminated instance 默认保留 8 小时。需要时可通过 `create({ retention: { successRetention, errorRetention } })` 分别覆盖成功和错误保留时间。 - 一个 step 最多记录 1000 条 dependency edge。单次 runtime dispatch turn 最多有 1000 个 in-flight workflow steps,且最多启动 1000 个 fresh backend steps。 - 启动了 `step.do` promise 后,必须 await 所有已启动 step;run 在仍有 started step 未 settle 时返回会失败为 `workflow_invalid_step`。 - 并行 `step.do` sibling 必须在同一个同步 fan-out batch 里创建,再统一 `await`。一旦 await 了其中一个 sibling,就必须等完整 batch 结束后才能启动下一批 durable step,这样 replay 才能计算相同的 dependency frontier。 @@ -41,7 +44,7 @@ wdl workflows terminate --yes `restart` 和 `terminate` 是破坏性实例生命周期操作;只有在已经独立确认 namespace、worker、workflow 和 instance id 后才传 `--yes`。 -Workflows API 的语义大小限制会返回 `request_too_large`;HTTP body 解析阶段的大小限制可能返回 `request_body_too_large`。Workflows 5xx 表示平台或 backend 故障,响应体会保持通用错误摘要;底层诊断进入平台日志,不作为稳定 CLI 输出。 +Workflows API 的语义大小限制会返回 `request_too_large`;HTTP body 解析阶段的大小限制可能返回 `request_body_too_large`。Workflows 5xx 表示平台或 backend 故障,响应体会保持通用错误摘要;底层诊断进入平台日志,不作为稳定 CLI 输出。`workflow_metadata_contention` 表示 control 读取期间 active workflow metadata 发生变化,重试命令即可。 ## 端到端示例 diff --git a/docs/workflows.md b/docs/workflows.md index 7a3fafc..d1e8191 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -33,9 +33,15 @@ if user code catches the thrown error. ## Programming limits +- `createBatch()` accepts at most 100 entries per call. +- A single workflow result is capped at 1 MiB, and one runtime-to-Workflows + backend JSON request is capped at 2 MiB. - Per-instance aggregate payload is capped at 16 MiB. Step/event writes over the cap fail the request; when the runtime writes an over-cap terminal result, it transitions the instance to failed in the same transaction. +- Newly created completed, failed, and terminated instances are retained for 8 + hours by default. Override success and error retention with + `create({ retention: { successRetention, errorRetention } })` when needed. - One step may record at most 1000 dependency edges. A single runtime dispatch turn may have at most 1000 in-flight workflow steps and start at most 1000 fresh backend steps. @@ -73,6 +79,8 @@ limits hit during HTTP body parsing may return `request_body_too_large`. A Workflows 5xx means a platform or backend failure, and the response body stays a generic error summary; underlying diagnostics go to platform logs and are not stable CLI output. +`workflow_metadata_contention` means the active workflow metadata changed while +control was reading it; retry the command. ## End-to-end example diff --git a/lib/delete-format.js b/lib/delete-format.js index a69461d..3c12ee8 100644 --- a/lib/delete-format.js +++ b/lib/delete-format.js @@ -54,8 +54,9 @@ const ASSET_WARNING_KEYS = [ * @property {boolean} [deleted] * @property {boolean} [noop] * @property {boolean} [hasWorkerSecrets] + * @property {boolean} [hasWorkflowDefs] * @property {string[]} [versionsDeleted] - * @property {string} [activeDeleted] + * @property {string | null} [activeDeleted] * @property {string[]} [affectedHosts] * @property {number} [queueConsumersRemoved] * @property {DeleteBlocker[]} [blockers] @@ -113,7 +114,8 @@ function formatDryRun(body) { `DRY RUN ${field(body.namespace)}/${field(body.name)} wouldDelete=${body.deleted ? "yes" : "no"} active=${active} versions=${versions}`, ]; if (body.noop) lines.push(" no worker-owned state found"); - if (body.hasWorkerSecrets) lines.push(" worker secrets would be deleted"); + if (body.hasWorkerSecrets) lines.push(" worker secrets present"); + if (body.hasWorkflowDefs) lines.push(" workflow definitions present"); if (Array.isArray(body.affectedHosts) && body.affectedHosts.length) { lines.push(` affected hosts: ${body.affectedHosts.map((host) => field(host)).join(",")}`); } diff --git a/lib/workers-format.js b/lib/workers-format.js index 7b58ac9..b56f011 100644 --- a/lib/workers-format.js +++ b/lib/workers-format.js @@ -1,11 +1,14 @@ // Human-readable rendering for `wdl workers`. +import { escapeTerminalText } from "./output.js"; + /** * @typedef {object} WorkerSummary * @property {string} [name] * @property {string[]} [versions] * @property {string | null} [activeVersion] null when the worker has no deployed version. * @property {boolean} [hasSecrets] + * @property {boolean} [hasWorkflowDefs] */ /** @@ -17,10 +20,13 @@ export function formatWorkersList(body) { if (workers.length === 0) return ["(no workers)"]; return workers.map((w) => { const versions = Array.isArray(w.versions) && w.versions.length - ? w.versions.join(",") + ? w.versions.map((version) => escapeTerminalText(version)).join(",") : "-"; - const active = w.activeVersion || "-"; + const active = w.activeVersion ? escapeTerminalText(w.activeVersion) : "-"; const secrets = w.hasSecrets ? "yes" : "no"; - return `${w.name}\tactive=${active}\tversions=${versions}\tsecrets=${secrets}`; + const workflowDefs = typeof w.hasWorkflowDefs === "boolean" + ? (w.hasWorkflowDefs ? "yes" : "no") + : "unknown"; + return `${escapeTerminalText(w.name)}\tactive=${active}\tversions=${versions}\tsecrets=${secrets}\tworkflow-defs=${workflowDefs}`; }); } diff --git a/lib/wrangler/command.js b/lib/wrangler/command.js index 46f577c..61487f0 100644 --- a/lib/wrangler/command.js +++ b/lib/wrangler/command.js @@ -126,7 +126,14 @@ export function checkWranglerVersion({ execFile = execFileSync, cwd, env, wrangl */ export function wranglerChildEnv(env) { /** @type {NodeJS.ProcessEnv} */ - const childEnv = { ...env, CLOUDFLARE_API_TOKEN: "dry-run-dummy" }; + const childEnv = { + ...env, + CLOUDFLARE_API_TOKEN: "dry-run-dummy", + // WDL captures Wrangler's banner and only needs local bundle output. Avoid + // its npm update check and anonymous telemetry during this subprocess. + WRANGLER_HIDE_BANNER: "true", + WRANGLER_SEND_METRICS: "false", + }; for (const key of WRANGLER_SCRUB_KEYS) { delete childEnv[key]; } diff --git a/lib/wrangler/config.js b/lib/wrangler/config.js index 8946bfb..34c0ff7 100644 --- a/lib/wrangler/config.js +++ b/lib/wrangler/config.js @@ -30,6 +30,7 @@ const TOP_LEVEL_ONLY_ENV_KEYS = new Set([ // Bundling-only keys (build, alias, tsconfig, rules, etc.) stay allowed // because Wrangler consumes them before the CLI collects the output manifest. const UNSUPPORTED_WRANGLER_KEYS = [ + "addresses", "agent_memory", "ai", "ai_search", @@ -42,6 +43,7 @@ const UNSUPPORTED_WRANGLER_KEYS = [ "compliance_region", "containers", "data_blobs", + "dependencies_instrumentation", "dispatch_namespaces", "first_party_worker", "flagship", diff --git a/package-lock.json b/package-lock.json index 4412a95..32bf2a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "1.4.1", "license": "Apache-2.0", "dependencies": { - "jsonc-parser": "3.2.0", - "smol-toml": "^1.3.1", - "wrangler": "^4.102.0" + "jsonc-parser": "3.3.1", + "smol-toml": "^1.7.0", + "wrangler": "4.112.0" }, "bin": { "wdl": "bin/wdl.js" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^22.19.21", - "eslint": "^10.4.1", - "globals": "^17.6.0", + "@types/node": "^22.20.1", + "eslint": "^10.7.0", + "globals": "^17.7.0", "typescript": "^6.0.3" }, "engines": { @@ -52,9 +52,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260617.1.tgz", - "integrity": "sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260714.1.tgz", + "integrity": "sha512-ZWXqAN8G7Cx9hMRQuk+59ziJhR3j1F4iO+Qs8aHdfKZ3Dq5Yi/57xvkJTgCGBnW1YU/L78r8f6HEy51bwbTpNw==", "cpu": [ "x64" ], @@ -68,9 +68,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260617.1.tgz", - "integrity": "sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260714.1.tgz", + "integrity": "sha512-tueWxWC3wyCbMG6zRAxsMXX0YLgrRWbiAPYFQ2uJ7dUH8G+5E7UTWaQS9B1HdJ0bpKFW1NWxhs1o2noKVFSUYg==", "cpu": [ "arm64" ], @@ -84,9 +84,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260617.1.tgz", - "integrity": "sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260714.1.tgz", + "integrity": "sha512-1VChTZRb0l0F7R4e1G5RtLKV4oFi6x+rQgxh2+yu887j3l/3TLgatuv1L8/5zhc9gKEhATTxOh0e52Rtd9dDWQ==", "cpu": [ "x64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260617.1.tgz", - "integrity": "sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260714.1.tgz", + "integrity": "sha512-rMm3G+NirG2UdgHIRDdF1asNC6FqgIzZzkRG+VDhhDGcVxAQwvrMT1E38BivEvHr3G04MB4AfhcOczX0+GtRkQ==", "cpu": [ "arm64" ], @@ -116,9 +116,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260617.1.tgz", - "integrity": "sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260714.1.tgz", + "integrity": "sha512-cGqnU3Hg2YZS/k3SAqrMp1DjpdsyFde72tWltdl6ZT9+SFz/Zrk/8gyTU1TcxC4YApXeNVH5TyU5cOGPgUJ0pg==", "cpu": [ "x64" ], @@ -144,9 +144,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "license": "MIT", "optional": true, "dependencies": { @@ -1367,9 +1367,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", - "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1571,11 +1571,14 @@ } }, "node_modules/eslint": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", - "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1822,9 +1825,9 @@ } }, "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", "dev": true, "license": "MIT", "engines": { @@ -1906,9 +1909,9 @@ "license": "MIT" }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, "node_modules/keyv": { @@ -1961,15 +1964,15 @@ } }, "node_modules/miniflare": { - "version": "4.20260617.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260617.0.tgz", - "integrity": "sha512-A+H5gcOCQZsKFg7/daZUtx8WHn4gGxwUfH1jnNDAisyAWSvvSZHe+GCeQWs16uthnUDcm72UQIQ1NXDJtnuo9Q==", + "version": "4.20260714.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260714.0.tgz", + "integrity": "sha512-MYlTCLdWCPqvrYY2uLwOjXwmglXuiHE3TGGkbOW4BwjUPa1r07E0iuHwrNDIs/sxK21r+o90Jx58AV2KeNdJZw==", "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", - "workerd": "1.20260617.1", + "workerd": "1.20260714.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2113,9 +2116,9 @@ } }, "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2192,9 +2195,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -2311,9 +2314,9 @@ } }, "node_modules/workerd": { - "version": "1.20260617.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260617.1.tgz", - "integrity": "sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew==", + "version": "1.20260714.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260714.1.tgz", + "integrity": "sha512-oIbQzfdyl9UQUnG6XLegcSq0Mgt/7WKDbFOoqGgOWCS+/fhyGB460uKEgdAQQ9RHCO/ttcNCX/KiMIQzdoeu3Q==", "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -2323,27 +2326,27 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260617.1", - "@cloudflare/workerd-darwin-arm64": "1.20260617.1", - "@cloudflare/workerd-linux-64": "1.20260617.1", - "@cloudflare/workerd-linux-arm64": "1.20260617.1", - "@cloudflare/workerd-windows-64": "1.20260617.1" + "@cloudflare/workerd-darwin-64": "1.20260714.1", + "@cloudflare/workerd-darwin-arm64": "1.20260714.1", + "@cloudflare/workerd-linux-64": "1.20260714.1", + "@cloudflare/workerd-linux-arm64": "1.20260714.1", + "@cloudflare/workerd-windows-64": "1.20260714.1" } }, "node_modules/wrangler": { - "version": "4.102.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.102.0.tgz", - "integrity": "sha512-GPljlQs9a+/Ai2h0TdEUYaaWv9upK4fUteSWTPlruas7tdixiIwr74CZSWHcEPuRgZYkyYKHIBbT1w+BYPkPrw==", + "version": "4.112.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.112.0.tgz", + "integrity": "sha512-5H+XUD0TySCv1LuktFHDIEOkboH2nTfQs+35L+USt3MtntjDTMVIJprLgQcL2WBjulOyjxpd1vyTiSTJVW5MjQ==", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260617.0", + "miniflare": "4.20260714.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260617.1" + "workerd": "1.20260714.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2357,7 +2360,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260617.1" + "@cloudflare/workers-types": "^5.20260714.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 283d278..8b177a2 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "GUIDE-zh.md" ], "dependencies": { - "jsonc-parser": "3.2.0", - "smol-toml": "^1.3.1", - "wrangler": "^4.102.0" + "jsonc-parser": "3.3.1", + "smol-toml": "^1.7.0", + "wrangler": "4.112.0" }, "engines": { "node": ">=22" @@ -63,9 +63,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^22.19.21", - "eslint": "^10.4.1", - "globals": "^17.6.0", + "@types/node": "^22.20.1", + "eslint": "^10.7.0", + "globals": "^17.7.0", "typescript": "^6.0.3" } } diff --git a/templates/AGENTS.md b/templates/AGENTS.md index af028d7..24fd55d 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -32,6 +32,7 @@ package. | WDL environment override rules (preview / production) | `env-overrides.md` | | Runtime secrets | `secrets.md` | | Storing control-plane tokens locally | `token.md` | +| Service bindings / JSRPC capability delegation | `deploy.md` | | Deploy / dry-run / list and delete workers | `deploy.md` | Open the relevant doc before editing `wrangler.json` / `wrangler.jsonc` / @@ -40,15 +41,17 @@ read each matching doc and merge their wrangler config snippets. New Wrangler configs should use `compatibility_date = "2026-06-17"` unless a project feature requires a newer target or the operator gives a different -target. WDL follows Wrangler config priority +target. Control rejects explicit dates before `2026-04-01`, invalid or future +dates, dates newer than the bundled workerd supports, upstream experimental +enable flags, `legacy_error_serialization`, and +`allow_irrevocable_stub_storage`. WDL follows Wrangler config priority (`wrangler.json`, then `wrangler.jsonc`, then `wrangler.toml`). The control plane is canonical for unsupported runtime shapes such as unsupported workerd -experimental compatibility flags and WDL-reserved injected module names; the -CLI still fails fast for cheap local cases such as Python Workers modules, -unmapped top-level or selected-env Wrangler runtime/deploy keys (`[site]`, -`workers_dev`, `pages_build_output_dir`, etc.), and ambiguous runtime `env` -name collisions between `[vars]`, explicit bindings, and the implicit `ASSETS` -binding. +compatibility flags and WDL-reserved injected module names. The CLI still fails +fast for cheap local cases such as Python Workers modules, unmapped top-level or +selected-env Wrangler runtime/deploy keys (`[site]`, `workers_dev`, +`pages_build_output_dir`, etc.), and ambiguous runtime `env` name collisions +between `[vars]`, explicit bindings, and the implicit `ASSETS` binding. ## Runnable end-to-end examples diff --git a/tests/unit/cli-config-doctor.test.js b/tests/unit/cli-config-doctor.test.js index 956c927..e497ce0 100644 --- a/tests/unit/cli-config-doctor.test.js +++ b/tests/unit/cli-config-doctor.test.js @@ -238,6 +238,25 @@ test("whoami calls control introspection and prints platform compatibility", asy }); }); +test("whoami reports an unavailable namespace URL when control omits it", async () => { + await withTempDir(async (cwd) => { + /** @type {string[]} */ + const lines = []; + await runWhoamiCommand(["--ns", "acme", "--control-url", "http://ctl.test", "--token", "secret-token"], { + cwd, + env: {}, + stdout: (/** @type {string} */ line) => lines.push(line), + controlFetch: async () => response({ + ok: true, + principal: { kind: "ns", ns: "acme" }, + urls: { control: "http://ctl.test" }, + }), + }); + + assert.match(lines.join("\n"), /Namespace URL: \(unavailable\)/); + }); +}); + test("whoami text reports configured namespace mismatch", async () => { await withTempDir(async (cwd) => { /** @type {string[]} */ diff --git a/tests/unit/cli-deploy.test.js b/tests/unit/cli-deploy.test.js index a0eae43..656acf1 100644 --- a/tests/unit/cli-deploy.test.js +++ b/tests/unit/cli-deploy.test.js @@ -1616,6 +1616,7 @@ test("validateUnsupportedWranglerConfig rejects unmapped wrangler runtime/deploy "workers_dev", ]); for (const key of [ + "addresses", "agent_memory", "ai", "artifacts", @@ -1623,6 +1624,7 @@ test("validateUnsupportedWranglerConfig rejects unmapped wrangler runtime/deploy "cache", "cloudchamber", "compliance_region", + "dependencies_instrumentation", "hyperdrive", "first_party_worker", "flagship", @@ -1678,6 +1680,20 @@ test("validateUnsupportedWranglerConfig rejects unmapped wrangler runtime/deploy /unsupported Wrangler field "vectorize"/ ); + for (const [key, value] of /** @type {Array<[string, unknown]>} */ ([ + ["addresses", []], + ["dependencies_instrumentation", null], + ])) { + assert.throws( + () => validateUnsupportedWranglerConfig({ + name: "demo", + main: "src/index.js", + [key]: value, + }, null, "wrangler.toml"), + new RegExp(`unsupported Wrangler field "${key}"`) + ); + } + assert.throws( () => validateUnsupportedWranglerConfig({ name: "demo", @@ -1714,8 +1730,10 @@ test("validateUnsupportedWranglerConfig rejects unmapped wrangler runtime/deploy * @returns {unknown} */ function unsupportedWranglerFixtureValue(key, objectShapeKeys, booleanShapeKeys) { + if (key === "dependencies_instrumentation") return { enabled: false }; if (objectShapeKeys.has(key)) return { binding: "B" }; if (booleanShapeKeys.has(key)) return true; + if (key === "addresses") return ["support@example.com"]; if (key === "compliance_region") return "eu"; if (key === "pages_build_output_dir") return "dist"; return [{ binding: "B" }]; @@ -2160,7 +2178,7 @@ test("resolveWranglerCommand on win32 fails loudly when only a bare PATH .cmd sh } }); -test("wranglerChildEnv strips WDL control-plane environment", () => { +test("wranglerChildEnv scrubs control env and disables Wrangler network extras", () => { assert.deepEqual( wranglerChildEnv({ ADMIN_TOKEN: "secret", @@ -2171,11 +2189,15 @@ test("wranglerChildEnv strips WDL control-plane environment", () => { // export does not leak the control endpoint into the bundler. ADMIN_URL: "https://legacy-admin.example", CLOUDFLARE_API_TOKEN: "real-cloudflare-token", + WRANGLER_HIDE_BANNER: "false", + WRANGLER_SEND_METRICS: "true", PATH: "/bin", KEEP_ME: "ok", }), { CLOUDFLARE_API_TOKEN: "dry-run-dummy", + WRANGLER_HIDE_BANNER: "true", + WRANGLER_SEND_METRICS: "false", PATH: "/bin", KEEP_ME: "ok", } @@ -3452,6 +3474,49 @@ test("runDeployCommand explains control-rejected experimental compatibility flag } }); +test("runDeployCommand explains control-rejected unsupported compatibility flags", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-unsupported-flag-")); + try { + mkdirSync(path.join(dir, "src"), { recursive: true }); + writeFileSync(path.join(dir, "src", "index.js"), "export default {}"); + writeFileSync(path.join(dir, "wrangler.toml"), [ + 'name = "api"', + 'main = "src/index.js"', + 'compatibility_flags = ["legacy_error_serialization"]', + ].join("\n")); + + /** @type {RecordedFetch[]} */ + const fetchCalls = []; + await assert.rejects( + () => runDeployCommand([dir, "--ns", "demo", "--control-url", "http://ctl.test"], { + env: { ADMIN_TOKEN: "tok" }, + stdout: () => {}, + stderr: () => {}, + execFile: fakeWranglerExecFile, + controlFetch: async (/** @type {string} */ url, /** @type {import("../../lib/control-fetch.js").ControlFetchInit} */ init = {}) => { + fetchCalls.push({ url, init }); + return response({ + error: "compatibility_flag_unsupported", + message: "unsupported compatibility flag: legacy_error_serialization", + }, 400); + }, + }), + (err) => { + const message = /** @type {Error} */ (err).message; + assert.match(message, /compatibility_flag_unsupported/); + assert.match(message, /remove the unsupported compatibility flag/); + return true; + } + ); + + assert.equal(fetchCalls.length, 1); + const manifest = JSON.parse(/** @type {string} */ (fetchCalls[0].init.body)); + assert.deepEqual(manifest.compatibilityFlags, ["legacy_error_serialization"]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("runDeployCommand projects unknown deploy warnings before printing", async () => { const dir = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-warning-project-")); try { diff --git a/tests/unit/cli-lifecycle.test.js b/tests/unit/cli-lifecycle.test.js index 0303d41..7ca0999 100644 --- a/tests/unit/cli-lifecycle.test.js +++ b/tests/unit/cli-lifecycle.test.js @@ -14,6 +14,7 @@ import { runWorkersCommand, formatWorkersList } from "../../commands/workers.js" import { runWorkflowsCommand } from "../../commands/workflows.js"; import { main as wdlMain } from "../../bin/wdl.js"; import { CliError, readJsonOrFail } from "../../lib/common.js"; +import { formatWorkerDelete } from "../../lib/delete-format.js"; import { LONG_CONTROL_TIMEOUT_MS, UNLIMITED_CONTROL_BODY_BYTES, @@ -74,29 +75,39 @@ function ttyStdinLine(value) { return stdin; } -test("readJsonOrFail compacts structured control errors", async () => { +test("readJsonOrFail compacts redacted D1 lifecycle errors", async () => { const errBody = { error: "d1_database_initialize_failed", namespace: "demo", databaseId: "d1_test", - message: "D1 backend is unavailable: internal error; reference = ref-1", + message: "Internal error", upstreamCode: "backend-unavailable", upstreamCategory: "internal", upstreamRetryable: true, upstreamStatus: 503, - detail: { - success: false, - error: "backend-unavailable", - message: "D1 backend is unavailable: internal error; reference = ref-1", - category: "internal", - retryable: true, - }, }; await assert.rejects( () => readJsonOrFail(response(errBody, 503), "create d1 database"), { - message: "create d1 database failed: 503 d1_database_initialize_failed: D1 backend is unavailable: internal error; reference = ref-1 namespace=demo databaseId=d1_test upstreamCode=backend-unavailable upstreamCategory=internal upstreamRetryable=true upstreamStatus=503", + message: "create d1 database failed: 503 d1_database_initialize_failed: Internal error namespace=demo databaseId=d1_test upstreamCode=backend-unavailable upstreamCategory=internal upstreamRetryable=true upstreamStatus=503", + } + ); +}); + +test("readJsonOrFail omits nested details from compact control errors", async () => { + await assert.rejects( + () => readJsonOrFail(response({ + error: "d1_database_initialize_failed", + message: "Internal error", + upstreamCode: "backend-unavailable", + detail: { + message: "unredacted upstream detail", + internalReference: "ref-1", + }, + }, 503), "create d1 database"), + { + message: "create d1 database failed: 503 d1_database_initialize_failed: Internal error upstreamCode=backend-unavailable", } ); }); @@ -262,7 +273,13 @@ test("workers command lists namespace worker state", async () => { const body = { namespace: "demo", workers: [ - { name: "api", activeVersion: "v2", versions: ["v1", "v2"], hasSecrets: true }, + { + name: "api", + activeVersion: "v2", + versions: ["v1", "v2"], + hasSecrets: true, + hasWorkflowDefs: true, + }, ], }; const { calls, lines, deps } = mockDeps(body); @@ -272,7 +289,9 @@ test("workers command lists namespace worker state", async () => { assert.equal(calls.length, 1); assert.equal(calls[0].url, "http://ctl.test/ns/demo/workers"); assert.deepEqual(/** @type {import("../../lib/control-fetch.js").ControlFetchInit} */ (calls[0].init).headers, { "x-admin-token": "tok" }); - assert.deepEqual(lines, ["api\tactive=v2\tversions=v1,v2\tsecrets=yes"]); + assert.deepEqual(lines, [ + "api\tactive=v2\tversions=v1,v2\tsecrets=yes\tworkflow-defs=yes", + ]); }); test("workers command does not double-slash paths when CONTROL_URL has a trailing slash", async () => { @@ -305,35 +324,61 @@ test("workers command rejects unexpected positional arguments", async () => { }); test("wdl workers escapes control sequences from the control plane but keeps tab columns", async () => { + const hostile = `${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`; /** @type {string[]} */ const lines = []; await runWorkersCommand(["--ns", "demo", "--control-url", "http://ctl.test"], { env: { ADMIN_TOKEN: "tok" }, stdout: (/** @type {string} */ line) => lines.push(line), controlFetch: async () => response({ - workers: [{ name: "ev\u001bil", activeVersion: "v1", versions: ["v1"], hasSecrets: false }], + workers: [{ + name: `worker-${hostile}`, + activeVersion: `v2-${hostile}`, + versions: [`v1-${hostile}`], + hasSecrets: false, + }], }), }); const out = lines.join("\n"); - assert.ok(!out.includes("\u001b"), "raw ESC must not reach the terminal"); - assert.ok(out.includes("ev\\u001bil"), "worker name must be escaped"); - assert.ok(out.includes("\t"), "tab column separators must be preserved"); + assertNoRawTerminalControls(out, "workers output"); + assert.match(out, /worker-\\u001b\[2J\\nFORGED\\rBAD\\tCOLUMN\\u009b/); + assert.match(out, /active=v2-\\u001b\[2J\\nFORGED\\rBAD\\tCOLUMN\\u009b/); + assert.match(out, /versions=v1-\\u001b\[2J\\nFORGED\\rBAD\\tCOLUMN\\u009b/); + assert.equal(out.split("\t").length - 1, 4, "only formatter column separators may remain as raw tabs"); }); -test("formatWorkersList handles empty and deploy-only entries", () => { +test("formatWorkersList handles empty and workflow-definition-only entries", () => { assert.deepEqual(formatWorkersList({ workers: [] }), ["(no workers)"]); - // NOTE: lib/workers-format.js types `activeVersion` as `string | undefined`, - // but the control plane (and this test) sends `null` for an undeployed - // worker. `formatWorkersList` handles it (`w.activeVersion || "-"`); the - // typedef just omits `null`. Cast through the real param type so the test - // keeps exercising the null path without widening the lib type here. assert.deepEqual( - formatWorkersList(/** @type {Parameters[0]} */ ( - /** @type {unknown} */ ({ - workers: [{ name: "draft", activeVersion: null, versions: ["v1"], hasSecrets: false }], - }) - )), - ["draft\tactive=-\tversions=v1\tsecrets=no"] + formatWorkersList({ + workers: [ + { + name: "draft", + activeVersion: null, + versions: [], + hasSecrets: false, + hasWorkflowDefs: true, + }, + { + name: "legacy", + activeVersion: "v1", + versions: ["v1"], + hasSecrets: false, + }, + { + name: "empty", + activeVersion: null, + versions: [], + hasSecrets: false, + hasWorkflowDefs: false, + }, + ], + }), + [ + "draft\tactive=-\tversions=-\tsecrets=no\tworkflow-defs=yes", + "legacy\tactive=v1\tversions=v1\tsecrets=no\tworkflow-defs=unknown", + "empty\tactive=-\tversions=-\tsecrets=no\tworkflow-defs=no", + ] ); }); @@ -458,6 +503,7 @@ test("delete worker supports dry-run query and raw json output", async () => { affectedHosts: ["demo.workers.example"], queueConsumersRemoved: 1, hasWorkerSecrets: true, + hasWorkflowDefs: true, }; const { calls, lines, deps } = mockDeps(body); @@ -472,6 +518,37 @@ test("delete worker supports dry-run query and raw json output", async () => { assert.deepEqual(lines, [JSON.stringify(body, null, 2)]); }); +test("delete worker dry-run reports state presence without overstating deletion", () => { + const base = { + dryRun: true, + namespace: "demo", + name: "api", + activeDeleted: null, + versionsDeleted: [], + }; + assert.deepEqual( + formatWorkerDelete({ + ...base, + deleted: true, + hasWorkerSecrets: true, + hasWorkflowDefs: true, + }), + [ + "DRY RUN demo/api wouldDelete=yes active=- versions=-", + " worker secrets present", + " workflow definitions present", + ] + ); + assert.deepEqual( + formatWorkerDelete({ ...base, deleted: false, hasWorkflowDefs: false }), + ["DRY RUN demo/api wouldDelete=no active=- versions=-"] + ); + assert.deepEqual( + formatWorkerDelete({ ...base, deleted: false }), + ["DRY RUN demo/api wouldDelete=no active=- versions=-"] + ); +}); + test("delete worker dry-run renders workflow blockers in human output", async () => { const hostile = `bad${ESC}[2J\nFORGED\rBAD`; const body = { @@ -482,6 +559,8 @@ test("delete worker dry-run renders workflow blockers in human output", async () activeDeleted: `v2-${hostile}`, versionsDeleted: [`v1-${hostile}`], affectedHosts: [`host-${hostile}.example`], + hasWorkerSecrets: true, + hasWorkflowDefs: true, blockers: [{ version: `v1-${hostile}`, referrers: [{ @@ -511,6 +590,8 @@ test("delete worker dry-run renders workflow blockers in human output", async () assert.ok(lines.some((line) => /workflow blocker/.test(line))); assert.match(joined, /DRY RUN demo-bad\\u001b\[2J\\nFORGED\\rBAD\/api-bad\\u001b\[2J\\nFORGED\\rBAD/); assert.match(joined, /affected hosts: host-bad\\u001b\[2J\\nFORGED\\rBAD\.example/); + assert.match(joined, /worker secrets present/); + assert.match(joined, /workflow definitions present/); assert.match(joined, /binding=binding-bad\\u001b\[2J\\nFORGED\\rBAD/); assert.match(joined, /workflow_instances_active-bad\\u001b\[2J\\nFORGED\\rBAD/); assert.match(joined, /wf-bad\\u001b\[2J\\nFORGED\\rBAD instance=inst-bad\\u001b\[2J\\nFORGED\\rBAD/); @@ -594,6 +675,22 @@ test("delete command exposes only documented destructive subcommands", async () ); }); +test("delete worker help lists workflow definitions among deleted state", async () => { + /** @type {string[]} */ + const lines = []; + await runDeleteCommand(["worker", "--help"], { + env: {}, + stdout: (/** @type {string} */ line) => lines.push(line), + controlFetch: async () => { + throw new Error("controlFetch should not be called for help"); + }, + }); + assert.match( + lines.join("\n"), + /Delete a worker, its versions, secrets, workflow definitions, routes, and queue consumers\./ + ); +}); + test("delete command rejects unexpected positional arguments", async () => { const deps = { env: { ADMIN_TOKEN: "tok" }, From 3364c7b93ef6eec06a44ff766e0859aa37a2b27a Mon Sep 17 00:00:00 2001 From: Lu Zhang Date: Sun, 19 Jul 2026 09:45:00 +0800 Subject: [PATCH 2/3] Expose retired workflows and preserve local deploy URLs Show retired workflow definitions and document workflow_not_exported recovery. Derive local Worker URL scheme and port from CONTROL_URL while keeping CONTROL_CONNECT_HOST transport-only. Cover active and retired workflow output plus default and custom local ports. Signed-off-by: Lu Zhang --- .claude/skills/wdl-deploy/SKILL.md | 3 ++- CHANGELOG.md | 7 +++++-- GUIDE-zh.md | 4 ++++ GUIDE.md | 9 +++++++++ commands/deploy.js | 10 ++++++++-- docs/deploy-zh.md | 2 ++ docs/deploy.md | 4 ++++ docs/workflows-zh.md | 2 ++ docs/workflows.md | 5 +++++ lib/workflows-format.js | 5 +++-- tests/unit/cli-deploy.test.js | 14 +++++++------- tests/unit/cli-lifecycle.test.js | 30 +++++++++++++++++++++--------- 12 files changed, 72 insertions(+), 23 deletions(-) diff --git a/.claude/skills/wdl-deploy/SKILL.md b/.claude/skills/wdl-deploy/SKILL.md index 7a45037..db4eb9d 100644 --- a/.claude/skills/wdl-deploy/SKILL.md +++ b/.claude/skills/wdl-deploy/SKILL.md @@ -75,7 +75,8 @@ overrides the TCP target the admin token connects to (Host header + TLS SNI still track `CONTROL_URL`), and a stale value in a CI or production shell could route the token to an unintended host. A URL-form override uses its scheme only to choose the default TCP port; transport still follows `CONTROL_URL`. GUIDE -covers the details. +covers the details. Local deploy output also derives the public Worker scheme +and port from `CONTROL_URL`, never `CONTROL_CONNECT_HOST`. `wdl deploy` runs the project's Wrangler dry-run and build hooks as the user, so they can read the on-disk token store (`~/.config/wdl/credentials`); only deploy diff --git a/CHANGELOG.md b/CHANGELOG.md index 82795b3..720462b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ ### Changed - `wdl workers` now reports workflow-definition state (`unknown` when an older - control omits it), and `wdl delete worker --dry-run` reports workflow - definition presence. + control omits it), `wdl workflows list` marks retired definitions, and + `wdl delete worker --dry-run` reports worker-secret and workflow-definition + presence without overstating blocked deletion. +- Local deploy output now derives the Worker URL scheme and port from + `CONTROL_URL` instead of assuming `http://...:8080`. - Platform capability docs now cover the current compatibility-date, Durable Object RPC, Workflows, R2 metadata, secret-key, D1 error, and JSRPC contracts. - The packaged deploy toolchain now pins Wrangler 4.112.0 and uses diff --git a/GUIDE-zh.md b/GUIDE-zh.md index f7140fe..f5df309 100644 --- a/GUIDE-zh.md +++ b/GUIDE-zh.md @@ -222,6 +222,8 @@ cron / queue delivery 会在 `wdl tail` 里显示 start/finish 事件,包含 w 不要把用户访问 Worker 的流量打到 control URL。control URL 只用于部署和管理命令。 +对于本地开发 control host,deploy 摘要会复用 `CONTROL_URL` 的 scheme 和对外端口生成 Worker URL。`CONTROL_CONNECT_HOST` 仍只覆盖 control socket 的连接目标,不会改变输出的 Worker origin。 + 自定义域名和 Wrangler `routes` 目前还不是 tenant self-service 的 GA 能力。除非管理方明确为你的 namespace 开通自定义 host,否则使用默认 Worker URL: ```text @@ -482,6 +484,8 @@ wdl workflows restart api orders order-123 --yes wdl workflows terminate api orders order-123 --yes ``` +`wdl workflows list` 会把 active Worker version 不再导出的定义标为 `retired=yes`。既有实例仍可查看和 terminate,但 restart 会返回 `workflow_not_exported`;需要先部署一个重新导出该 workflow name 的 active version。 + 这是 WDL Workflows 支持,不是完整 Cloudflare Workflows parity。 `script_name`、跨 worker workflow、跨 worker callback、service-binding callback 和 Cloudflare source-AST visualizer 不支持。same-worker DO progress callback 和 runtime-observed parallel/DAG `step.do` execution 可用。 重要运行限制和编程规则: diff --git a/GUIDE.md b/GUIDE.md index fefecaa..d43ebe2 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -326,6 +326,10 @@ process is not consuming output fast enough. Do not send end-user Worker traffic to the control URL. The control URL is only for deployment and management commands. +For local-development control hosts, deploy summaries reuse `CONTROL_URL`'s +scheme and public port for the Worker URL. `CONTROL_CONNECT_HOST` remains only a +control socket override and does not change the printed Worker origin. + Custom domains and Wrangler `routes` are not generally available for tenant self-service yet. Use the default Worker URL unless your operator explicitly enables a custom host for your namespace: @@ -704,6 +708,11 @@ wdl workflows restart api orders order-123 --yes wdl workflows terminate api orders order-123 --yes ``` +`wdl workflows list` marks definitions absent from the active Worker version as +`retired=yes`. Existing instances remain inspectable and may be terminated, but +restart returns `workflow_not_exported` until an active version exports that +workflow name again. + This is WDL Workflows support, not full Cloudflare Workflows parity. `script_name`, cross-worker workflows, cross-worker callbacks, service-binding callbacks, and Cloudflare source-AST visualizer are not supported. Same-worker diff --git a/commands/deploy.js b/commands/deploy.js index 5f3cde4..103028d 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -304,8 +304,14 @@ async function runDeploy({ values, positionals, context: baseContext }) { const controlHost = new URL(controlUrl).hostname; const isLocal = isLocalDevHost(controlHost); if (isLocal) { - const host = `${ns}.${platformDomain || "workers.local"}`; - writeStatusLine(stdout, ` http://${host}:8080/${workerName}/`); + const workerUrl = new URL(controlUrl); + workerUrl.username = ""; + workerUrl.password = ""; + workerUrl.hostname = `${ns}.${platformDomain || "workers.local"}`; + workerUrl.pathname = `/${workerName}/`; + workerUrl.search = ""; + workerUrl.hash = ""; + writeStatusLine(stdout, ` ${workerUrl.href}`); } else if (platformDomain) { writeStatusLine(stdout, ` https://${ns}.${platformDomain}/${workerName}/`); } diff --git a/docs/deploy-zh.md b/docs/deploy-zh.md index 869d010..cda9ddd 100644 --- a/docs/deploy-zh.md +++ b/docs/deploy-zh.md @@ -52,6 +52,8 @@ https://.// Worker 看到的路径是**剥掉 `/` 之后的路径**。除非运维方明确启用,租户没有自定义路由能力;首次配置不要加 `route` / `routes`。 +对于本地开发 control host,deploy 摘要会复用 `CONTROL_URL` 的 scheme 和对外端口生成 Worker URL。`CONTROL_CONNECT_HOST` 只改变 control socket 的连接目标,不会改变输出的 Worker origin。 + ## 核心命令 | 目标 | 命令 | diff --git a/docs/deploy.md b/docs/deploy.md index 587d481..cb3c123 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -95,6 +95,10 @@ The Worker sees the path **with the `/` prefix stripped**. Tenants have no custom routing capability unless the operator explicitly enables it; do not add `route` / `routes` in a first-time setup. +For local-development control hosts, the deploy summary reuses `CONTROL_URL`'s +scheme and public port when constructing the Worker URL. `CONTROL_CONNECT_HOST` +changes only the control socket target and never the printed Worker origin. + ## Core commands | Goal | Command | diff --git a/docs/workflows-zh.md b/docs/workflows-zh.md index a9e8d56..d5691c6 100644 --- a/docs/workflows-zh.md +++ b/docs/workflows-zh.md @@ -44,6 +44,8 @@ wdl workflows terminate --yes `restart` 和 `terminate` 是破坏性实例生命周期操作;只有在已经独立确认 namespace、worker、workflow 和 instance id 后才传 `--yes`。 +`wdl workflows list` 会把 active Worker version 不再导出的定义标为 `retired=yes`。既有实例仍可查看和 terminate,但 restart 会返回 `workflow_not_exported`;需要先部署一个重新导出该 workflow name 的 active version。 + Workflows API 的语义大小限制会返回 `request_too_large`;HTTP body 解析阶段的大小限制可能返回 `request_body_too_large`。Workflows 5xx 表示平台或 backend 故障,响应体会保持通用错误摘要;底层诊断进入平台日志,不作为稳定 CLI 输出。`workflow_metadata_contention` 表示 control 读取期间 active workflow metadata 发生变化,重试命令即可。 ## 端到端示例 diff --git a/docs/workflows.md b/docs/workflows.md index d1e8191..5085d6e 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -74,6 +74,11 @@ wdl workflows terminate --yes `--yes` only after independently confirming the namespace, worker, workflow, and instance id. +`wdl workflows list` marks definitions absent from the active Worker version as +`retired=yes`. Existing instances remain inspectable and may be terminated, but +restart returns `workflow_not_exported` until an active version exports that +workflow name again. + Semantic size limits in the Workflows API return `request_too_large`; size limits hit during HTTP body parsing may return `request_body_too_large`. A Workflows 5xx means a platform or backend failure, and the response body stays a diff --git a/lib/workflows-format.js b/lib/workflows-format.js index da38b70..7b80d1c 100644 --- a/lib/workflows-format.js +++ b/lib/workflows-format.js @@ -2,10 +2,11 @@ * @typedef {object} WorkflowSummary * @property {string} [worker] * @property {string} [name] - * @property {string} [binding] + * @property {string | null} [binding] * @property {string} [className] * @property {string} [activeVersion] * @property {string} [workflowKey] + * @property {boolean} [retired] */ /** @@ -29,7 +30,7 @@ export function formatWorkflowList(body) { const workflows = Array.isArray(body?.workflows) ? body.workflows : []; if (workflows.length === 0) return ["(no workflows)"]; return workflows.map((entry) => - `${entry.worker}/${entry.name}\tbinding=${entry.binding || "-"}\tclass=${entry.className || "-"}\tactive=${entry.activeVersion || "-"}\tkey=${entry.workflowKey || "-"}` + `${entry.worker}/${entry.name}\tbinding=${entry.binding || "-"}\tclass=${entry.className || "-"}\tactive=${entry.activeVersion || "-"}\tkey=${entry.workflowKey || "-"}\tretired=${entry.retired ? "yes" : "no"}` ); } diff --git a/tests/unit/cli-deploy.test.js b/tests/unit/cli-deploy.test.js index 656acf1..75e8a14 100644 --- a/tests/unit/cli-deploy.test.js +++ b/tests/unit/cli-deploy.test.js @@ -2615,7 +2615,7 @@ test("runDeployCommand rejects non-object vars before bundling", async () => { } }); -test("runDeployCommand prints a direct http URL for a local deploy", async () => { +test("runDeployCommand preserves the local control scheme and port in the Worker URL", async () => { const dir = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-localurl-")); try { mkdirSync(path.join(dir, "src"), { recursive: true }); @@ -2626,9 +2626,9 @@ test("runDeployCommand prints a direct http URL for a local deploy", async () => const lines = []; let fetchCount = 0; await runDeployCommand( - [dir, "--ns", "demo", "--control-url", "http://localhost:8080"], + [dir, "--ns", "demo", "--control-url", "https://localhost:8443"], { - env: { ADMIN_TOKEN: "tok" }, + env: { ADMIN_TOKEN: "tok", CONTROL_CONNECT_HOST: "127.0.0.1:18080" }, stdout: (/** @type {string} */ line) => lines.push(/** @type {string} */ line), stderr: () => {}, execFile: (/** @type {string} */ _cmd, /** @type {readonly string[]} */ args) => { @@ -2646,7 +2646,7 @@ test("runDeployCommand prints a direct http URL for a local deploy", async () => } ); - assert.ok(lines.includes(" http://demo.workers.local:8080/api/"), "local deploy prints a direct http URL with the gateway port"); + assert.ok(lines.includes(" https://demo.workers.local:8443/api/")); assert.equal(lines.some((line) => line.includes("curl -H")), false, "no curl hint"); } finally { rmSync(dir, { recursive: true, force: true }); @@ -2694,7 +2694,7 @@ test("runDeployCommand detects local control by hostname only", async () => { } }); -test("runDeployCommand treats a .test control host as local (http URL, not https)", async () => { +test("runDeployCommand uses the default port from a local control URL", async () => { const dir = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-test-host-")); try { mkdirSync(path.join(dir, "src"), { recursive: true }); @@ -2726,8 +2726,8 @@ test("runDeployCommand treats a .test control host as local (http URL, not https ); assert.ok( - lines.includes(" http://demo.workers.local:8080/api/"), - "a .test control host prints the local http URL" + lines.includes(" http://demo.workers.local/api/"), + "a .test control host without an explicit port uses the http default" ); assert.equal(lines.some((line) => line.startsWith(" https://")), false, "no production https URL for a local deploy"); } finally { diff --git a/tests/unit/cli-lifecycle.test.js b/tests/unit/cli-lifecycle.test.js index 7ca0999..faa85bf 100644 --- a/tests/unit/cli-lifecycle.test.js +++ b/tests/unit/cli-lifecycle.test.js @@ -1475,14 +1475,25 @@ test("workflows commands call encoded control endpoints", async () => { calls.push({ url, init }); if (url.endsWith("/workflows")) { return response({ - workflows: [{ - worker: "api", - name: "orders", - binding: "ORDERS", - className: "OrderWorkflow", - activeVersion: "v2", - workflowKey: "wf_1234", - }], + workflows: [ + { + worker: "api", + name: "orders", + binding: "ORDERS", + className: "OrderWorkflow", + activeVersion: "v2", + workflowKey: "wf_1234", + }, + { + worker: "api", + name: "legacy", + binding: null, + className: "LegacyWorkflow", + activeVersion: "v2", + workflowKey: "wf_retired", + retired: true, + }, + ], }); } if (url.includes("/instances?")) { @@ -1520,7 +1531,8 @@ test("workflows commands call encoded control endpoints", async () => { assert.equal(calls[6].url, "http://ctl.test/ns/demo%20space/workflows/api/orders/instances/order%2F1/terminate"); assert.equal(calls[6].init.method, "POST"); assert.deepEqual(calls[0].init.headers, { "x-admin-token": "tok" }); - assert.ok(lines.includes("api/orders\tbinding=ORDERS\tclass=OrderWorkflow\tactive=v2\tkey=wf_1234")); + assert.ok(lines.includes("api/orders\tbinding=ORDERS\tclass=OrderWorkflow\tactive=v2\tkey=wf_1234\tretired=no")); + assert.ok(lines.includes("api/legacy\tbinding=-\tclass=LegacyWorkflow\tactive=v2\tkey=wf_retired\tretired=yes")); assert.ok(lines.includes("Next cursor: 1")); assert.ok(lines.includes("steps=1")); assert.equal(lines.at(-1), "OK demo space/api/orders/order/1 terminate status=paused"); From 81a87e8002e286e12f6e53e49e3931bb79b327bd Mon Sep 17 00:00:00 2001 From: Lu Zhang Date: Sun, 19 Jul 2026 11:08:31 +0800 Subject: [PATCH 3/3] Harden workflow output and clarify diagnostics Escape control-plane fields across workflow list, instance status, and lifecycle human output. Keep --json responses unchanged while routing lifecycle success lines through the single-line status writer. Clarify Wrangler update-check behavior and scope D1 5xx redaction docs to runtime/backend failures. Signed-off-by: Lu Zhang --- .claude/skills/wdl-deploy/SKILL.md | 6 ++- CHANGELOG.md | 6 ++- GUIDE-zh.md | 4 +- GUIDE.md | 16 +++--- commands/workflows.js | 15 ++++-- docs/d1-zh.md | 2 +- docs/d1.md | 10 ++-- docs/deploy-zh.md | 2 +- docs/deploy.md | 6 ++- lib/workflows-format.js | 21 +++++--- lib/wrangler/command.js | 4 +- tests/unit/cli-deploy.test.js | 2 +- tests/unit/cli-lifecycle.test.js | 80 ++++++++++++++++++++++++++++++ 13 files changed, 140 insertions(+), 34 deletions(-) diff --git a/.claude/skills/wdl-deploy/SKILL.md b/.claude/skills/wdl-deploy/SKILL.md index db4eb9d..ce222bd 100644 --- a/.claude/skills/wdl-deploy/SKILL.md +++ b/.claude/skills/wdl-deploy/SKILL.md @@ -63,8 +63,10 @@ WDL-only `[[exports]]`, `[[platform_bindings]]`, `[[triggers.schedules]]`, and `[[services]].ns` are parsed by the CLI and removed from Wrangler's temporary bundle config; other fields retain their existing Wrangler passthrough behavior. Wrangler's object-shaped declarative `exports` config is unsupported. -The dry-run child disables Wrangler's banner/update check and anonymous -telemetry; project build hooks retain their normal network access. +The dry-run child hides Wrangler's banner (and its normal update check) and +disables anonymous telemetry. Wrangler may still consult the configured npm +registry when reporting an unknown configuration field; project build hooks +retain their normal network access. For `[[services]]` and `[[exports]]`, read `docs/deploy.md`: tenant JSRPC may delegate service or Durable Object class stubs as opaque capabilities, but the receiver cannot rewrite their host-authored caller properties. Keep delegated diff --git a/CHANGELOG.md b/CHANGELOG.md index 720462b..4a54c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ jsonc-parser 3.3.1 and smol-toml 1.7.0. Unmapped Wrangler `addresses` and `dependencies_instrumentation` fields now fail loudly instead of being silently dropped. -- `wdl deploy` disables Wrangler's update check and anonymous telemetry for its - dry-run subprocess; project build hooks retain their normal network access. +- `wdl deploy` hides Wrangler's banner (and its normal update check) and + disables anonymous telemetry for its dry-run subprocess. Wrangler can still + consult the configured npm registry when explaining unknown config fields; + project build hooks retain their normal network access. ### Fixed diff --git a/GUIDE-zh.md b/GUIDE-zh.md index f5df309..98eef5d 100644 --- a/GUIDE-zh.md +++ b/GUIDE-zh.md @@ -148,7 +148,7 @@ APP_NAME = "hello" Control 会拒绝早于 `2026-04-01` 的显式日期、无效或未来日期,以及超出 bundled workerd 支持范围的日期。上游 experimental enable flags、`legacy_error_serialization` 和 `allow_irrevocable_stub_storage` 不受支持。这些校验由 control canonical 持有,CLI 不复制 workerd flag table。 -你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。WDL 会关闭这个 dry-run 子进程的 Wrangler banner、更新检查和匿名遥测;项目 build hook 仍保留正常的网络访问能力。 +你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查)并关闭匿名遥测。Wrangler 在报告未知配置字段时仍可能访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 如果同时存在多个 Wrangler 配置文件,WDL 跟随 Wrangler 的优先级:`wrangler.json`,然后 `wrangler.jsonc`,最后 `wrangler.toml`。 @@ -411,7 +411,7 @@ D1 运行时请求在执行前有边界:binary query body 最大 8 MiB;解 `wdl d1 migrations status/apply` 走 control-plane JSON request parser,所以请求体上限是 1 MiB。特别大的 migration 集合或 SQL 文件应拆成更小批次再 apply。 -Control-plane D1 lifecycle API 对外返回 5xx 时会把消息收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。对外 4xx SQL / migration failure 仍可保留可操作的诊断。 +Control-plane D1 lifecycle API 会把 D1 runtime/backend 转发来的 5xx failure 收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。Control 自己生成的 contention / collision 503 response 仍保留可操作消息;对外 4xx SQL / migration failure 也可保留可操作诊断。 `examples/d1-demo` 提供了一个最小 visitor counter 示例,包含 D1 binding 和 forward-only migration。 diff --git a/GUIDE.md b/GUIDE.md index d43ebe2..f5ada40 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -236,8 +236,10 @@ platform, use `wdl deploy` instead. The deploy command runs wrangler, then `PATH`. TypeScript, module resolution, esbuild bundling, and related build behavior still follow Wrangler. -WDL disables Wrangler's banner/update check and anonymous telemetry for this -dry-run subprocess. Project build hooks retain their normal network access. +WDL hides Wrangler's banner (which skips the normal banner update check) and +disables anonymous telemetry for this dry-run subprocess. Wrangler may still +consult the configured npm registry when reporting an unknown configuration +field. Project build hooks retain their normal network access. When several Wrangler config files exist, WDL follows Wrangler's priority: `wrangler.json`, then `wrangler.jsonc`, then `wrangler.toml`. @@ -617,10 +619,12 @@ call are rolled back. its request body is capped at 1 MiB. Split very large migration sets or SQL files into smaller batches before applying. -Control-plane D1 lifecycle APIs redact outward 5xx errors to `Internal error` -and retain only bounded machine classifiers (`upstreamCode`, -`upstreamCategory`, `upstreamRetryable`, and `upstreamStatus`). Outward 4xx SQL -and migration failures can retain actionable diagnostics. +Control-plane D1 lifecycle APIs redact 5xx failures forwarded from the D1 +runtime/backend to `Internal error` and retain only bounded machine classifiers +(`upstreamCode`, `upstreamCategory`, `upstreamRetryable`, and +`upstreamStatus`). Control-generated contention and collision 503 responses +retain actionable messages. Outward 4xx SQL and migration failures can retain +actionable diagnostics. See `examples/d1-demo` for a minimal visitor counter using D1 plus a forward-only migration. diff --git a/commands/workflows.js b/commands/workflows.js index 90ded62..1257ea0 100644 --- a/commands/workflows.js +++ b/commands/workflows.js @@ -1,7 +1,12 @@ import { defineCommand } from "../lib/command.js"; import { CliError, defineCliOption, formatHelp, isMain, optionHelp, unexpectedArgument } from "../lib/common.js"; import { confirmAction } from "../lib/stdin.js"; -import { escapeTerminalText, writeResult } from "../lib/output.js"; +import { + escapeTerminalText, + writeJsonOr, + writeResult, + writeStatusLine, +} from "../lib/output.js"; import { formatInstanceList, formatInstanceStatus, @@ -97,9 +102,11 @@ async function runWorkflows({ values, positionals, context }) { { method: "POST", headers }, `${subcommand} workflow instance`, )); - writeResult(Boolean(values.json), body, () => [ - `OK ${ns}/${worker}/${workflow}/${body.id || instanceId} ${subcommand} status=${body.status || "-"}`, - ], stdout); + if (writeJsonOr(Boolean(values.json), body, stdout)) return; + writeStatusLine( + stdout, + `OK ${ns}/${worker}/${workflow}/${body.id || instanceId} ${subcommand} status=${body.status || "-"}` + ); return; } diff --git a/docs/d1-zh.md b/docs/d1-zh.md index 3533cbe..c108d7d 100644 --- a/docs/d1-zh.md +++ b/docs/d1-zh.md @@ -113,7 +113,7 @@ D1 请求在执行前会被限流:binary query body 最大 8 MiB;解码后 D1 migration 管理走 control-plane JSON request parser,所以 `wdl d1 migrations status/apply` 请求体上限是 1 MiB。特别大的 migration 集合或 SQL 文件应拆成更小批次再 apply。`d1 execute --file` 的路径必须存在、可读,并且留在项目根目录内;文件缺失或不可读时 CLI 会在本地拒绝,不会联系 control。 -Control-plane D1 lifecycle API 对外返回 5xx 时会把消息收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。对外 4xx SQL / migration failure 仍可保留可操作的 D1 诊断。 +Control-plane D1 lifecycle API 会把 D1 runtime/backend 转发来的 5xx failure 收敛为 `Internal error`,只保留有界的 machine classifier:`upstreamCode`、`upstreamCategory`、`upstreamRetryable` 和 `upstreamStatus`。Control 自己生成的 contention / collision 503 response 仍保留可操作消息;对外 4xx SQL / migration failure 也可保留可操作的 D1 诊断。 以 `_cf_` 开头的 SQLite object name 是 workerd 保留名,大小写不敏感。不要创建或 `RENAME TO` 到 `_cf_*` 形式的应用 table、index、trigger 或 view;包含这类 DDL 的 migration 在新数据库上可能失败。已经 apply 的 migration 文件不要回改;需要修正时新增 forward migration,把应用数据迁到非保留名称。 diff --git a/docs/d1.md b/docs/d1.md index 0c0562b..c37bcbf 100644 --- a/docs/d1.md +++ b/docs/d1.md @@ -138,10 +138,12 @@ large migration sets or large SQL files into smaller batches before applying. project root; the CLI rejects missing or unreadable files before contacting control. -Control-plane D1 lifecycle APIs redact outward 5xx failures to `Internal error` -and expose only bounded machine classifiers (`upstreamCode`, -`upstreamCategory`, `upstreamRetryable`, and `upstreamStatus`). Outward 4xx SQL -and migration failures can retain actionable D1 diagnostics. +Control-plane D1 lifecycle APIs redact 5xx failures forwarded from the D1 +runtime/backend to `Internal error` and expose only bounded machine classifiers +(`upstreamCode`, `upstreamCategory`, `upstreamRetryable`, and +`upstreamStatus`). Control-generated contention and collision 503 responses +retain actionable messages. Outward 4xx SQL and migration failures can retain +actionable D1 diagnostics. SQLite object names beginning with `_cf_` are reserved by workerd, case-insensitively. Do not create or rename application tables, indexes, diff --git a/docs/deploy-zh.md b/docs/deploy-zh.md index cda9ddd..7575e2b 100644 --- a/docs/deploy-zh.md +++ b/docs/deploy-zh.md @@ -6,7 +6,7 @@ wrangler 解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。默认不会临时 `npx --yes wrangler` 拉包;只有设置 `WDL_ALLOW_NPX_WRANGLER=1` 时才允许这个 fallback。 -WDL 会关闭这个 dry-run 子进程的 Wrangler banner、更新检查和匿名遥测;项目 build hook 仍保留正常的网络访问能力。 +WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查)并关闭匿名遥测。Wrangler 在报告未知配置字段时仍可能访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 ## CLI 调用形式 diff --git a/docs/deploy.md b/docs/deploy.md index cb3c123..ee58bf5 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -12,8 +12,10 @@ wrangler, the CLI package's local wrangler, then `PATH`. By default there is no transient `npx --yes wrangler` fetch; that fallback is allowed only when `WDL_ALLOW_NPX_WRANGLER=1` is set. -WDL disables Wrangler's banner/update check and anonymous telemetry for this -dry-run subprocess. Project build hooks retain their normal network access. +WDL hides Wrangler's banner (which skips the normal banner update check) and +disables anonymous telemetry for this dry-run subprocess. Wrangler may still +consult the configured npm registry when reporting an unknown configuration +field. Project build hooks retain their normal network access. ## CLI invocation forms diff --git a/lib/workflows-format.js b/lib/workflows-format.js index 7b80d1c..fd292f5 100644 --- a/lib/workflows-format.js +++ b/lib/workflows-format.js @@ -1,3 +1,5 @@ +import { escapeTerminalText, formatDiagnosticValue } from "./output.js"; + /** * @typedef {object} WorkflowSummary * @property {string} [worker] @@ -22,6 +24,11 @@ * @property {string} status */ +/** @param {unknown} value */ +function field(value) { + return value == null || value === "" ? "-" : escapeTerminalText(value); +} + /** * @param {{ workflows?: WorkflowSummary[] } | null | undefined} body * @returns {string[]} @@ -30,7 +37,7 @@ export function formatWorkflowList(body) { const workflows = Array.isArray(body?.workflows) ? body.workflows : []; if (workflows.length === 0) return ["(no workflows)"]; return workflows.map((entry) => - `${entry.worker}/${entry.name}\tbinding=${entry.binding || "-"}\tclass=${entry.className || "-"}\tactive=${entry.activeVersion || "-"}\tkey=${entry.workflowKey || "-"}\tretired=${entry.retired ? "yes" : "no"}` + `${field(entry.worker)}/${field(entry.name)}\tbinding=${field(entry.binding)}\tclass=${field(entry.className)}\tactive=${field(entry.activeVersion)}\tkey=${field(entry.workflowKey)}\tretired=${entry.retired ? "yes" : "no"}` ); } @@ -42,8 +49,8 @@ export function formatInstanceList(body) { const instances = Array.isArray(body?.instances) ? body.instances : []; const lines = instances.length === 0 ? ["(no workflow instances)"] - : instances.map((entry) => `${entry.id}\tstatus=${entry.status || "-"}`); - if (body?.cursor) lines.push(`Next cursor: ${body.cursor}`); + : instances.map((entry) => `${field(entry.id)}\tstatus=${field(entry.status)}`); + if (body?.cursor) lines.push(`Next cursor: ${escapeTerminalText(body.cursor)}`); return lines; } @@ -58,18 +65,18 @@ export function formatInstanceList(body) { * @returns {string[]} */ export function formatInstanceStatus(body) { - const lines = [`${body.id || "-"}\tstatus=${body.status || "-"}`]; + const lines = [`${field(body.id)}\tstatus=${field(body.status)}`]; if (body.output !== undefined && body.output !== null) { - lines.push(`output=${JSON.stringify(body.output)}`); + lines.push(`output=${formatDiagnosticValue(body.output)}`); } if (body.error !== undefined && body.error !== null) { - lines.push(`error=${JSON.stringify(body.error)}`); + lines.push(`error=${formatDiagnosticValue(body.error)}`); } if (Array.isArray(body.steps?.entries)) { const suffix = body.steps.truncated ? " (truncated)" : ""; lines.push(`steps=${body.steps.entries.length}${suffix}`); for (const step of body.steps.entries) { - lines.push(` #${step.ordinal} ${step.name} status=${step.status}`); + lines.push(` #${escapeTerminalText(step.ordinal)} ${field(step.name)} status=${field(step.status)}`); } } return lines; diff --git a/lib/wrangler/command.js b/lib/wrangler/command.js index 61487f0..18723fa 100644 --- a/lib/wrangler/command.js +++ b/lib/wrangler/command.js @@ -129,8 +129,8 @@ export function wranglerChildEnv(env) { const childEnv = { ...env, CLOUDFLARE_API_TOKEN: "dry-run-dummy", - // WDL captures Wrangler's banner and only needs local bundle output. Avoid - // its npm update check and anonymous telemetry during this subprocess. + // WDL captures Wrangler's banner and only needs local bundle output. Hide + // the banner (and its normal update check) and disable anonymous telemetry. WRANGLER_HIDE_BANNER: "true", WRANGLER_SEND_METRICS: "false", }; diff --git a/tests/unit/cli-deploy.test.js b/tests/unit/cli-deploy.test.js index 75e8a14..8c798da 100644 --- a/tests/unit/cli-deploy.test.js +++ b/tests/unit/cli-deploy.test.js @@ -2178,7 +2178,7 @@ test("resolveWranglerCommand on win32 fails loudly when only a bare PATH .cmd sh } }); -test("wranglerChildEnv scrubs control env and disables Wrangler network extras", () => { +test("wranglerChildEnv scrubs control env, hides Wrangler's banner, and disables telemetry", () => { assert.deepEqual( wranglerChildEnv({ ADMIN_TOKEN: "secret", diff --git a/tests/unit/cli-lifecycle.test.js b/tests/unit/cli-lifecycle.test.js index faa85bf..c377cc8 100644 --- a/tests/unit/cli-lifecycle.test.js +++ b/tests/unit/cli-lifecycle.test.js @@ -19,6 +19,11 @@ import { LONG_CONTROL_TIMEOUT_MS, UNLIMITED_CONTROL_BODY_BYTES, } from "../../lib/control-fetch.js"; +import { + formatInstanceList, + formatInstanceStatus, + formatWorkflowList, +} from "../../lib/workflows-format.js"; import { ESC, assertNoRawTerminalControls, mockDeps, response } from "./helpers.js"; /** @typedef {import("./helpers.js").ControlCall} ControlCall */ @@ -1538,6 +1543,81 @@ test("workflows commands call encoded control endpoints", async () => { assert.equal(lines.at(-1), "OK demo space/api/orders/order/1 terminate status=paused"); }); +test("workflow formatters escape control fields but preserve their own layout", () => { + const hostile = `${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`; + const lines = [ + ...formatWorkflowList({ + workflows: [{ + worker: hostile, + name: hostile, + binding: hostile, + className: hostile, + activeVersion: hostile, + workflowKey: hostile, + retired: true, + }], + }), + ...formatInstanceList({ + instances: [{ id: hostile, status: hostile }], + cursor: hostile, + }), + ...formatInstanceStatus({ + id: hostile, + status: hostile, + output: { value: hostile }, + error: { message: hostile }, + steps: { + entries: [{ ordinal: 0, name: hostile, status: hostile }], + }, + }), + ]; + const out = lines.join("\n"); + + assertNoRawTerminalControls(out, "workflow formatter output"); + assert.ok(out.includes("\\u001b[2J\\nFORGED\\rBAD\\tCOLUMN\\u009b")); + assert.equal( + out.split("\t").length - 1, + 7, + "only formatter-owned column separators may remain as raw tabs" + ); +}); + +test("workflow lifecycle status lines escape control fields and preserve JSON", async () => { + const hostile = `${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`; + const body = { id: `id-${hostile}`, status: `status-${hostile}` }; + const human = mockDeps(body); + + await runWorkflowsCommand([ + "pause", + "api", + "orders", + "instance", + "--ns", + "demo", + "--control-url", + "http://ctl.test", + ], human.deps); + + assert.equal(human.lines.length, 1); + assertNoRawTerminalControls(human.lines[0], "workflow lifecycle status"); + assert.ok(human.lines[0].includes("id-\\u001b[2J\\nFORGED\\rBAD\\tCOLUMN\\u009b")); + assert.equal(human.lines[0].includes("\t"), false, "status lines must not preserve raw tabs"); + + const json = mockDeps(body); + await runWorkflowsCommand([ + "pause", + "api", + "orders", + "instance", + "--ns", + "demo", + "--control-url", + "http://ctl.test", + "--json", + ], json.deps); + assert.deepEqual(JSON.parse(json.lines[0]), body); +}); + test("workflows list accepts flags before the subcommand", async () => { const { calls, lines, deps } = mockDeps({ workflows: [] });