---
name: yudefine-deploy
description: "Use when 使用者說「部署到 YuDefine」「deploy to yudefine」、要求把專案放到 yudefine.com.tw。支援 void.cloud 或自有 Cloudflare 帳號。NOT for BigByte。"
---
<!-- 🔒 LOCKED — managed by clade · auto-generated by sync-to-codex; edit source in .claude/ then re-run sync -->

# YuDefine 部署（void.cloud / 自有 CF 雙軌）

YuDefine fleet 內**兩條合法 deploy track**，本 skill 兩條都支援。**MUST** Step 0 詢問使用者該專案走哪條，再執行對應 Step 1+。

> **本 skill 只規範 YuDefine fleet 政策層**（compat flags hard rule、yudefine.com.tw zone DNS、CF token 來源、fleet consumer table、fleet-specific 踩坑）。
>
> **void.cloud 平台 SOP**（CLI 命令、`env.ts`、binding helper API、自動 migration、resource inference 等通用知識）走 **official `void` skill**（`.agents/skills/void/`）或 **`void mcp`**（`search_docs` / `get_page` / `list_pages`），與 `void` npm package version lockstep。

## Step 0 — 詢問 deploy target（**必跑**）

向使用者用 `request_user_input` 確認 deploy target，不要從專案名 / fleet 慣例直接推斷：

| 選項 | 適用 | Track |
|---|---|---|
| **(A) void.cloud** | 想用 void / VoidZero 平台（一鍵 deploy、自動 provision D1/R2、內建 drizzle / migration / log / domain CLI、無需自己管 CF 帳號 binding） | 走下方 Track A |
| **(B) 自有 Cloudflare 帳號** | 想用 `cloudflare/wrangler-action@v4` GitHub Actions 部署到自家 CF account（D1/R2 自管、wrangler.jsonc binding 明確、用 GitHub Secrets 對接） | 走下方 Track B |

**MUST** 把選項的取捨給使用者：

- (A) **void.cloud trade-off**：少寫 CI workflow、少管 binding，但鎖住 void 平台 + Vite+ 工具鏈 + 自訂網域走 cname.void.app
- (B) **自有 CF trade-off**：完全控制 binding / wrangler config / deploy 流程，但要自己寫 GitHub Actions + secrets sync + D1/R2 provisioning

**Fleet 現況**（只是 reference，**不**是限制；2026-07-14 更新）：

- void.cloud（active）：
  - `yudefine-blog`（apex `yudefine.com.tw`）— current `void@0.10.x`、GitHub OIDC、無 legacy deploy patch
  - `co-purchase`（`co-purchase.void.app`，未設自訂網域）— legacy `void@0.8.x`；升 current 後必須退役 patch 與長效 token
  - `quotation-generator`（`quotation.yudefine.com.tw`）— legacy `@void-sdk/void@0.6.x`，待獨立 migration
- 自有 CF（wrangler-action）：
  - `perno` / `nuxt-edge-agentic-rag` / `yuntech-usr-sroi` / `TDMS` / `nuxt-supabase-starter`
  - **`rental-scout`**（@nuxthub/core + wrangler-action；2026-05-27 仍未遷 void）

> 同一個 YuDefine 專案**可以**從 (B) 遷到 (A) 或反向 — 走 § 遷移 段。

## 觸發條件

使用者提到「部署到 YuDefine」、「deploy to yudefine」、要 setup 任何 YuDefine fleet 專案的 deploy pipeline、或要求把專案放到 `yudefine.com.tw`。

---

# Track A — void.cloud（YuDefine fleet 配置）

## A.1 Setup — 取得 official void skill + MCP

```bash
npx void init --agents
```

`void init --agents` 會：

- symlink `.agents/skills/void` + `.agents/skills/migrate-vite-cloudflare-to-void`（跟 `void` package version 對齊）
- 寫 `void mcp` 進 `.claude/settings.json`（offline docs query：`list_pages` / `get_page` / `search_docs`）
- patch `AGENTS.md` 加 Void instructions section
- patch `.gitignore` 加 Void defaults
- 自動 patch `nuxt.config.ts` / `vite.config.ts` 加 `voidPlugin()`（若缺）

