T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

发布检查清单 ​

面向维护者。正在使用 T3 Code?请参阅 用户文档。

本文档说明 stable 与 nightly 桌面发布所使用的统一发布流程。

Beta 是产品标签,不是发布渠道,也不是版本后缀。发布渠道仍然是 stable 和 nightly。要发布 0.1.0 Beta,使用 channel=stable 和 version=0.1.0。以后去掉 Beta 标签,是一次与版本无关的品牌变更。

工作流做什么 ​

  • 工作流:.github/workflows/release.yml
  • 触发条件:
    • 手动 workflow_dispatch 并带上 channel=stable,这是发布 stable 的常规做法。派发 stable 和 nightly 时必须选中 main;preview 可以选择任意分支。渠道默认是 preview,因此漏选时不会发布 stable。
    • 推送与 v*.*.* 匹配的标签,对某一个明确的提交做 stable 发布
    • 每 30 分钟一次的定时 nightly 检查
    • 手动 workflow_dispatch 并带上 channel=nightly
    • 手动 workflow_dispatch 并带上 channel=preview,这是维护者的测试列车。它针对一个终端用户绝不能收到的提交,把整条发布流程走一遍(构建、签名、公证、冒烟、发布)。未合并的分支,或一项有风险的改动,就是这样在合入之前先获得一次真实的发布运行。它按 nightly 的版本规则构建触发这次运行的提交,预发布标识为 preview(0.0.41-preview.<date>.<run>),然后发布一个 GitHub 预发布,并把 npm 包发到 preview 这个 dist-tag 下。Preview 不在定时计划里,也没有任何默认的 npm dist-tag 指向它;它的桌面构建不带更新源,也不会附上更新器清单(latest*.yml、nightly*.yml 以及 blockmap),因此 stable 或 nightly 的已安装版本不会被提供这个构建。装上它的仅有途径,是手动下载这次发布、使用 npx t3@preview、让安装脚本使用 T3CODE_CHANNEL=preview,或者在终端里执行 t3 update --channel preview。每一种方式都会打印警告;如果当前正在运行的构建本身不是 preview,CLI 还会要求确认。这次发布被命名为维护者测试构建,正文是一段警告,而不是生成出来的说明:由未合并分支的历史做成的变更日志,并不能算一份变更日志。nightly 和 stable 的说明不受影响,因为每个系列都在自己的渠道内解析上一个标签。托管 Web 应用、AUR 和 Discord 公告都会跳过。请保留它;空闲时它没有任何成本。
  • 手动的 stable 发布构建的是最近一次已发布 nightly 的那个提交,而不是 main 的 HEAD。Nightly 就是发布候选:先验证 nightly,再提升它。你验证期间,合入 main 的提交会继续进来,但绝不会漏进这次 stable 构建。
    • 版本默认就是 nightly 预览过的那个(0.0.39-nightly.* 会以 0.0.39 发布)。传入 version 可以覆盖它,例如做一次次版本号提升。
    • GitHub Release 发布时,stable 标签会打在这次 nightly 的提交上。
    • 手动推送 vX.Y.Z 标签仍然有效,而且构建的就是被打上标签的那个提交。当要发布的提交并不是最新的 nightly 时就用这种方式,比如发布分支上拣选出来的一处修复。
  • 构建产物的同时会跑 lint、类型检查和测试。发布会等每一项检查都结束。
  • 在打包客户端(client)之前,读取共享的生产环境 T3 Connect 中继(relay)URL,以及 Clerk 的客户端配置。
  • 在 build_bundle 作业里只构建一次与平台无关的 JS(服务端包、Web 客户端、Electron 主进程),再把它作为 js-bundle 产物交给每一个平台作业。平台作业只负责打包,因此没有运行器会重新构建它。
  • 两个渠道都会并行构建六个桌面产物,每一个都是独立的作业(desktop_<platform>_<arch>,也就是对 release-desktop.yml 的一次调用),门禁只有这个包。macOS 和 Linux 目标在与自己架构相同的硬件上构建。两个 Windows 目标都在 Linux 运行器上构建并用 jsign 签名,这种打包比 Windows 运行器更快;与目标架构相同的 Windows 作业(windows-2025 或 windows-11-arm,即 verify_runner)会运行打包好的原生程序和 CLI 归档。Windows 作业把同架构的 Linux CLI 归档嵌进去,当作 WSL 运行时,并且在中途等待这份产物,而不是等整个 Linux 作业结束:
    • macOS arm64 DMG
    • macOS x64 DMG
    • Linux 的 x64 与 arm64 AppImage,以及 .deb,来自同一次 electron-builder 运行。.deb 通过 electron-updater 在应用内更新,安装时使用 dpkg。
    • Windows 的 x64 与 arm64 NSIS 安装程序
  • 对照一份已经灌入种子数据的数据库,打开打包后的 Linux x64 AppImage(在 Xvfb 下)和 macOS arm64 DMG,并打开一个对话线程(thread)(scripts/smoke-desktop-app.ts)。后端或包启动不了的构建会在这里失败,因此什么都不会发布。scripts/fixtures/desktop-launch 里的数据库故意是一份旧快照:打包后的服务端必须把它向前迁移,就像为正在升级的用户所做的那样。数据库模式变更时,不要重新生成它。
  • 用全部产出的文件发布一个 GitHub Release。
    • X.Y.Z 之后带有后缀的 stable 标签(例如 1.2.3-alpha.1)会作为 GitHub 预发布发布。
    • 只有不带后缀的 stable X.Y.Z 发布,才会被标成仓库的最新发布。
    • Nightly 运行始终是 GitHub 预发布,并且从不会被标成最新。
    • 自动生成的发布说明固定对照同一渠道里的上一个标签,因此 stable 对照上一个 stable 标签,nightly 对照上一个 nightly 标签。
    • 发布作业里的编写发布说明步骤(scripts/generate-release-notes.ts)负责生成正文:一行计算出来的首行、一段来自 gpt-6.1-terra 的简短摘要,以及折叠在 <details> 下面的 GitHub 生成列表。这一步需要 OPENAI_API_KEY 密钥。桌面端的更新弹出层最多展示七条要点,并给出其余修复和性能改进的数量。只涉及移动端的提交会计入发布总数,但不能成为要点。所提供发布的已发布时长来自更新源里的 releaseDate。nightly 的 Discord 帖子展示 <details> 以上的那一部分。如果摘要生成失败,这次发布就改用 GitHub 生成的说明。
  • 发布资产里会带上 Electron 的自动更新元数据(例如 latest*.yml、nightly*.yml 和 *.blockmap)。
  • 每个平台都会构建一份自包含的 CLI 归档(t3-<version>-<platform>-<arch>.tar.gz,Windows 上是 .zip),和该目标的桌面产物放在同一个作业里,并连同 SHA256SUMS 文件一起附到 GitHub Release 上。每个渠道都如此,一共五个目标:macOS arm64、Linux x64 和 arm64、Windows x64 和 arm64。每一份归档都在与自己架构相同的硬件上做冒烟测试;除了 Windows 归档,其余的也在那台硬件上构建并签名。没有 macOS x64 归档:x64 的 macOS 不支持 Node 单可执行文件(SEA 文档把 macOS 列为只有 arm64),而且这个二进制文件一启动就会段错误。x64 桌面应用走的是 Electron,不受影响。
    • 归档把服务端做成一个 Node 单可执行文件(scripts/build-cli-archive.ts),所以解包时不需要 Node,不需要 npm,也不需要编译器。这是 T3 Code 管理运行时的唯一形式:桌面端的 SSH 环境(environment)、引导服务、t3 update 以及安装脚本,都会下载这份归档,并对照 SHA256SUMS 校验。t3 这个 npm 包是给那些自己运行 npx t3 或 npm install -g t3 的人用的,它下载的也是同一份归档;产品里没有任何一部分是从 npm 安装的。curl | sh 的安装器是 scripts/install.sh 和 scripts/install.ps1;营销站点在构建时把它们复制到自己的 public/(apps/marketing/scripts/stage-install-scripts.mjs),并在 t3.codes/install.sh 和 /install.ps1 提供它们。
    • 根目录的 package.json 把 engines.node 固定成开发、CI 和独立可执行文件所使用的那个精确 Node 版本。请保持精确:跨目标构建会下载这个版本,而原生可执行文件的构建会拒绝不一样的宿主 Node 版本。临时:Linux arm64 可执行文件只能在 Linux arm64 宿主上构建,并用 postject 注入,因为 Node 的 --build-sea 输出无法在页大小为 16 KiB 或 64 KiB 的内核上加载。等 nodejs/node#67048 修复之后再撤销。运行时一旦改动,就需要一个新的 T3 发布版本,因为已经装上的运行时是按这个版本缓存的。
    • Apple 密钥齐全时,macOS 归档会用 Developer ID 证书签名并完成公证(否则是 ad hoc 签名,这种归档从 curl 或 tar 安装之后仍然能运行)。Windows 可执行文件使用和安装程序同一套 Azure Trusted Signing 配置。macOS 归档里的每一个原生插件也会签名,因为强化运行时会拒绝未签名的库。
    • 每一份归档都会在自己的构建运行器上解压并执行(scripts/smoke-cli-archive.ts);Windows 则在 verify_runner 上执行。
  • GitHub Release 发布之后,同一个工作流文件会用 OIDC 可信发布来发布 t3 这个 npm 包。它只是一个启动器(scripts/npm-launcher/t3.mjs):第一次运行时,从 GitHub Release 下载与自己版本完全一致的 CLI 归档,对照 SHA256SUMS 校验,再解包到 <T3 home>/runtime/versions/<version>,这也是安装脚本和 t3 update 共用的布局。可执行文件不在 npm 上。它们以前在,是作为 @t3code/t3-<platform>-<arch> 的可选依赖,但 npm 在发布时的扫描会在启动器已经上线之后,把这些 80–250 MB 的包扣留长达一个小时,那段时间里每一次安装拿到的都是一个没有可执行文件的启动器。这些包以旧版本留在 npm 上,不再发布新版本。因为启动器读的是这次发布,所以 publish_cli 排在 release 之后,绝不会排在前面。
    • stable 发布使用的 npm dist-tag 是 latest
    • nightly 发布使用的 npm dist-tag 是 nightly
    • preview 发布使用的 npm dist-tag 是 preview;除非点名要求,否则没有任何解析会落到它上面
    • 一次性设置:包 t3 需要为本工作流文件注册一个可信发布者(见下文)。
  • 桌面作业跑着的时候,会在 Vercel 上构建托管 Web 应用,并且只有在发布完成之后才让它上线:
    • stable 发布会别名到 latest 托管应用渠道
    • nightly 发布会别名到 nightly 托管应用渠道
  • 签名是可选的,各个平台会根据密钥自动检测。

