# 上线：域名、自动部署、回滚与监控

> 用 wrangler 完成首次部署，绑定自定义域名，用 GitHub Actions 实现推送即部署并给 API Token 最小权限，再备好检查清单、回滚方案与基础监控。

- 网址：https://coding.chengyu.dev/learn/ship/deploy/
- 学习路径：实战：从想法到上线一个网站
- 预计用时：45 分钟；核实日期：2026-10-25
- 官方来源：[Deploy your Astro site to Cloudflare Workers（Astro 官方文档）](https://docs.astro.build/en/guides/deploy/cloudflare/)，[Custom Domains（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/)，[GitHub Actions（Cloudflare Workers 官方文档）](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/)，[Workers CI/CD 概览（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/ci-cd/)，[wrangler-action（Cloudflare 官方仓库）](https://github.com/cloudflare/wrangler-action)，[Create API token（Cloudflare 官方文档）](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)，[API token permissions（Cloudflare 官方文档）](https://developers.cloudflare.com/fundamentals/api/reference/permissions/)，[Rollbacks（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/)，[Wrangler Workers commands（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/wrangler/commands/workers/)，[Wrangler D1 commands（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/wrangler/commands/d1/)，[Workers Logs（Cloudflare 官方文档）](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)，[Web Analytics（Cloudflare 官方文档）](https://developers.cloudflare.com/web-analytics/get-started/)，[Using secrets in GitHub Actions（GitHub 官方文档）](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)，[Building and testing Node.js（GitHub 官方文档）](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)

## 学习目标

- 能按正确顺序完成首次部署：迁移、构建、部署、设置密钥
- 能给 Worker 绑定自定义域名，并说清常见的冲突
- 能写出 GitHub Actions 工作流，并为 Cloudflare API Token 授予最小权限
- 能用版本回滚和 D1 备份应对线上事故，用日志与分析观察线上运行

本地跑得好好的网站，上线当天常见的三类事故：数据库迁移忘了在线上执行，页面一片 500；密钥没设置，管理页进不去；改了一行代码推上去，线上整个挂掉，却不知道怎么退回去。

这一课是整个路径的收尾：**第一次上线怎么做才稳，之后怎样每次都稳，出了事怎么办**。

## 第一次部署：顺序比命令重要

首次部署手动完成，目的是亲眼看每一步的结果，之后再交给自动化。顺序如下，每一步都有原因：

```text
# 0. 本地全绿再上线
$ npx vitest run && npx playwright test && npm run build
# 1. 先对远程库执行迁移：新代码上线时表必须已经存在
$ npx wrangler d1 migrations list my-shelf-db --remote
$ npx wrangler d1 migrations apply my-shelf-db --remote
# 2. 构建并试跑（不真正部署）
$ npx wrangler deploy --dry-run
# 3. 部署。成功后终端会打印 workers.dev 地址
$ npx wrangler deploy
# 4. Worker 存在之后再设置线上密钥（命令会立即新建版本并生效）
$ npx wrangler secret put ADMIN_TOKEN
```

两点说明。第一，第 4 步在部署之后：这样密钥是挂在已存在的 Worker 上的。代价是第 3 步到第 4 步之间管理令牌为空，这正是第 4 课 `isAdmin` 要求 `token.length > 0` 并与 `env.ADMIN_TOKEN` 严格相等的原因：**配置缺失时要「拒绝」而不是「放行」**。第二，第 1 步和第 3 步都是写线上的操作，让智能体帮你准备命令，但执行时由你确认，这也是 CLAUDE.md 红线里写明「不自行 deploy」的用意。

部署后马上用 `curl -i https://你的地址/api/books` 和浏览器各访问一次，再用管理页新增一本书，确认摘要生成。

## 绑定自定义域名

`workers.dev` 地址能用，但你想要的是 `shelf.你的域名.com`。官方文档里自定义域名的前提有三条：域名已经作为 zone 托管在你的 Cloudflare 账号下、Worker 已部署、目标主机名上没有已存在的 CNAME 记录。

有两种做法，选一种即可。面板：Workers & Pages，选择你的 Worker，进入 Settings，Domains & Routes，Add，Custom Domain。或者写进 `wrangler.jsonc`，随代码一起管理：

```jsonc
{
  "routes": [
    { "pattern": "shelf.example.com", "custom_domain": true }
  ]
}
```

然后 `npx wrangler deploy`。Cloudflare 会替你创建 DNS 记录并签发证书，不需要手动改 DNS。几个容易踩的坑：

- **已有 CNAME 会冲突**。主机名上如果原来有 CNAME，要先删掉再添加。
- **精确匹配，不含通配符**。`example.com` 和 `www.example.com` 是两个主机名，只绑定一个，另一个不会自动指过来，需要再加重定向。
- **自定义域名不等于路由**。官方区分了 Custom Domain 与 Routes 两种机制，我们用的是前者：它直接让 Worker 成为该主机名的源站。

> **你需要亲自完成的部分**
>
> 购买域名、把域名接入 Cloudflare、修改 DNS 属于账户级操作，请自己在面板里完成。让智能体只负责修改 `wrangler.jsonc` 和解释报错。

## GitHub Actions：推送即部署

手动部署适合第一次，之后应该交给自动化：每次合并到 `main`，自动测试、迁移、部署。Cloudflare 自己也提供 Git 集成的构建服务（Workers Builds），但本课用 GitHub Actions，因为工作流写在仓库里，可评审、可复用，也方便你控制「先测试再部署」的顺序。

```yaml
# .github/workflows/deploy.yml
name: CI and Deploy

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

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

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx vitest run
      - run: npm run build

  deploy:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - name: 应用数据库迁移
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: d1 migrations apply my-shelf-db --remote
      - name: 部署 Worker
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy
```

设计要点：

- **`pull_request` 只跑 `test`**。GitHub 不会把仓库密钥交给来自 fork 的 PR，所以陌生人的 PR 既部署不了，也读不到你的 Token。部署任务限定为 `main` 上的 `push`。
- **先迁移、后部署**。迁移必须向后兼容（新增列、新增表），这样老代码在迁移后到新代码上线前的几十秒里仍然能工作。要删列、改列，分两次发布完成。
- **`permissions: contents: read`**。工作流自己的 `GITHUB_TOKEN` 也只给读权限。
- **进阶**：官方示例用版本标签（`@v4`），更严格的做法是把第三方 Action 固定到具体提交哈希。
- 端到端测试暂时没放进 CI，因为它需要本地 D1 初始化和测试令牌。想加可以作为练习，令牌用单独的测试值。

## API Token：只给它需要的

工作流里的 `CLOUDFLARE_API_TOKEN` 是整个流程最敏感的东西，泄露等于别人能往你的账号部署代码。创建方式：Cloudflare 面板里选账号级别的 API Tokens，Create Token。官方示例从「Edit Cloudflare Workers」模板起步，再自己收紧。权限的取舍可以这样想：

| 权限 | 作用 | 是否需要 |
|---|---|---|
| 账号 · Workers Scripts · Edit | 上传、更新 Worker | 需要 |
| 账号 · D1 · Edit | CI 里执行 `d1 migrations apply --remote` | 需要（若 CI 不跑迁移就不要） |
| 账号 · Workers AI | AI 绑定是运行时能力 | 通常不需要，以实际报错为准 |
| 区域 · Workers Routes / DNS | 部署时创建自定义域名记录 | 取决于是否每次由 CI 维护域名 |

我没法替你逐项验证每个模板里到底含哪些权限，所以最稳妥的办法是：**从最小集合开始，让第一次失败的部署告诉你缺什么，缺一项补一项**，而不是一开始就勾上所有 Edit。同时：

- 范围限定到**单个账号**，区域权限限定到**你的那个域名**；
- 设置**过期时间（TTL）**；GitHub 托管的运行器 IP 不固定，所以不要启用 IP 白名单；
- 新生成的 Token 前缀为 `cfut_`，能被密钥扫描工具识别；只会显示一次，复制后马上存好；
- 写入 GitHub 时不要贴在聊天或代码里，在终端用 `gh secret set CLOUDFLARE_API_TOKEN`，它会提示你输入值；账号 ID 同理用 `gh secret set CLOUDFLARE_ACCOUNT_ID`。

万一 Token 泄露：立刻在 Cloudflare 面板里删除或轮换它，再更新 GitHub 里的 secret，并查看账号下近期的部署记录。

## 上线检查清单

每次发布前后过一遍，比任何经验都可靠：

| 阶段 | 检查项 | 怎么确认 |
|---|---|---|
| 发布前 | 测试和构建全绿 | CI 的 `test` 任务通过 |
| 发布前 | 迁移向后兼容 | 新迁移只加不删；看一眼 SQL |
| 发布前 | 线上 secret 已设置 | `npx wrangler secret list` 里有 `ADMIN_TOKEN` |
| 发布后 | 首页与详情页能打开 | 浏览器和 `curl -I` |
| 发布后 | 无令牌写接口返回 401 | `curl -i -X POST .../api/books` |
| 发布后 | 私有书不在公开接口 | 对线上接口的响应过一遍 |
| 发布后 | 新增一本书，摘要和标签生成 | 管理页实测 |
| 发布后 | 没有新的错误日志 | 面板或 `wrangler tail` |
| 发布后 | 域名与 HTTPS 正常 | 浏览器访问自定义域名 |

## 回滚：代码、数据、迁移分开想

线上出事时先止血，再查原因。回滚分三层：

**代码回滚**用 Worker 的版本机制，几秒完成：

```bash
npx wrangler deployments list                      # 最近的部署记录
npx wrangler versions list                         # 最近的版本
npx wrangler rollback <VERSION_ID> --message "回滚：详情页 500"
```

不带版本号时，它会回到上一个版本。官方文档强调了几个限制：回滚只切换代码，**不会改变绑定的资源**，所以 D1 里的数据和表结构不会跟着变；旧代码遇到新结构可能报错；只能回到最近的 100 个版本。面板里也可以操作：Workers & Pages，选 Worker，Deployments，目标版本旁点 Rollback。

**数据回滚**依靠 D1 的备份。高风险的迁移之前，先导出一份；D1 的 Time Travel 可以把线上库恢复到 30 天内的某个时间点：

```bash
npx wrangler d1 export my-shelf-db --remote --output backup-before-0002.sql
npx wrangler d1 time-travel info my-shelf-db        # 查看当前书签/时间点
npx wrangler d1 time-travel restore my-shelf-db --timestamp <时间>   # 恢复，需你确认
```

`restore` 会覆盖线上数据，属于重大操作，必须由你确认后执行。

**修复主线**也别忘了：用 `wrangler rollback` 回滚后，如果问题代码还在 `main` 上，下一次推送又会把坏代码部署回去。所以回滚之后要在仓库里 `git revert` 或修复，让 `main` 与线上一致。

## 基础监控：日志与分析

我们在配置里早就打开了 `observability`。它的作用：Worker 里的 `console.log`、`console.error` 和请求信息会被收集，可以在面板的 Observability 里搜索。免费计划每天 20 万条日志事件，保留 3 天；官方页面还提示，从 2026 年 12 月 1 日起计费方式会切换到 Cloudflare Observability 的定价，上线前看一眼最新说明。

需要实时观察时用 `wrangler tail`：

```bash
npx wrangler tail --status error      # 只看出错的请求
npx wrangler tail --search "enrichBook" # 只看包含关键字的日志
```

记日志有两条纪律：**不要记令牌、完整请求头和读者的短评原文**；错误要带上能定位的信息，比如书的 `id`，而不是只写「失败了」。第 4 课的 `console.error("enrichBook failed", id, err)` 就是这个思路。

流量分析可以用 Cloudflare Web Analytics：面板里进入 Web analytics，Add a site，选择你的主机名。官方文档说明经 Cloudflare 代理的站点默认启用；也可以改为手动贴 JS 片段。具体是否使用 Cookie 的隐私细节，请看官方的隐私说明后再决定要不要开。

还要留意免费额度：Workers 免费计划每天 10 万次请求（静态资源请求免费且不限量），D1 免费计划每天 500 万行读取、10 万行写入，Workers AI 每天 10,000 Neurons。个人书单远用不到，但如果被爬虫打满，这些数字是你判断的依据。

**生成 GitHub Actions 部署工作流与上线清单**

```text
读取 CLAUDE.md、wrangler.jsonc 和 package.json，为书单项目准备自动部署。严格遵守红线：不读取 `.dev.vars`，不执行任何 `--remote` 命令，不执行 wrangler deploy，不设置任何 secret；这些都由我手动完成。

任务：
1. 创建 .github/workflows/deploy.yml：push 到 main 与 pull_request 触发；test 任务运行 npm ci、vitest、构建；deploy 任务 needs test，仅在 main 的 push 时运行，先用 cloudflare/wrangler-action@v4 执行 `d1 migrations apply my-shelf-db --remote`，再执行 deploy；整个工作流 permissions 仅 contents: read，并设置 concurrency 与 timeout；checkout 用 v6，setup-node 用 v7，Node 版本满足 Astro 的最低要求；
2. 对照 Astro 与 Cloudflare 的官方部署文档，检查 wrangler.jsonc 中 main、assets、compatibility_date、observability 是否合理，列出你发现的问题但先不修改；
3. 写 docs/DEPLOY.md（简短）：首次部署的命令顺序、需要在 GitHub 里设置的两个 secret 名称（CLOUDFLARE_API_TOKEN、CLOUDFLARE_ACCOUNT_ID）与设置方法、API Token 建议的最小权限与 TTL、上线检查清单、三种回滚方式（wrangler rollback、D1 export 与 Time Travel、git revert）；
4. 完成后运行 `git diff --stat`，并逐项说明哪些步骤需要我手动执行。
```

> 小测：CI 里执行了 `wrangler rollback` 把线上退回上一个版本，但没有改仓库。结果是？

> 小测：下面哪个做法最符合 API Token 的最小权限原则？

## 动手任务

- [ ] 按顺序完成首次部署：远程迁移、构建、`wrangler deploy`、`wrangler secret put ADMIN_TOKEN`
- [ ] 对线上地址跑一遍上线检查清单，保留 `curl` 的 401 结果作为证据
- [ ] 绑定自定义域名（或决定暂时只用 workers.dev），确认 HTTPS 正常
- [ ] 创建最小权限的 API Token，用 `gh secret set` 写入两个 secret，并让一次 PR 与一次合并分别触发工作流
- [ ] 故意部署一个有问题的小改动，练习 `wrangler rollback`，再用 `git revert` 修复主线
- [ ] 在面板里找到 Observability，用 `wrangler tail --status error` 看一次实时日志

恭喜，你从一句话的想法出发，走完了规格、骨架、界面、后端、测试与上线。回头看，真正可迁移的是方法：**先对齐再动手，给智能体可验证的标准，把危险操作留给自己，让独立的眼睛检查**。接下来可以去 [Claude Code 精通](/learn/claude-code/headless-ci/) 路径，把评审和测试进一步交给无头模式与 CI。