> ⚠️ **不是 `pnpm dlx` / `pnpm add` 預裝再 init** — 用 `npx` 走最新 void package，避免本機 cache stale。

## A.2 後續操作走 official skill / MCP（hard rule）

**MUST**：

- 所有 void CLI 命令、config 細節、API surface、runtime helper、resource inference、auth flow、env.ts 等問題 **MUST** 走以下任一：
  - `.agents/skills/void/SKILL.md` + `docs/**/*.md`（symlink 跟 void package version 對齊）
  - `void mcp` query (`list_pages` / `get_page docs/<path>.md` / `search_docs <keyword>`)
- 從 Vite + `@cloudflare/vite-plugin` 遷 void：走 `.agents/skills/migrate-vite-cloudflare-to-void/`

**NEVER**：

- **NEVER** 從本檔複製 CLI 命令當權威 — 本檔不維護 CLI cache，official skill 跟 void package version lockstep
- **NEVER** 假設「之前可以這樣跑」就還能跑 — void CLI flag drift 是已知風險，每次跑前查 docs

## A.3 YuDefine Fleet 政策層

### A.3.1 compat flags hard rule

Current void 的 compatibility settings **MUST** 先查 official Nuxt integration，並與 `cloudflare-workers.md` § 3.2 對齊。以下配置 1/2/3 規則只適用 legacy `void@0.8.x`：

- **`appType: "framework"` (Nuxt / SvelteKit / Astro 等 — fleet 多數)**：**MUST** 配置 3（`["nodejs_compat", "nodejs_als", "no_nodejs_compat_v2"]`）
  - 配置 2（純 v2）對 Nitro `cloudflare-module` preset **不可用** — Nitro build 主動 warn「`Please consider replacing nodejs_compat_v2 with nodejs_compat ... or USE IT AT YOUR OWN RISK as it can cause issues with nitro`」+ 撞 `Cannot read private member #t in get stdout`（yudefine-blog 2026-05-27 first-ever CI deploy 實證；blog 之前 prod live 是 user 本機 manual deploy 沒踩到）
- **`appType: "void"` (pure Vite+ void app，fleet 少見)**：配置 2 可用（無 Nitro 中間層、可直吃 workerd 原生 v2）；配置 3 也行
- **禁配置 1**（`["nodejs_compat", "nodejs_als"]` 不含 no_v2）— 必撞 err 10021 `Cannot read private member #t ... in get stdout`

詳見 [`cloudflare-workers.md` § 3.2](../../../../rules/core/cloudflare-workers.md)、[`void 0.8 compat flags pitfall`](../../../../docs/pitfalls/2026-05-25-void-cloud-voidjson-compat-flags-10021.md)。

> Legacy 0.8 曾只採用 void.json flags；current void 可採用 wrangler compatibility settings，不得沿用「永遠不讀 wrangler」的舊假設。

### A.3.2 `compatibility_date` 對齊

`void.json` + `wrangler.jsonc` 的 `compatibility_date` **MUST** 對齊 void Nuxt 官方範例（`.agents/skills/void/docs/integrations/frameworks/nuxt.md`）。2026-07-14 驗證的官方範例為 `2026-02-24`。

**MUST** 兩個檔對齊（void.json 是 deploy 真相層、wrangler.jsonc 是 dev binding 對齊用）。

### A.3.3 current void 強制

YuDefine fleet 新 consumer **MUST** 使用 current `void@0.10.x`（每次實作前再以 npm / official docs 驗證 latest），**禁用** legacy `@void-sdk/void@0.6.x` 與 `void@0.8.x`。

### A.3.4 Vite+ override

`pnpm-workspace.yaml` **MUST** override `vite` / `vitest` 成 VoidZero fork（voidPlugin 需要 `parseSync` export，純 vite 沒有）：

```yaml
catalog:
  vite: npm:@voidzero-dev/vite-plus-core@^0.1.22
  vite-plus: 0.1.22
  vitest: npm:@voidzero-dev/vite-plus-test@^0.1.22
overrides:
  vite: 'catalog:'
  vitest: 'catalog:'
peerDependencyRules:
  allowAny: [vite, vitest]
  allowedVersions: { vite: '*', vitest: '*' }
```

### A.3.5 NuxtHub gating