拉取请求(pull request)的 macOS 预览 ​

给 PR 打上 preview:mac 标签,会把一个已签名、已公证、并且启用了 T3 Connect 的 Apple Silicon DMG 发布到滚动的 desktop-preview 预发布上,来自 fork 的 PR 也同样适用。这个标签是针对它所标上的那一次提交的一次性请求:可信工作流一旦拿到构建,就会把标签去掉,之后的推送不会再构建,直到维护者再次打上它。因此,每一次带签名的预览都是维护者按提交做出的决定。这一点很重要,因为结果带有 Developer ID 签名。给一位贡献者作保,就能让此人被打上标签的提交获得签名;这并不是一项长期授权。构建被拆开,使 Developer ID 证书绝不会和 PR 代码出现在同一个作业里:

  • .github/workflows/desktop-macos-preview.yml 在 pull_request 上运行,不带任何密钥,并且只从 PR 构建 JS 包(同一份 js-bundle 产物,由 release.yml 产出)。
  • .github/workflows/desktop-macos-preview-publish.yml 在 workflow_run 上、从 main 运行。除非 PR 仍然打开、标签还在、它的 head 就是已构建的那次提交,而且作者是机器人、协作者,或者列在 .github/VOUCHED.td 里(从默认分支读取,因此 PR 不能为自己作保),否则这个作业会拒绝。接着,它通过 release-desktop.yml(检出在 main)来打包并签名这个包,所以打包逻辑、原生辅助程序,以及 Electron 和桌面端依赖,都来自 main,而不是 PR。从 PR 提交里读出的,只有 .env.example 中的版本和公开的 T3 Connect 标识,而且是当作数据来读,这样已签名应用的通行密钥授权项才能和这个包对得上。改动了打包的 PR,必须改用上面的 channel=preview 发布列车。

