# 在 CI 中混淆你的构建

AfterPack 在你的流水线中所处的位置：仅用于生产环境、构建一次并将同一产物推广到各阶段，附带一份 CI 配方，以及关于种子、产物、source map 和云端不可用时的规则。

Source: https://www.afterpack.dev/zh/docs/builds

AfterPack 是一个构建后步骤：你的打包工具生成 JavaScript，然后 AfterPack 对其进行混淆。我们建议只在生产构建中混淆一次，并将该产物原封不动地推广到每个阶段。下面这一条不是建议：部署出去的 source map 会把你的原始源码原样交还，所以要让 source map 远离部署产物。

## 开发环境不会被混淆

混淆作用于已构建的输出，所以对开发服务器来说没有可以处理的对象。Vite dev、`next dev` 以及 HMR 提供的都是未构建或部分构建的代码。

- **开发。** 你的打包工具照常运行。AfterPack 处于空闲状态。调用栈指向你的真实源码，HMR 依然即时，调试与平时无异。
- **生产构建。** AfterPack 在打包完成后运行。[框架插件](https://www.afterpack.dev/zh/docs/frameworks)会自动执行它。在纯打包工具技术栈上，你需要手动调用一次。

要构建不经混淆的生产版本（例如一次性的调试构建），可以在配置中设置 [`{ build: { autorun: false } }`](https://www.afterpack.dev/zh/docs/config#build-autorun)，或在环境变量中设置 [`AFTERPACK_build_autorun=false`](https://www.afterpack.dev/zh/docs/config#build-autorun)。

## 构建一次，推广该产物

混淆后的代码包就是你要发布的产物，所以最干净的流水线只构建一次，然后将同一份字节在每个阶段之间搬运。

*(build-once, promote-everywhere flow diagram — see the web page)*

按每个环境各自重新运行 AfterPack 是另一种做法，代价是丢掉你的测试信号：每一跳都会产生形态不同的代码包，于是 QA 验证过的东西并不是生产环境最终提供的东西。只推广一份产物，能让你在 QA 中跑过的集成测试，始终针对到达生产环境的那份完全相同的字节。无论哪种做法，你的开发循环都保持未混淆。

## 命令行界面

```bash
npx afterpack@latest dist/
```

一个指向已生成 JavaScript 的路径，加上[以标志形式给出的每一个配置项](https://www.afterpack.dev/zh/docs/cli#flags)。在 CI 中显式命名构建目录是值得的，因为自动检测出的目录一旦选错，就会悄无声息地混淆错误的目录树。`npx afterpack@latest verify [dir]` 会依据它写下的[保护凭据](https://www.afterpack.dev/zh/docs/frameworks/nextjs#verify)重新核验一次构建，这是你在部署前应该运行的一步。[CLI 参考](https://www.afterpack.dev/zh/docs/cli#usage)包含完整的命令面。

## 默认随机种子，如需可复现则固定它

[种子](https://www.afterpack.dev/zh/docs/config#seed)让每次发布在结构上都与上一次不同，所以针对上周 bundle 做的工具或补丁，到这一次就会失效。默认每次发布都使用新的随机种子，你无需任何设置。

种子不是秘密。把它当作任何其他构建参数一样存放，例如放进 CI 变量里。

只有在你需要可复现的输出时才固定它：构建缓存命中、金标准快照测试，或者可审计的逐字节重建。

```bash
npx afterpack@latest dist/ --seed=git
```

[`--seed=git`](https://www.afterpack.dev/zh/docs/config#seed) 会解析为 `git rev-parse HEAD`。在仓库之外使用它时，会回退为一个全新的随机种子并打印一条提示，而不会让构建失败。任何其他字符串都会被引擎哈希处理；整数则按原值使用。固定种子每次都会产生完全相同的输出。

> **要固定，就固定到一个每次发布都会变化的值上**
>
> 一个在多次发布之间保持不变的种子，会把攻击者对上一次的分析原样带到下一次，而这恰恰是你安装 AfterPack 想要避免的属性。当固定种子确有必要时，把它固定到一个每次发布都会变化的值上，比如提交 SHA。

## 文件名与缓存

框架插件会给每个混淆后的文件一个跟随其混淆后字节的内容哈希文件名，因此同一个文件名永远不会对应两个不同的文件，浏览器、CDN 和 service worker 也永远不会在复用的文件名下拿到过期文件。文件名由文件未混淆的内容加上种子、引擎版本、你的设置、你的指令和[构建模式](https://www.afterpack.dev/zh/docs/config#build-mode)共同派生。

因此缓存取决于种子。一次抽取新种子的发布（默认如此，或使用 `--seed=git`）会重命名每个混淆后的文件，浏览器和 service worker 会重新下载每一个文件。常量种子在引擎版本和设置不变时，会让未改动的文件在多次发布之间保持相同的字节和文件名，长期缓存因此得以保留，代价就是上面警告所描述的那一点。

使用 [`key`](https://www.afterpack.dev/zh/docs/config#key) 时，云端引擎的版本也是文件名的一部分。如果构建无法从 API 读取该版本，它会发出警告，并为这次构建使用随机文件名：这样是安全的，但固定种子也无法复现它们。

[CLI](https://www.afterpack.dev/zh/docs/cli#content-hashed-file-names) 在 Next.js、Angular、Vite 和 webpack 构建上以同样的方式命名文件。在其他任何输出上，它保留打包器写下的文件名并打印一条提示；如果你在那里依赖长期缓存，请使用你框架的插件，或固定一个常量种子。

## 按计划轮换

按发布随机取种已经让每次发布的产物形态各不相同。发布不频繁的团队通常会在此基础上再加一层按计划的重建——每晚或每周从同一份源码重新构建一次——这样即使代码没有变化，部署出去的代码包每个周期也会改变形态。

一个从第 N 次构建开始逆向的攻击者，到第 N+1 次构建时就会被重置：两次构建之间没有共同的结构可以让之前的工作延续下去。这对反机器人、反作弊和抗 CAPTCHA 这类逻辑最为关键，因为对手在这些场景里有充裕的时间。轮换本身并不能阻止作弊或机器人流量，客户端逻辑背后仍然需要服务端的权威校验。

## 在 CI 中

这条流水线是：先构建，再混淆，然后把产物挡在部署之外。使用框架插件时，混淆步骤是你日常 `build` 的一部分，你可以省去显式的 AfterPack 调用。在纯打包工具技术栈上，需要你自己运行它。

```yaml
# .github/workflows/release.yml (excerpt)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1. Build. With @afterpack/<fw> installed, obfuscation runs here automatically.
      - run: npm ci
      - run: npm run build

      # 2. Pure-bundler stacks only (no framework plugin): obfuscate explicitly.
      #    Pin --seed for Turborepo/Nx remote-cache hits; omit it for a fresh
      #    random seed per release. --diagnostics.level=all prints every diagnostic
      #    instead of rolling the info ones up to one line.
      - run: npx afterpack@latest dist/ --seed=git --diagnostics.level=all --protectionMap.enabled

      # 3. Keep the Protection Map as a reviewable CI artifact. It is off under
      #    CI=true unless asked for: --protectionMap.enabled above, or
      #    protectionMap: { enabled: true } in your plugin options. It is written to
      #    the gitignored .afterpack/ at the PROJECT ROOT, never into the deploy
      #    tree. It embeds your ORIGINAL SOURCE — treat the upload as
      #    source-confidential.
      - uses: actions/upload-artifact@v4
        with:
          name: protection-map
          path: .afterpack/

      # 4. Deploy only the bundle. Never the .afterpack/ directory, never a .map.
      - run: ./deploy.sh dist/
```

Turborepo 和 Nx 按输出哈希来给它们的远程缓存建立键值。随机种子每次运行都会产生不同的代码包，所以这类缓存永远会未命中。传入 [`--seed=git`](https://www.afterpack.dev/zh/docs/config#seed) 以换取缓存命中：SHA 仍会随每次发布变化，因此构建在同一个提交内保持可复现，而在不同发布之间保持变化。

把 Protection Map 附到 PR 上，让审阅者能确认哪些区域的保护级别发生了变化。这是发现某次重构不小心把一个被标记的区域挪出保护范围的最快方式。把它当作供审阅者使用的产物。

> **没有 `afterpack audit ./dist` 这样的命令**
>
> 驱动[安全扫描器](https://www.afterpack.dev/zh/security-scanner)的审计针对一个 URL 运行并打印文本，它不接受本地目录。请在部署步骤之后，针对你部署好的预发布 URL 运行它。参见[审计](https://www.afterpack.dev/zh/docs/audit)。

## 产物，以及如何让它们远离部署

AfterPack 会写下三种本地产物。每一种都比它所描述的代码包更敏感，且三者默认都取更安全的一侧。

| 产物 | 默认值 | 存放位置 | 标志 |
| --- | --- | --- | --- |
| [**Protection Map**](https://www.afterpack.dev/zh/docs/protection-map)（`protectionMap.html`） | 当打包工具生成了可发现的 source map 且构建不在 CI 中运行时[默认开启](https://www.afterpack.dev/zh/docs/config#protectionMap-enabled)，否则需手动开启 | Free 构建中，始终位于项目根目录下被 gitignore 忽略的 [`.afterpack/`](https://www.afterpack.dev/zh/docs/cli#files) 目录，绝不会出现在被提供服务的构建产物旁边。Pro 云端构建不会写入本地文件：它的地图保存在[控制台](https://www.afterpack.dev/zh/docs/dashboard/builds)中，构建结束时会打印指向它的链接 | [`--protectionMap.enabled=false`](https://www.afterpack.dev/zh/docs/config#protectionMap-enabled) |
| **原始文件的备份** | CLI 中[默认开启](https://www.afterpack.dev/zh/docs/config#build-backup)，框架插件中默认关闭 | 对 CLI 来说是项目根目录下的 `.afterpack/backups/<build dir>/`；当插件被要求生成备份时，是输出文件旁边的 `.backup.<hash>` 同级文件 | [`--build.backup=false`](https://www.afterpack.dev/zh/docs/config#build-backup) 可关闭 |
| **`.map` source map 同级文件** | [生产环境默认关闭](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled)，仅在开发环境下发现了输入 source map 时才生成 | 生成的 JS 文件旁边 | [`--sourceMap.enabled=false`](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled) |

每次集成时会自动发生两件事：

- **`.afterpack/` 会忽略自身。** AfterPack 会在 `.afterpack/` 内写入一个 `.gitignore`，从不修改你的 `.gitignore`。写在构建产物旁边的 `.map` 或 `.backup.<hash>` 文件不会被忽略：请自行把它们挡在 git 和部署产物之外。
- **落入被提供服务路径的产物会触发警告。** 如果某个产物落入被提供服务的目录，AfterPack 会发出警告。一份被提供服务的 source map 就等于完全去混淆。

这份备份就是你的原始源码本身。CLI 把它保存在项目根目录下、被 gitignore 忽略的 `.afterpack/backups/` 中，并附带一份清单记录每个文件被混淆成了什么；`npx afterpack@latest restore` 会把原始文件还原回某一次运行之前的状态。当插件被要求执行 `build.backup` 时，会把它的副本写在输出文件旁边，部署时就会把它一并带走。

> **泄露的 source map 会撤销混淆**
>
> Source map 会把混淆后的输出直接链回你的原始源码，[`sourcesContent`](https://www.afterpack.dev/zh/docs/config#sourceMap-sourcesContent) 更是直接携带源码本身。永远不要部署它。

[生产构建](https://www.afterpack.dev/zh/docs/config#build-mode)还会删除打包工具写出的 source map，因为其中包含你的原始源代码：`.map` 文件、hidden map 和内联 map、`//# sourceMappingURL=` 注释，以及 CSS map。CLI 和每个插件都会这样做。开启 [`sourceMap.enabled`](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled) 时，AfterPack 的 map 会取代打包工具的 JavaScript map，CSS map 则会保留。无法删除的 map 会让构建失败。`node_modules`，以及构建目录中带有自己 `package.json` 的文件夹，都不会被改动。

> **引擎无法混淆的文件会让构建失败**
>
> 构建会停止并指出该文件。用 [`paths.exclude`](https://www.afterpack.dev/zh/docs/config#paths-exclude) 把某个必须原样发布的文件排除在外；[诊断](https://www.afterpack.dev/zh/docs/diagnostics)列出了每个代码的含义。

## CI 关口

CLI 的任何[非零退出](https://www.afterpack.dev/zh/docs/diagnostics#exit-codes)都意味着这次构建没有交付你所配置的内容，所以关口写成 `if ! npx afterpack@latest dist/ ...; then fail; fi`，而不是针对某个具体代码分支。

这包括用法错误（`64`）、体积上限（`3`）、需要更新的引擎（`6`），以及以 `1` 退出的空构建目录、引擎错误，或者一次 [Pro](https://www.afterpack.dev/zh/docs/tiers) 构建的 [`key`](https://www.afterpack.dev/zh/docs/config#key) 无法连接到 [AfterPack 云端](https://www.afterpack.dev/zh/docs/deployment-modes)。最后这种情况会让构建直接失败，而不是回退成更弱的输出，所以一次绿色运行就意味着你要求的保护确实交付了。Free 构建完全在本地完成，永远不会调用云端。[诊断](https://www.afterpack.dev/zh/docs/diagnostics#exit-codes)拥有完整的契约以及一次构建可能发出的每一个代码。

## 发布之后追踪构建

每一次 Pro 云端构建都会落在控制台里：它何时运行、来自哪个提交、处理了多少文件、花费了多少 [MB](https://www.afterpack.dev/zh/docs/dashboard/usage) 和构建时间，以及它的覆盖率和[膨胀率](https://www.afterpack.dev/zh/docs/config#inflation-max)。用它来追踪你的轮换节奏，并对比不同时间的构建。参见[控制台中的构建](https://www.afterpack.dev/zh/docs/dashboard/builds)。

Free 本地构建永远不会到达 AfterPack，所以它们永远不会出现在那里。

## 下一步

- [最佳实践](https://www.afterpack.dev/zh/docs/best-practices)：标记什么、放过什么，以及如何让 CI 保持诚实。
- [控制台中的构建](https://www.afterpack.dev/zh/docs/dashboard/builds)：这页所产出的一切，逐次构建的记录。
- [威胁模型](https://www.afterpack.dev/zh/docs/threat-model)：为什么一个每次发布形态都会变化的构建是首要防御手段。
- [CLI 参考](https://www.afterpack.dev/zh/docs/cli#flags)：上面命令概要背后的完整标志表。
- [诊断](https://www.afterpack.dev/zh/docs/diagnostics#exit-codes)：完整的退出契约与失败即中止规则。
- [Protection Map](https://www.afterpack.dev/zh/docs/protection-map)：读懂你的 CI 任务上传的那份产物。