void.cloud + D1 **MUST NOT** 帶 `@nuxthub/core`（per `cloudflare-workers.md` § 1 矩陣第三列）— 用 `void/db` + `void/storage` 不用 NuxtHub helper。

### A.3.6 legacy `patch-void-deploy.ts` 退役規則

`void@0.8.x` 的 SQLite migration handler 走 `copyFileSync` 不 bundle deps（`deploy-OPo_tSWl.mjs:1994`，對比 postgres handler 走 `bundlePgMigrationHandler` rollup bundle），handler 內 `import "../canonical-json-XXX.mjs"` 指向沒被 emit + 也不在 worker upload set 的 path → CF Workers 撞 10021 internal error（per [void-sdk/void#52](https://github.com/void-sdk/void/issues/52)）。

只有仍停在 legacy `void@0.8.x` 的 consumer 可暫時安裝 monkey-patch 繞掉：

```bash
cp ~/offline/clade/vendor/snippets/cloudflare-workers/patch-void-deploy.ts scripts/patch-void-deploy.ts
```

`package.json` postinstall：

```jsonc
"scripts": {
  "postinstall": "nuxt prepare && node scripts/patch-void-deploy.ts"
}
```

Upstream void-sdk/void#52 已於 2026-05-27 關閉並在當日 release 修正。current void consumer **MUST NOT** 保留此 script、postinstall hook 或 unenv `patchedDependencies`。

### A.3.7 `package.json` scripts naming（pnpm 保留字陷阱）

**禁**把 deploy 命令命名 `deploy`：

```jsonc
// ❌ 錯誤
"scripts": { "deploy": "void deploy" }

// 結果：pnpm 把 `deploy` 當保留字（workspace deploy 命令），
// 跑 `pnpm deploy` 撞 ERR_PNPM_NOTHING_TO_DEPLOY，不會觸發 script
```

**MUST** 用 `void:deploy`（或其他帶 prefix 的 name）：

```jsonc
"scripts": {
  "void:deploy": "void deploy",
  "void:env:check": "void env check"
}
```

CI workflow / chat invocation 一律走 `pnpm run void:deploy` 或 `npx void deploy`。

> Pitfall：`docs/pitfalls/2026-05-27-pnpm-deploy-reserved-word.md`（2026-05-27 新增）

## A.4 yudefine.com.tw 自訂網域 DNS

```bash
void domain add <hostname> --project <project>   # 印出要設的 DNS 記錄
```

void 回傳 3 類記錄：**CNAME（流量）** → `cname.void.app`、**TXT `_cf-custom-hostname`**（所有權）、**TXT `_acme-challenge`**（SSL 驗證，可能 2 筆）。

yudefine.com.tw zone 在 **Cloudflare**（NS `*.ns.cloudflare.com`）。設記錄走 CF API（見下方 token）：

- **子網域**（如 `rent.yudefine.com.tw`）：CNAME → `cname.void.app`，**proxied=false（DNS-only / grey cloud）**。CF-for-SaaS 跨 CF 帳號 proxied 會觸發 1014「CNAME Cross-User Banned」，務必 grey。
- **apex**（`yudefine.com.tw`）：一樣 CNAME @ → `cname.void.app` **proxied=false**；Cloudflare **CNAME flattening** 會壓成 A 記錄，**與既有 MX / SPF / TXT 共存**（郵件不受影響，實測 yudefine.com.tw 的 Zoho MX 不動）。
- 三筆 TXT 照 void 給的值建。apex 因 flatten 後對外是 A（非 CNAME），CF-for-SaaS 改靠 `_cf-custom-hostname` TXT **pre-validation** 驗所有權。

建完 `void domain status <hostname>` 追蹤：`pending` → `issuing_cert`（CF 簽 SSL，數分鐘）→ `active`。

CF API 建記錄範例（DNS-only CNAME）：

```bash
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE}/dns_records" \
  -H "Authorization: Bearer ${CF_DNS_TOKEN}" -H "Content-Type: application/json" \
  -d '{"type":"CNAME","name":"<host>","content":"cname.void.app","proxied":false,"ttl":1}'
```

## A.5 Cloudflare 帳號 / token（僅自訂網域 DNS 需要）

固定值（非 secret）：

```bash
CLOUDFLARE_ACCOUNT_ID=0eac599c12df10586d97a78179b9f11f
CLOUDFLARE_ZONE_ID=ffc8f1d16b67f9b1676f9dc7c782d4a5   # yudefine.com.tw
```

**Token（從 Notion `🔑 Scrects` 取）**：建 DNS 記錄需要 **Zone `DNS:Edit`** 權限。

- ✅ **用 `cfat_*`**（Notion「YuDefine - for vite-plugin-cloudflare-tunnel」）— 含 Account `Cloudflare Tunnel:Edit` + Zone `SSL:Edit` + **Zone `DNS:Edit`**。建 void 自訂網域 DNS 用這顆。
- ❌ **不要用 `cfut_*`**（「for Worker 通用」）— scope 只有 Workers/KV/R2/D1/Pages，**沒有 Zone DNS:Edit**，建 DNS 會失敗。

```
notion-fetch({ id: "https://www.notion.so/yudefine/Scrects-32fb791117218009a3d4deddf6364828" })
```

> void.cloud 部署本身**不需要** CF token（走 `void auth`）。CF token 只在「設自訂網域 DNS」這步用到。

## A.6 觀測 / 維運

走 `void mcp` 或 official `void` skill 查詢：

- 運行 log：`void project logs` / `void mcp` query
- 部署歷史 / 回滾：`void project rollback`
- Secrets / Domains / Migrations：CLI 對應命令見 `docs/reference/cli.md`

## A.7 Track A 注意事項速查（fleet-specific）

- **legacy 0.8 err 10021 `private member #t`** → 配置 1 或 framework 配置 2 的歷史相容性問題；current void 先查 official docs，不直接套舊 workaround
- **legacy 0.8 err 10021 純 internal error（無 stack）** → 可能是已修正的 SQLite handler emit bug；優先升 current void 並移除 patch，不在 current consumer 新裝 workaround
- **`pnpm deploy` 撞 `ERR_PNPM_NOTHING_TO_DEPLOY`** → scripts.deploy 改名 `void:deploy`（A.3.7）
- **`'vite' does not provide an export named 'parseSync'`** → 沒套 Vite+ override（A.3.4）
- **建 DNS 403 / 權限不足** → 用錯 token，要 `cfat_*` 不是 `cfut_*`（A.5）
- **自訂網域 1014 CNAME Cross-User Banned** → CNAME 設成 proxied，要改 DNS-only（A.4）
- **`void domain status` 卡 error「does not CNAME to this zone」** → apex 正常現象（flatten 成 A），靠 `_cf-custom-hostname` TXT pre-validation
- **drift-check `install latest drizzle-orm`** → void 平台層 pnpm temp-copy 問題（official skill `docs/guide/database.md` 有 SOP）

---

# Track B — 自有 Cloudflare 帳號（wrangler-action）

## 前置需求

- **CF 帳號** + 對應 API token 已加進 GitHub repo secrets（`CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`），per `~/.claude/rules/secrets.md`
- **D1 / R2** 已在 CF 帳號建好（手動或透過 Cloudflare MCP `d1_database_create` / `r2_bucket_create`）
- consumer 走 NuxtHub 派（`@nuxthub/core` + `hub: {}`）或 raw wrangler.jsonc 派，per `~/.claude/rules/cloudflare-workers.md`

## B.1 CF 資源 provisioning

```bash
# D1（命名慣例：<consumer>-db）
wrangler d1 create <consumer>-db
# 拿到 uuid 填進 nuxt.config.ts hub.db.connection.databaseId 或 wrangler.jsonc

# R2（命名慣例：<consumer>-blob）
wrangler r2 bucket create <consumer>-blob
```

或走 Cloudflare MCP：`mcp__claude_ai_Cloudflare_Developer_Platform__d1_database_create` / `r2_bucket_create`。

## B.2 wrangler.jsonc + nuxt.config.ts binding

兩種 pattern 擇一，per `~/.claude/rules/cloudflare-workers.md` § 4：

**Pattern A**（D1-only 或簡單）：binding 寫進 `nuxt.config.ts` `hub.db.connection`，wrangler.jsonc 不寫 `d1_databases`
**Pattern B**（多 binding / DO / AI）：binding 全寫進 `wrangler.jsonc`，`hub: {}` 只啟用 helper

詳見 cloudflare-workers.md。

## B.3 GitHub Actions deploy.yml

```yaml
name: Deploy
on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  deployments: write

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: [self-hosted, gh-runner-lxc]    # 或 ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      # self-hosted runner 釘 v5：v6 的 pnpm 自我更新會壞掉 persistent runner 上的既有安裝
      - uses: pnpm/action-setup@v5
      - uses: actions/setup-node@v6
        with:
          node-version: 24
      - run: pnpm config set store-dir /home/runner/.pnpm-store
      # wrangler-action@v4 找 $PNPM_HOME/bin/pnpm（v11 layout），pnpm v10 的版面是
      # $PNPM_HOME/pnpm。MUST 用 cp，NEVER `ln -sf "$(which pnpm)"`（會 self-link）。
      - run: |
          if [ -f "$PNPM_HOME/pnpm" ]; then
            mkdir -p "$PNPM_HOME/bin"
            cp -f "$PNPM_HOME/pnpm" "$PNPM_HOME/bin/pnpm"
          fi
      - run: pnpm install --frozen-lockfile
      - name: Build
        run: pnpm build
        env:
          NITRO_PRESET: cloudflare_module

      - name: Apply D1 migrations
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: d1 migrations apply <DB-binding> --remote --config .output/server/wrangler.json

      - name: Deploy Worker
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          workingDirectory: .output
          command: deploy
          secrets: |
            # 列出所有 runtime secret name
            ...
        env:
          # 對應從 GitHub secret pull
          ...
```

## B.4 自訂網域

走 Cloudflare dashboard 加 Worker route（`<host>.example.com/*` → worker-name），或在 `wrangler.jsonc` `routes` 設好讓 deploy 時自動建。詳見 Cloudflare Workers docs。

## Track B 驗證

- GitHub Actions 跑綠燈
- `curl -I https://<worker>.workers.dev` 應回 200
- 自訂網域：dashboard Worker route 設好後 `curl -I https://<host>` 應回 200

---

# § 遷移（Track B ↔ Track A）

**MUST Read [migration.md](migration.md) before proceeding with any platform migration.**

B → A（NuxtHub → void.cloud）是 fleet 主要遷移方向（10 phase，已實證 co-purchase）。A → B（void → 自有 CF）較少見、保留 high-level。完整步驟見 [migration.md](migration.md)。

---

## 反向相容性

`/yudefine-deploy` 不限制單一 target — 同一 fleet 內可混合多種 deploy 派。Step 0 詢問是「該專案此次的選擇」，不會把 fleet 鎖死成單一 track。

## 變更歷史

- **2026-07-14**: centralize 到 clade `hub-runtime-cf-workers` plugin；current baseline 升至 void 0.10 + GitHub OIDC，退役長效 `VOID_TOKEN`、`void secret set` 與 void-sdk/void#52 patch；legacy compat workaround 明確 version-gate。
- **2026-05-27 PM**: A.3.1 compat flags 規約改成依 appType 分流（framework type 必 配置 3，配置 2 對 Nitro 不可用）；新增 A.3.6 patch-void-deploy postinstall hook hard rule（drizzle schema consumer 必裝，per void-sdk/void#52）；A.3.7 重編號（原 A.3.6 pnpm 保留字）；A.7 注意事項加 framework type 配置 2 警告 + missing patch 撞 internal error 線索；fleet table 更新 yudefine-blog 配置 2 → 3 + co-purchase 完成遷移。實證：yudefine-blog 2026-05-27 first-ever CI deploy 成功（之前 prod live 是 user 本機 manual deploy）。
- **2026-05-27**: 大幅瘦身（732 → ~430 行）。Track A 平台 SOP 全部委派給 official `void` skill + `void mcp`；本 skill 只保留 YuDefine fleet 政策層（compat flags hard rule、yudefine.com.tw zone DNS、CF token、consumer table、scripts naming pitfall）。新增 A.3.6 `pnpm deploy` 撞保留字、A.3.2 `compatibility_date` 對齊。
- **2026-05-26**: Track A void.cloud 段內容首次從 co-purchase 遷移實證濃縮成 B → A runbook。新增 dual-track 結構（A void.cloud / B own CF）。