把包交给签名运行器之前,可信工作流会检查它的 ZIP 条目,只接受位于 server/dist 和 desktop/dist-electron 之下的常规文件,以及通向这些根目录的目录条目。这份产物不能覆盖打包代码,也不能覆盖已经安装的依赖。在签名运行器上,这个包会被复制进应用,绝不会被执行。发布工作流里的 pull_request_target 清理作业会在 PR 关闭时移除这份下载;如果构建还没用到这个标签、标签就被人手动去掉,它也会移除下载。这个作业绝不会检出 PR 代码。

发布所需凭据 ​

Stable 发布除了下文记录的平台凭据和部署凭据之外,还需要这些 GitHub Actions 密钥:

  • RELEASE_APP_ID
  • RELEASE_APP_PRIVATE_KEY

收尾作业用它们以 Release App 的身份,把已经对齐的包版本提交并推送到 main。发布 GitHub Release 时使用的是仓库范围的工作流令牌,因此它拥有一份与共享的 Release App 安装相互独立的速率限制配额。

T3 Connect 中继(relay)部署 ​

中继是一套共享的控制平面,版本与客户端(client)发布彼此分开。Stable 和 nightly 的客户端构建必须指向同一个中继,这样用户在切换发布渠道时,会看到相同的已链接环境(environment)。

.github/workflows/deploy-relay.yml 会部署 Alchemy 阶段 prod,每一次推送到 main 都会触发。Stable 和 nightly 发布在构建桌面、CLI 或托管 Web 产物之前,会从已有的 production GitHub Actions 环境读取中继 URL 和 Clerk 客户端配置。这个环境只接受 main 和 v* 标签,因此可能从任意分支运行的 preview 发布,会从 .env.example 取得同样的公开标识;请让它与 production 的值保持一致。每个渠道都用仓库级的 CLOUDFLARE_API_TOKEN,从 Alchemy 状态读取中继的客户端追踪配置,所以把这个令牌移进 production 会破坏 preview 发布。

中继部署共用的必需仓库变量:

  • CLOUDFLARE_ACCOUNT_ID
  • PLANETSCALE_ORGANIZATION
  • AXIOM_ORG_ID

中继部署共用的必需仓库密钥:

  • CLOUDFLARE_API_TOKEN
  • PLANETSCALE_API_TOKEN_ID
  • PLANETSCALE_API_TOKEN
  • AXIOM_TOKEN

必需的 production 环境变量:

  • RELAY_API_ZONE_NAME
  • RELAY_TUNNEL_ZONE_NAME
  • CLERK_PUBLISHABLE_KEY
  • CLERK_JWT_AUDIENCE
  • CLERK_JWT_TEMPLATE
  • CLERK_CLI_OAUTH_CLIENT_ID
  • APNS_ENVIRONMENT
  • APNS_TEAM_ID
  • APNS_KEY_ID
  • APNS_BUNDLE_ID

可选的 production 环境变量:

  • RELAY_DOMAIN,用于覆盖派生出来的 relay.<RELAY_API_ZONE_NAME> 域名
  • RELAY_TUNNEL_CLEANUP_MODE,取值为 off、dry-run 或 enabled。缺失值和空白值使用 off。
  • RELAY_LEGACY_TUNNEL_CLEANUP_MODE,取值与上面相同,用于宿主从未注册过恢复的隧道。缺失值和空白值使用 off。

必需的 production 环境密钥:

  • CLERK_SECRET_KEY
  • APNS_PRIVATE_KEY

可选的 production 环境密钥:

  • 用于 T3 Chat 账户链接的 T3CHAT_LINK_SECRET。它必须与 T3 Chat 上的 T3CODE_LINK_SECRET 一致。

更改变量或密钥之后,从 main 手动运行部署 T3 Connect 中继工作流,并保持 force 未勾选。Alchemy 会比较 Worker 读到的值,只要其中有一项变了,就重新部署。只有在要重新部署那些没有检测到变化的资源时,才勾选 force。

账户范围的仓库凭据由 Alchemy 在置备中继阶段时使用;它们不会绑定进中继 Worker。生产部署使用的是 Axiom 个人访问令牌,因此 AXIOM_ORG_ID 必须和 AXIOM_TOKEN 一起提供。prod 阶段拥有那份保留的 PlanetScale 数据库。本地的个人阶段从它置备出隔离分支,并且从不由 CI 部署。生产环境会把已配置的中继 API 和隧道 DNS 区域,作为保留的 Cloudflare 资源接管下来。个人阶段引用由生产环境持有的这些区域。

开发者在本地部署个人阶段:

sh
vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local

同一个仓库里打了 relay:preview 标签的 PR,会部署阶段 pr-<number>,经由 .github/workflows/relay-preview.yml,并要先在 relay-preview 环境中获得批准。去掉标签或关闭 PR 会运行 .github/workflows/relay-preview-cleanup.yml,它会销毁这个阶段,并清扫该阶段管理的隧道和 DNS 记录。Nightly 运行会销毁那些已经关闭、带有标签、而且拆卸失败的 PR 所对应的阶段。fork 的 PR 不会得到预览。

托管隧道清理的推出 ​

首次生产部署请保留 RELAY_TUNNEL_CLEANUP_MODE=off。这次部署会应用可空分配迁移,并加上恢复端点。Web 和移动客户端不需要配套发布。CLI 和桌面服务端构建必须先到达用户,然后才能启用清理,因为这些构建会注册恢复,并在唤醒之后替换一条已被删除的隧道。

  1. 在清理为 off 的情况下部署中继和迁移。
  2. 发布服务端构建,并确认当前宿主会注册恢复。较旧的宿主仍然标记为遗留,只有在下面的遗留开关之下才会成为候选。
  3. 设置 dry-run,跑一次中继部署,并在多次清扫中读取清扫计数器(scanned、wouldDelete、skippedLegacy、skippedOrphan、failed、truncated)。每一次清扫都会把这些计数器,以及当时生效的 mode,记成 relay.managed_endpoint_reaper.* 属性,附在 Axiom 里这次清扫的 relay.managed_endpoint_reaper.sweep 跨度(span)上。
  4. 运行下面的一次性宿主金丝雀测试。
  5. 只有在金丝雀测试无需重启服务端就已恢复之后,才设置 enabled。

这个作业每五分钟跑一次,对丢失了连接器的隧道给五分钟宽限,因此一个候选通常在下线后五到十分钟被移除。从未连接过的隧道要等一个小时。一次清扫最多尝试 100 次删除,所以有积压时会更久。更改 RELAY_TUNNEL_CLEANUP_MODE,包括在故障期间关掉清理,都需要一次不带 force 的中继部署。在下一次清扫跨度上确认新的 mode。

要回滚,把清理设为 off,并在降级任何宿主之前跑一次中继部署。当前的服务端构建还在使用时,恢复端点要保持部署。可空列可以留着。

遗留隧道清理 ​

遗留隧道属于从未注册过恢复的宿主,通常是在恢复构建发布之前就已经离线的宿主。一旦 Cloudflare 报告它们处于下线、或者从未连接的时间超过 7 天,RELAY_LEGACY_TUNNEL_CLEANUP_MODE 就会删除这些隧道。它与 RELAY_TUNNEL_CLEANUP_MODE 相互独立,其余每一项检查仍然适用。

被删掉的遗留隧道会保留自己的分配,因此主机名也保留下来。宿主重新上线时:

  • 在带有恢复的构建上,连接器会被拒绝,宿主会请求一条同一主机名上的替换隧道。
  • 在通过 CLI 链接的较旧构建上,启动时会置备一条新隧道。
  • 在从 Web 或移动端链接的较旧构建上,那台计算机上的 T3 Code 更新之前,宿主保持离线。

启用遗留清理之前,先发布会显示离线原因的 Web 和移动端构建,这样宿主受到影响的用户就知道该怎么做。中继在带有这项改动的首次部署里加入 tunnel_released_at 分配列;在你设置之前,遗留开关保持为 off。

  1. 用只读的 Cloudflare 令牌运行 vp run --filter t3code-relay tunnels:census。它会统计每一个中继阶段里的隧道。收割器(reaper)只能看到自己这个阶段的隧道,其余的要手工清理。
  2. 把遗留模式设为 dry-run 并部署,然后用一天时间读取清扫跨度上的 wouldDeleteLegacy、legacyOver30Days、totalDown 和 totalInactive。wouldDeleteLegacy 只统计一次清扫检查过的隧道,每种状态最多 500 条。totalDown 和 totalInactive 是 Cloudflare 对这一阶段中下线超过五分钟、以及从未连接超过一小时的隧道的计数。其中包含收割器会跳过的隧道,因此这是积压量的上限。wouldDeleteLegacy 在每次清扫的 scanned 里所占的比例,可以估计这个总数中有多少符合条件。
  3. 运行下面一次性宿主金丝雀测试里的遗留步骤。
  4. 启用之前,确认会显示「在那台计算机上更新 T3 Code」这条消息的 Web 和移动端构建已经上线。没有这些构建,较旧宿主丢了隧道的用户只会看到它处于离线。
  5. 把遗留模式设为 enabled。一次清扫最多删除 100 条隧道,每次四条,并且在 90 秒之后不再开始新的删除。如果每次清扫都能删完自己的 100 条,20,000 条积压大约需要 17 小时。请观察 deletedLegacy、attempted、failed 和 truncated。attempted 远低于 100,并且 truncated 已被设置,就说明这次清扫提前停了:要么时间预算用尽,要么 Cloudflare 对某次删除做了速率限制。计数器看不出是哪一种;中继会为每一次失败的删除记下一条带有 Cloudflare 错误的警告。

在 Axiom 里,按 name == "relay.managed_endpoint_reaper.sweep" 过滤中继追踪数据集,并把 attributes.custom.relay.managed_endpoint_reaper.* 字段随时间画出来。

如果出现下面任何一种情况,把遗留模式设回 off 并部署:

  • failed 每次清扫都持续高于数条。请到警告日志里看 Cloudflare 的错误。
  • 用户报告:他们已经在那台计算机上更新并重启了 T3 Code,某个环境却仍然离线,而且带有那条更新提示。
  • 清扫运行期间,中继的请求错误升高。删除操作和请求处理程序共用 Postgres 连接池。

关掉遗留模式会停止新的遗留删除;只要 RELAY_TUNNEL_CLEANUP_MODE 仍是 enabled,它就会继续删除那些具备恢复能力的宿主的隧道。已经删除的隧道保持删除;它们的宿主按上文所述恢复。

一次性宿主金丝雀 ​

这项测试还没有在真实的 Cloudflare 账户上跑过。请在一次性的中继阶段、测试用 Cloudflare 账户、一次性宿主和一次性 T3 主目录上运行它。在它通过之前,生产环境的清理请保持在 off 或 dry-run。不要停掉日常使用的 T3 服务端。

  1. 在清理为 dry-run 的情况下部署这个一次性阶段。通过 Web 或移动端设置链接第一个一次性环境,并确认它的隧道是健康的,而且恢复已经注册。
  2. 停掉这个宿主,再在另一个本地端口上启动同一个 T3 主目录。确认公共主机名到达的是新端口,并且不会向旧端口发送任何内容。
  3. 用一个早于恢复注册的服务端构建,链接第二个一次性环境。捕获它托管的 cloudflared 子进程 PID,确认这个进程属于该宿主,并且只用 kill -STOP <legacy-pid> 暂停这一个子进程。等到 Cloudflare 报告它已下线超过五分钟。
  4. 从第一个环境的服务端日志里捕获它的 cloudflared 子进程 PID,确认归属,再用 kill -STOP <first-pid> 暂停它。等到 Cloudflare 报告它已下线超过五分钟。
  5. 确认试运行把第一条隧道计在 wouldDelete,把第二条计在 skippedLegacy。
  6. 在这个一次性阶段上把清理设为 enabled 并部署。到测试用的 Cloudflare 账户里确认第一条隧道已经删除,遗留隧道还在。
  7. 用 kill -CONT <first-pid> 恢复第一个子进程。确认正在运行的服务端发现了反复出现的拒绝,发起了恢复请求,并且无需重启就能在同一主机名上重新可达。
  8. 用 kill -CONT <legacy-pid> 恢复遗留子进程,并确认它的隧道重新连上。
  9. 在大范围推出之前,先在一台一次性使用的笔记本电脑上,用一次真实的睡眠和唤醒再做一遍。

同一个一次性阶段上的遗留清理:

  1. 设置 RELAY_LEGACY_TUNNEL_GRACE_MINUTES=10,并把遗留模式设为 dry-run,然后部署。这个覆盖值会缩短 7 天的宽限期,并且在 prod 上会被忽略。再次暂停遗留子进程,等到 Cloudflare 报告它已下线超过十分钟。
  2. 确认清扫把它计进了 wouldDeleteLegacy,然后把遗留模式设为 enabled 并部署。确认遗留隧道已经删除,它的分配行还在。
  3. 遗留宿主仍停留在旧构建上时,恢复这个子进程。通过 CLI 链接的宿主会在下一次重启时置备一条新隧道;通过 Web 或移动端链接的宿主则保持离线。
  4. 把这个宿主更新到当前构建并启动。确认它会请求恢复,并且能在同一主机名上访问到。
  5. 从一次性阶段中去掉 RELAY_LEGACY_TUNNEL_GRACE_MINUTES。

营销站点部署 ​

nightly 发布时,发布工作流会在桌面作业运行期间,把同一个提交构建成营销站点 Vercel 项目(project)的暂存生产部署,并在发布完成之后用 vercel promote 把它提升上去。Stable 发布不部署营销站点,因为它们可能会提升一个更早的 nightly 提交。

这个作业查找 t3code-marketing 项目,使用的是现有的 VERCEL_TOKEN 和 VERCEL_ORG_ID 密钥。它也会遵守可选的 VERCEL_TEAM_SLUG 变量。Vercel 项目的根目录必须是 apps/marketing。apps/marketing/vercel.ts 里的 Git 部署保持禁用。

移动端 TestFlight 与商店发布 ​

合入 main 会发布兼容的 OTA,并用 testflight 配置文件、以版本 3.0.0 构建 iOS 预发布。构建号会自动递增。已经提交到仓库的应用版本仍然是 App Store 的发布版本;Android 继续使用这个版本。

要发布 iOS,请带上 mode=build、profile=production、platform=ios 以及打算使用的版本(例如 2.0.1),派发 Mobile EAS Production。工作流会先提交并推送这个版本,然后再构建和上传。在 App Store Connect 里选中这次构建,提交 App Store 审核。

要强制重新构建 TestFlight,选择 profile=testflight 和 platform=ios,并把版本留空。在 App Store Connect 里把第一个 3.0.0 构建提交外部测试审核。之后的构建可能仍然需要审核;只上传并不能保证获得外部批准。TestFlight 构建会在 90 天后过期。

发布版和 TestFlight 的二进制都会收到兼容的生产渠道 OTA,并按各自的主版本指纹彼此分开。手动的 mode=update 使用所选配置文件的版本。在把 3.0.0 发布到 App Store 之前,把 build.testflight.env.T3CODE_MOBILE_VERSION(位于 apps/mobile/eas.json 中)推进到下一个尚未发布的主版本,并让这个主版本的第一个测试构建获得批准。

Android APK 镜像 ​

.github/workflows/mobile-android-apk.yml 每小时检查一次 Google Play 的 Alpha 封闭测试轨道。每一个新的测试版都会得到一个 android-beta-<build-number> 预发布,其中包含它的 APK。没有变化的检查不会发布任何东西,中断了的草稿上传会在下一次检查时继续。

常规的 Nightly 发布会带上发布当时所能拿到的、最新一份已经处理完的内部测试 APK。内部构建不会单独产生 Android 发布。如果 Play 凭据或已经处理好的 APK 不可用,或者下载超过五分钟,这次 Nightly 仍然可以不带 APK 发布。Android 的提交仍然进入内部测试;把某个构建提升到 Alpha,才会把它作为测试版发布出来。

这个 APK 是 Play 根据上传的 bundle 生成并签名的通用 APK,因此 GitHub 里不存放签名密钥。在 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON 持有一把具备「发布到生产、排除设备,并使用 Play 应用签名」权限的 Play 服务账户密钥之前,这个作业会跳过自己;下载生成出来的 APK 需要这项权限。该作业读取轨道时不会打开 Play edit,因此可以和 EAS Submit 共用同一个服务账户。

托管 Web 应用的发布部署 ​

托管应用有意不通过 Vercel 的 Git 集成来部署。Web 项目(project)在 apps/web/vercel.ts 里经由 git.deploymentEnabled: false 禁用了自动 Git 部署。.github/workflows/release.yml 在桌面作业运行期间,用 Vercel CLI 把 Web 应用构建成暂存生产部署(--skip-domain),并在 GitHub Release 成功之后,把渠道域名别名到这次部署。

必需的 GitHub Actions 密钥:

  • VERCEL_TOKEN
  • VERCEL_ORG_ID
  • VERCEL_PROJECT_ID

可选的 GitHub Actions 变量:

  • VERCEL_TEAM_SLUG:当希望使用团队 slug,而不是 VERCEL_ORG_ID 密钥时,用它覆盖 Vercel CLI 的 scope。
  • T3CODE_WEB_ROUTER_URL:默认为 https://app.t3.codes。
  • T3CODE_WEB_LATEST_DOMAIN:默认为 latest.app.t3.codes。
  • T3CODE_WEB_NIGHTLY_DOMAIN:默认为 nightly.app.t3.codes。

必需的 Vercel 域名:

  • app.t3.codes:用户打开的路由域名,由 stable 发布更新。
  • latest.app.t3.codes:由 stable 发布更新的渠道别名。
  • nightly.app.t3.codes:由 nightly 发布更新的渠道别名。

路由域名使用 apps/web/vercel.ts 路由。用户通过访问 /__t3code/channel?channel=latest 或 /__t3code/channel?channel=nightly 来选择加入某个渠道;路由器会存下 t3code_web_channel cookie,并把之后发往 app.t3.codes 的请求重写到对应的渠道别名。

发布部署作业会在上传之前改写发布包的版本,让托管应用的「关于」面板显示出这次发布的版本。Stable 部署把同一次部署同时别名到 latest 渠道和路由域名,这样路由规则能保持最新。Nightly 部署只为 nightly 渠道设置别名。这个作业还会传入 VITE_HOSTED_APP_CHANNEL=latest|nightly,用来在「关于」面板里渲染托管应用的更新轨道选择器。更改选择器时,会先经过路由域名上的 /__t3code/channel,在重定向到托管应用根路径之前更新用户的渠道 cookie。

一次性的 Vercel 仪表板设置:

  1. 确认 Web 项目的根目录仍然是 apps/web。
  2. 把上面这三个域名加到 Web 项目。
  3. 如果需要,在仪表板里禁用自动 Git 部署;已提交的 vercel.ts 设置才是事实来源,不过在仪表板里断开 Git 也同样安全。
  4. 跑一次 stable 发布部署,或者手动把当前的 stable 部署设成别名,让 app.t3.codes 指向一份包含 apps/web/vercel.ts 中路由规则的部署。以后的 stable 发布会继续维护这个别名。

Nightly 构建 ​

  • 工作流:.github/workflows/release.yml
  • 触发条件:
    • 每 30 分钟一次的定时检查
    • 手动 workflow_dispatch 并带上 channel=nightly
  • 自动 nightly 要求有新的提交,并且距离上一次 nightly 发布(包括手动 nightly)至少已经过了六小时。
  • 手动 nightly 会跳过时间检查和变更检查。Nightly 运行仍然串行执行。定时运行会先等正在进行的 nightly 结束,再在构建之前检查发布间隔。
  • 运行与带标签的发布流程相同的桌面质量门禁和产物矩阵。
  • 只发布 GitHub 预发布:
    • 当前的标签格式:vX.Y.Z-nightly.YYYYMMDD.<run_number>
    • nightly-v... 只在作为上一次 nightly 的遗留标签时才被接受
    • 发布名称里包含短提交 SHA
    • make_latest 始终是 false
  • 把下一个 stable 的补丁版本当作 nightly 的基数。例如,0.0.17 会在 0.0.18-nightly.* 上产生 nightly。
  • 把 Electron 自动更新元数据发布到专用的 nightly 更新器渠道,因此桌面用户可以独立于 stable,自行选择加入这条轨道。
  • 使用与这次 nightly 相同的版本,把 t3 这个 npm 启动器发布到 nightly 这个 npm dist-tag。
  • 不会把版本号提升提交回 main。

服务端自更新的发布不变量 ​

已经连上的服务端会更新到客户端(client)的精确版本,而不是某个 npm dist-tag。自己更新的服务端,会从 GitHub Release 下载这个版本的 CLI 归档;通过 npm 运行的服务端,则会得到一条要去执行的 t3@<version> 命令(command)。因此,用户能够收到某个已发布的客户端版本之前,必须先有它的发布归档,然后再有它的 npm 启动器。

工作流强制这一顺序:

  1. release 发布带有 CLI 归档和 SHA256SUMS 的 GitHub Release。每个渠道都先上传到草稿,等全部资产都附加上去之后才发布,因此桌面更新源里出现的新版本,归档已经齐备。全部更新靠的就是这一点,好在 npm 还没有这个版本时,先更新后台服务的宿主。发布之后再次运行 release,会替换掉公开发布里的资产,所以上传完成之前,服务更新可能会失败。
  2. publish_cli 在每个渠道上发布 t3@<version> 启动器,这个启动器从那次发布下载。
  3. deploy_web 要等这两者都完成,才把托管渠道切到新的客户端。build_web 会更早用 vercel deploy --prod --skip-domain 构建这个客户端;这不会改动自定义域名,但会移动项目(project)自己的 *.vercel.app 生产主机名。这个主机名位于 Vercel SSO 之后,因此用户只能通过自定义域名拿到这个客户端。

桌面自动更新直接读取 GitHub Release,因此桌面客户端可能比 t3@<version> 出现在 npm 上早几分钟就看到新版本。只有那条复制给通过 npm 运行的服务端的手动命令才依赖 npm,而且 publish_cli 一完成就可以用。

更改发布图时要保住这些依赖。如果先发布客户端,更新服务端这个操作就会指向一个还不存在的包版本。

做发布冒烟测试时,先确认 npm view t3@<version> version 返回的是预期版本,再把新客户端连到仍处于上一版本的服务端,并验证更新操作会重新连上版本匹配的服务端。如果这次发布带有数据库迁移,要验证远程更新会应用这些迁移并重新连上。试验失败时,必须恢复数据库快照,并重启先前的服务端。如果已安装的启动器不支持目标协议,要验证更新会在重启之前停下来,并在服务端那台机器上运行一次 npx t3@<version> service update。这些环境(environment)具备时,也要测试手动方式以及由桌面托管时给出的指引。

桌面自动更新说明 ​

  • 更新器运行时:apps/desktop/src/updates/DesktopUpdates.ts。
  • electron-updater 适配器(adapter):apps/desktop/src/electron/ElectronUpdater.ts。
  • apps/desktop/src/main.ts 只是把更新器的各层接到桌面运行时里。
  • 更新体验:
    • 后台检查按照「启动延迟 + 间隔」执行。
    • 不会自动下载,也不会自动安装。
    • 有更新可用时,桌面界面会显示一个火箭更新按钮;点一次开始下载,下载完成后再点一次,即可重启并安装。
  • 提供方(provider):GitHub Releases(provider: github),在构建时配置。
  • 仓库 slug 的来源:
    • 若已设置,则为 T3CODE_DESKTOP_UPDATE_REPOSITORY(格式为 owner/repo)。
    • 否则使用来自 GitHub Actions 的 GITHUB_REPOSITORY。
  • 更新器所需的发布资产:
    • 平台安装程序(.exe、.dmg、.AppImage、.deb,另外还有供 Squirrel.Mac 更新载荷使用的 macOS .zip)
    • 渠道元数据:stable 发布使用 latest*.yml,nightly 发布使用 nightly*.yml
    • *.blockmap 文件(用于差分下载)
  • macOS 元数据说明:
    • electron-updater 在 stable 上读取 latest-mac.yml,在 nightly 上读取 nightly-mac.yml,Intel 和 Apple Silicon 都是如此。
    • 工作流在发布 GitHub Release 之前,会把各架构的 mac 清单合并成一份按渠道区分的 mac 清单。

Windows 载荷拓扑与更新校验 ​

Windows 把捆绑进来的服务端,以及仅仅是它的运行时外部/原生依赖闭包,打包进 resources/server.asar。被这份归档声明为 unpacked 的原生模块和辅助可执行文件,必须出现在 resources/server.asar.unpacked 下面的对应路径上。Windows 原生后端通过 Electron 就地读取这份归档。打包后的 Windows 构建还会带上 resources/wsl-runtime.tar.gz 以及它的 SHA-256 伴生文件(sidecar):这份 Linux CLI 归档(t3-<version>-linux-<arch>.tar.gz,架构与 Windows 宿主相同)由 Linux 桌面作业构建,并以 --wsl-runtime 交给 Windows 桌面构建,原样复制进去,这样 WSL 跑到的字节与 Linux 用户下载到的完全一致。WSL 会校验这份归档,并把它解压到所选发行版内部的 ~/.t3/wsl-runtime/sha256-<archive-digest>,然后在同一次更新之后再次启动时复用它。

Windows 把 JavaScript 和包元数据留在 app.asar 里面,只把原生库和辅助可执行文件解出来。不要启用整包的智能解包:每一个松散文件都会增加 NSIS 安装的工作量,并且计入载荷上限。

只要下面任何一条不变量被打破,产物构建器就会拒绝这个 Windows 包:

  • resources/server.asar 缺失,或者里面没有服务端入口。
  • ASAR 头里标记为 unpacked 的任何一个文件,没有出现在 resources/server.asar.unpacked 中。
  • 在同架构的 Windows 构建上,打包后的主程序无法从 server.asar 内部、经由与之同级的 .unpacked 加载 fff 原生库。
  • 单独解压出来的那份伴生文件,无法用普通 Node 加载服务端入口。
  • 拿到了 --wsl-runtime 的 Windows 构建漏掉了 WSL 归档或 SHA-256 伴生文件,或者伴生文件的摘要与实际产出的归档不一致。
  • 产出的 WSL 归档不是一份 Linux CLI 发布归档:它必须解压成单独一个 t3-<version>-linux-<arch> 目录,其中有 t3、client/,以及带有 Linux 版 node-pty 二进制的 node_modules/,并且不能带有松散的服务端包(bin.mjs)。
  • 外部的 Windows 资源监视器缺失。
  • 展开后的 Windows 应用包含的文件超过 80 个。

跨架构的 Windows 构建会保留全部结构检查和已解压伴生文件检查,但会跳过执行目标 Electron 二进制。每一个发布目标都必须有一次同架构构建,来实际跑一遍主程序的原生加载探测。

NSIS 差分打包保持启用。伴生文件布局的一次切换,可能带来一次更大的下载;之后的小幅发布仍会保留各自的 blockmap,一次有代表性的、从伴生文件到伴生文件的更新,上限为 60 MB。

0) npm OIDC 可信发布设置(CLI) ​

build_bundle 构建与版本匹配的 t3.tgz,使用的是 node scripts/build-npm-launcher.ts,并把它上传为 npm-launcher 产物。GitHub Release 发布之后,publish_cli 只会检出为这个项目(project)设置 Node.js 所需要的内容,按 build_bundle 输出的产物 ID 下载那一份上传(绝不按名称),核对 tarball 的版本,并对它运行 npm publish --provenance。每次发布只发布这一个包。

检查清单:

  1. 确认 npm 组织拥有包 t3。
  2. 针对 t3,在 npm 的包设置里配置可信发布者(Trusted Publisher):
    • 提供方(provider):GitHub Actions
    • 仓库:本仓库
    • 工作流文件:.github/workflows/release.yml
    • 环境(environment)(若使用):与你的 npm 可信发布配置保持一致
  3. 确认 npm 账户和组织的策略允许这个包使用可信发布。
  4. 创建发布标签 vX.Y.Z 并推送;工作流会先发布 GitHub Release,再用 npm dist-tag latest 发布启动器。
  5. Nightly 运行用 npm dist-tag nightly 发布;preview 运行用 preview。

1) 发布验证与未签名构建 ​

没有试运行用的标签路径。推送任何一个会被接受的非 nightly 标签,包括 v0.0.0-test.1,都会把这次运行归为 stable 渠道。它会发布 t3,使用的 npm dist-tag 是 latest,创建一次真实的 GitHub Release,把托管应用别名到 latest.app.t3.codes 和 app.t3.codes,并且收尾作业还可能把版本号提升提交到 main。不要靠推送测试标签来验证工作流。

工作流没有一种不发布的 workflow_dispatch 模式。想验证检查和构建、又不真正发布时,用常规 CI 或本地的质量门禁。如果想在更低的 stable 风险下把完整的发布图走一遍,可以手动派发 channel=nightly;这样做仍然会发布真实的 nightly npm 包、GitHub 预发布、桌面更新器发布、托管的 nightly 别名和营销站点,但不会更新 stable 应用的别名,也不会把版本号提升提交到 main。只有在可以接受一次真实的 nightly 发布时才去运行它。

手动的 channel=stable 同样是一次真实的 stable 渠道发布。不提供签名密钥,只会让平台产物处于未签名状态;这并不能阻止发布。

2) Apple 签名与公证设置(macOS) ​

工作流使用的必需密钥:

  • CSC_LINK
  • CSC_KEY_PASSWORD
  • APPLE_API_KEY
  • APPLE_API_KEY_ID
  • APPLE_API_ISSUER
  • MACOS_PROVISIONING_PROFILE(经过 base64 编码、带有 Associated Domains 的预置描述文件)

必需的仓库变量:

  • APPLE_TEAM_ID

可选的仓库变量:

  • CLERK_PASSKEY_RP_DOMAINS:用逗号分隔的 RP 域名覆盖值。默认情况下,构建会从生产环境的 Clerk 可发布密钥推导出这个域名。

检查清单:

  1. Apple Developer 账户权限:
    • 团队有权创建 Developer ID 证书。
  2. 为 com.t3tools.t3code 创建一个显式 App ID,并启用 Associated Domains。
  3. 创建一张 Developer ID Application 证书,以及一份为该 App ID 启用了 Associated Domains 的兼容预置描述文件。
  4. 在钥匙串里把证书和私钥导出为 .p12。
  5. 把 .p12 做 Base64 编码,存成 CSC_LINK。
  6. 把预置描述文件做 Base64 编码,存成 MACOS_PROVISIONING_PROFILE。
  7. 把 .p12 的导出密码存成 CSC_KEY_PASSWORD,并把 APPLE_TEAM_ID 设为 10 个字符的 Apple Developer Team ID。
  8. 在 App Store Connect 中创建一个 API 密钥(Team key)。
  9. 填上 API 密钥的值:
    • APPLE_API_KEY:下载到的 .p8 的内容
    • APPLE_API_KEY_ID:Key ID
    • APPLE_API_ISSUER:Issuer ID
  10. 按 T3 Connect 设置 完成 Clerk Native API 和 AASA 的配置。
  11. 重新跑一次标签发布,确认 macOS 产物已经签名并公证,并且带有预期的 com.apple.developer.associated-domains 授权项。

说明:

  • APPLE_API_KEY 以原始密钥文本的形式存在密钥里。
  • 工作流在运行时会把它写到一个临时的 AuthKey_<id>.p8 文件。
  • 工作流会解码 MACOS_PROVISIONING_PROFILE,用 security cms 校验,再交给桌面打包器。
  • 应用内浏览器的通行密钥依赖同一份描述文件。打包器只在描述文件授予了某项授权时才加入该项,因为如果应用声明的授权超出描述文件所允许的范围,macOS 不会启动它。构建日志会写明启用了哪几项。
    • keychain-access-groups(<TEAM_ID>.com.t3tools.t3code.webauthn)用来启用 Touch ID 通行密钥。凡是授予了该团队钥匙串访问组(<TEAM_ID>.*)的描述文件,都覆盖这一项。
    • com.apple.developer.web-browser.public-key-credential 让系统通行密钥面板(iCloud 钥匙串、密码管理器、手机、安全密钥)可以为任意网站服务。Apple 把它作为一项托管能力来授予:由账户持有人(Account Holder)通过 macOS Browsers Passkeys 表单 申请。批准之后,重新生成描述文件,并更新 MACOS_PROVISIONING_PROFILE。

3) Azure Trusted Signing 设置(Windows) ​

工作流使用的必需密钥:

  • AZURE_TENANT_ID
  • AZURE_CLIENT_ID
  • AZURE_CLIENT_SECRET
  • AZURE_TRUSTED_SIGNING_ENDPOINT
  • AZURE_TRUSTED_SIGNING_ACCOUNT_NAME
  • AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME
  • AZURE_TRUSTED_SIGNING_PUBLISHER_NAME

检查清单:

  1. 创建 Azure Trusted Signing 账户和证书配置文件。
  2. 记下这些 ATS 值:
    • 端点
    • 账户名称
    • 证书配置文件名称
    • 发布者名称
  3. 创建或选用一个 Entra 应用注册(服务主体)。
  4. 授予 Trusted Signing 所要求的服务主体权限。
  5. 为这个服务主体创建客户端密钥(client secret)。
  6. 把上面列出的 Azure 密钥加到 GitHub Actions 密钥里。
  7. 重新跑一次标签发布,确认 Windows 安装程序已经签上名。

4) 持续发布检查清单 ​

  1. 选中最新的 nightly 并验证它:拿它的产物跑一遍上面的冒烟测试,并检查 nightly 渠道有没有回归。
  2. 带上 channel=stable 派发 Release 工作流。除非版本需要和 nightly 预览过的那个不同,否则把 version 留空。
  3. 确认 Resolve release commit 的提示里写的是你验证过的那个 nightly 标签和提交。如果中间又发布了更新的 nightly,这次运行会改去构建那一个。
  4. 核对工作流步骤:
    • 预检通过
    • 发布质量检查通过
    • build_bundle 以及全部平台构建通过
    • publish_cli 在发布作业之后,发布与这次完全一致的版本
    • 发布作业上传了预期中的文件
  5. 对下载下来的产物做冒烟测试。

5) 故障排除 ​

  • 预期应已签名的 macOS 构建实际上未签名:
    • 检查全部 Apple 密钥以及 APPLE_TEAM_ID 都已填写,并且不是空值。
    • 确认预置描述文件属于 APPLE_TEAM_ID.com.t3tools.t3code,并且包含 Associated Domains。
  • 预期应已签名的 Windows 构建实际上未签名:
    • 检查全部 Azure ATS 密钥和身份验证密钥都已填写,并且不是空值。
  • 构建因签名错误失败:
    • 去掉密钥后再试一次,确认未签名这条路径仍然能走通。
    • 再核对证书和描述文件的名称,以及租户凭据和客户端凭据(client credentials)。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools