跳转至

发布检查清单

面向维护者。使用 T3 Code?请参阅 docs/user。

本文档涵盖稳定版和夜间版桌面发布的统一发布流程。

Beta 是产品标签,不是发布渠道或版本后缀。发布渠道仍然只有 stable 和 nightly。要发布 0.1.0 Beta,请使用 channel=stable 和 version=0.1.0。稍后移除 Beta 标签是一个品牌变更,与版本号无关。

该工作流的作用

  • 工作流:.github/workflows/release.yml
  • 触发方式:
    • 手动 workflow_dispatch 并使用 channel=stable,这是发布稳定版的常规方式。stable 和 nightly 触发必须选择 main;preview 可以选择任意分支。渠道默认为 preview,因此省略选择不会发布稳定版。
    • 推送与 v*.*.* 匹配的标签,用于发布指定提交的稳定版
    • 每 30 分钟进行一次计划性夜间检查
    • 手动 workflow_dispatch 并使用 channel=nightly
    • 手动 workflow_dispatch 并使用 channel=preview,这是维护者的测试列车。它会对一个最终用户绝不会收到的提交执行完整的发布流程(构建、签名、公证、冒烟测试、发布),这就是未合并分支或有风险变更在合并前获得真实发布运行的方式。它使用夜间版的版本号方案构建触发提交,并带有 preview 预发布标识符(0.0.41-preview.<日期>.<运行>),同时发布一个 GitHub 预发布版以及 npm 包到 preview dist-tag 下。Preview 不在计划中,没有默认的 npm dist-tag 指向它,其桌面构建不携带任何更新源,也不附加任何更新器清单(latest*.yml、nightly*.yml、blockmap),因此 stable 或 nightly 安装无法被提供 preview 更新。进入 preview 的唯一方式是手动下载发布版、npx t3@preview、为安装脚本设置 T3CODE_CHANNEL=preview,或在终端中运行 t3 update --channel preview;每次都会打印警告,并且当正在运行的构建本身不是 preview 时,CLI 会要求确认。该发布本身被命名为维护者测试构建,其正文是警告而非自动生成的说明:未合并分支历史的变更日志不是变更日志,而 nightly 和 stable 的说明不受影响,因为每个系列都在自己的渠道内解析其上一个标签。托管的 Web 应用、AUR 和 Discord 公告会被跳过。保留它;空闲时不会花费任何成本。
  • 手动稳定版发布构建的是最新已发布夜间版的提交,而不是 main 的最新 HEAD。 夜间版是发布候选版:先验证夜间版,再提升它。在您验证期间,对 main 的合并会继续落地,且不会泄漏到稳定版构建中。
    • 版本默认采用夜间版预览过的版本(0.0.39-nightly.* 以 0.0.39 发布)。 传入 version 输入可覆盖它,例如用于次要版本提升。
    • 当 GitHub Release 发布时,稳定版标签会在夜间版的提交上创建。
    • 手动推送 vX.Y.Z 标签仍然有效,并且会精确构建该标签对应的提交。当要发布的提交不是最新的夜间版时使用它,例如发布分支上的精选修复。
  • 在构建产物的同时运行 lint、typecheck 和测试。发布会等待所有检查通过。
  • 在打包客户端之前读取共享的生产 T3 Connect 中继 URL 和 Clerk 客户端配置。
  • 在 build_bundle 作业中一次性构建平台无关的 JS(服务器 bundle、Web 客户端、Electron 主进程),并将其作为 js-bundle 产物交给每个平台作业;平台作业只负责打包,因此没有运行器会重新构建它。
  • 同时为两个渠道并行构建六个桌面产物,每个产物都是独立的作业(desktop_<平台>_<架构>,调用一次 release-desktop.yml),仅以 bundle 为门槛。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,并连接到一个预置数据库,同时打开一个线程(scripts/smoke-desktop-app.ts)。如果后end或 bundle 无法启动的构建会在此失败,因此不会发布任何内容。scripts/fixtures/desktop-launch 中的数据库是有意保留的旧快照:打包好的服务器必须将其向前迁移,就像对升级用户所做的那样。不要在 schema 变更时重新生成它。
  • 发布一个包含所有生成文件的 GitHub Release。
    • 带有 X.Y.Z 之后后缀的稳定版标签(例如 1.2.3-alpha.1)会作为 GitHub 预发布版发布。
    • 只有普通的稳定版 X.Y.Z 发布会被标记为仓库的最新版本。
    • Nightly 运行始终是 GitHub 预发布版,从不会被标记为最新。
    • 自动生成的发布说明会固定到同一渠道中的上一个标签,因此 stable 会与上一个 stable 标签比较,nightly 会与上一个 nightly 标签比较。
    • 发布作业的 Write release notes 步骤(scripts/generate-release-notes.ts)会构建正文:一个计算出的标题行、来自 gpt-6.1-terra 的简短摘要,以及折叠在 <details> 下的 GitHub 生成列表。它需要 OPENAI_API_KEY 机密。桌面更新弹窗最多显示七条亮点以及其余修复和性能改进的数量。仅移动端的提交会计入发布总数,但不会成为亮点。所提供发布的年龄来自更新源的 releaseDate。夜间版 Discord 帖子显示 <details> 之前的部分。如果摘要生成失败,发布将使用 GitHub 生成的说明。
  • 在发布资产中包含 Electron 自动更新元数据(例如 latest*.yml、nightly*.yml 和 *.blockmap)。
  • 在每个渠道上,为该目标的桌面产物在同一作业中为五个目标构建自包含的 CLI 压缩包(t3-<版本>-<平台>-<架构>.tar.gz,Windows 上为 .zip),并将它们连同 SHA256SUMS 文件一起附加到 GitHub Release:macOS arm64、Linux x64 和 arm64、Windows x64 和 arm64。每个压缩包都在其自身架构的硬件上进行了冒烟测试,除 Windows 压缩包外,其余也都在那里构建和签名。没有 macOS x64 压缩包:Node 单可执行文件在 x64 macOS 上不受支持(SEA 文档仅列出 macOS arm64),并且该二进制文件在启动时会段错误;x64 桌面应用是 Electron,不受影响。
    • 该压缩包将服务器作为 Node 单可执行文件(scripts/build-cli-archive.ts)保存,因此解压它既不需要 Node、npm,也不需要编译器。这是 T3 Code 管理运行时的唯一形式:桌面的 SSH 环境、启动服务、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 或 64 KiB 页大小的内核上加载。在 nodejs/node#67048 修复后恢复。运行时变更需要新的 T3 发布版本,因为已安装的运行时按该版本进行缓存。
    • macOS 压缩包使用 Developer ID 证书签名,并在存在 Apple 机密时进行公证(否则使用临时签名,这仍然可以从 curl/tar 安装运行)。Windows 可执行文件使用与安装程序相同的 Azure Trusted Signing 设置。macOS 压缩包中的每个原生插件也都经过签名,因为强化运行时拒绝未签名的库。
    • 每个压缩包都会在其构建运行器上(Windows 则在 verify_runner 上)被解压并执行(scripts/smoke-cli-archive.ts)。
  • 在 GitHub Release 发布后,使用 OIDC 可信发布从同一工作流文件发布 t3 npm 包。它只是一个启动器(scripts/npm-launcher/t3.mjs):首次运行时,它从 GitHub Release 下载其确切版本的 CLI 压缩包,对照 SHA256SUMS 验证,并将其解压到 <T3 home>/runtime/versions/<版本>,这是安装脚本和 t3 update 共享的布局。可执行文件不在 npm 上。它们曾经以 @t3code/t3-<平台>-<架构> 可选依赖的形式存在,但 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 托管应用渠道
  • 签名是可选的,并根据每个平台的机密自动检测。

拉取请求 macOS 预览

为 PR 添加 preview:mac 标签会发布一个已签名、通过公证的 Apple Silicon DMG,启用 T3 Connect,并发布到滚动发布的 desktop-preview 预发布版本,同时适用于 fork 的 PR。该标签是对其所应用提交的一次性请求:可信工作流一旦拿到构建产物就会移除该标签,后续推送不会触发构建,直到维护者再次应用该标签。因此,每个已签名的预览都是维护者针对单个提交作出的决定,这一点很重要,因为产物携带 Developer ID 签名。为贡献者担保可让其带标签的提交获得签名;但这并不是长期授权。构建流程被拆分,因此 Developer ID 证书永远不会与 PR 代码共用同一个作业:

  • .github/workflows/desktop-macos-preview.yml 在 pull_request 上运行,不含任何机密,且仅构建来自 PR 的 JS bundle(即 release.yml 生成的 js-bundle 工件)。
  • .github/workflows/desktop-macos-preview-publish.yml 在来自 main 的 workflow_run 上运行。除非 PR 处于打开状态、仍带有该标签、其 head 是已构建的提交,并且作者是机器人、协作者或列在 .github/VOUCHED.td 中(该文件从默认分支读取,因此 PR 无法为自己担保),否则该工作流会拒绝执行。然后它通过签出到 main 的 release-desktop.yml 打包并签名 bundle,因此打包逻辑、原生辅助程序以及 Electron/desktop 依赖均来自 main,而不是 PR。只有 .env.example 中的版本号和公开 T3 Connect 标识符会作为数据从 PR 提交中读取,以使签名应用的 passkey 授权与 bundle 匹配。更改打包逻辑的 PR 必须改用上述 channel=preview 发布轨道。

在将 bundle 交给签名 runner 之前,可信工作流会验证其 ZIP 条目,并且只接受 server/dist 和 desktop/dist-electron 下的常规文件,以及指向这些根目录的目录条目。该工件不能覆盖打包代码或已安装的依赖。在签名 runner 上,bundle 会被复制到应用中,而绝不会被执行。发布工作流中的 pull_request_target 清理作业会在 PR 关闭,或标签在构建尚未消费它之前被手动移除时,删除下载内容,并且永远不会签出 PR 代码。

必需的发布凭据

稳定发布除了下文记录的平台和部署凭据外,还需要以下 GitHub Actions 机密:

  • RELEASE_APP_ID
  • RELEASE_APP_PRIVATE_KEY

finalize 作业使用这些凭据以 Release App 身份将对齐的包版本提交并推送到 main。GitHub Release 发布使用仓库作用域的工作流令牌,因此它拥有独立于共享 Release App 安装的速率限制配额。

T3 Connect 中继部署

该中继是一个共享控制平面,其版本与客户端发布相互独立。稳定版和 nightly 客户端构建必须指向同一个中继,这样用户在切换发布频道时看到的是相同的已链接环境。

.github/workflows/deploy-relay.yml 在每次推送到 main 时部署 Alchemy 的 prod 阶段。稳定版和 nightly 发布在构建 desktop、CLI 或托管 web 工件之前,会从现有的 production GitHub Actions 环境中读取中继 URL 和 Clerk 客户端配置。该环境只允许 main 和 v* 标签,因此可能从任何分支运行的预览发布会从 .env.example 获取相同的公开标识符;请使其与 production 值保持同步。每个频道都会通过仓库级 CLOUDFLARE_API_TOKEN 从 Alchemy 状态读取中继客户端追踪配置,因此将该令牌移入 production 会破坏预览发布。

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

  • 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 环境机密:

  • T3CHAT_LINK_SECRET:用于 T3 Chat 账户关联。它必须与 T3 Chat 上的 T3CODE_LINK_SECRET 匹配。

更改变量或机密后,请从 main 手动运行 Deploy T3 Connect relay 工作流,并保持 force 未选中。Alchemy 会比较 Worker 读取的值,并在某个值发生变化时重新部署它。仅在需要重新部署未检测到变化的资源时,才勾选 force。

账户级仓库凭据由 Alchemy 在预置中继阶段时消耗;它们不会绑定到中继 Worker 中。生产部署使用 Axiom 个人访问令牌,因此 AXIOM_ORG_ID 必须与 AXIOM_TOKEN 一起提供。prod 阶段拥有保留的 PlanetScale 数据库。本地个人阶段会从该数据库预置隔离分支,并且永远不会由 CI 部署。生产环境会将配置好的中继 API 和隧道 DNS 区域作为保留的 Cloudflare 资源采用。个人阶段引用生产环境拥有的区域。

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

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

同一仓库中带有 relay:preview 标签的 PR,在 relay-preview 环境批准后,会通过 .github/workflows/relay-preview.yml 部署 pr-<number> 阶段。移除标签或关闭 PR 会运行 .github/workflows/relay-preview-cleanup.yml,它会销毁该阶段并清理其管理的隧道和 DNS 记录。nightly 运行会销毁已关闭且带标签的 PR 中拆除失败的阶段。Fork 的 PR 不会获得预览。

受管隧道清理发布

首次生产部署时保持 RELAY_TUNNEL_CLEANUP_MODE=off。该部署应用可空分配迁移并添加恢复端点。Web 和移动客户端无需协调发布。CLI 和桌面服务器构建必须在启用清理之前到达用户手中,因为这些构建会注册恢复并在唤醒后替换已删除的隧道。

  1. 以清理 off 部署 relay 和迁移。
  2. 发布服务器构建并确认当前主机注册恢复。较旧的主机仍被标记为旧版,并且仅在下面的旧版开关下成为候选。
  3. 设置 dry-run,运行一次 relay 部署,并在多次清扫中读取清扫计数器(scanned、wouldDelete、skippedLegacy、skippedOrphan、failed、truncated)。每次清扫都会将它们以及活动的 mode 记录为 Axiom 中 relay.managed_endpoint_reaper.sweep span 上的 relay.managed_endpoint_reaper.* 属性。
  4. 运行下面的一次性主机金丝雀测试。
  5. 只有在金丝雀测试无需服务器重启即可恢复后,才设置 enabled。

该任务每五分钟运行一次,并为失去连接器的隧道提供五分钟的宽限期,因此候选隧道通常在其下线后五到十分钟内被移除。从未连接过的隧道会等待一个小时。一次清扫最多尝试 100 次删除,因此积压需要更长时间。更改 RELAY_TUNNEL_CLEANUP_MODE(包括在事件期间关闭清理)需要不带 force 的 relay 部署。在下一个清扫 span 上确认新的 mode。

要回滚,请在任何主机降级之前将清理设置为 off 并运行一次 relay 部署。在当前服务器构建仍在使用时,保持恢复端点已部署。可空列可以保留。

旧版隧道清理

旧版隧道属于从未注册恢复的主机,通常是那些在恢复构建发布之前就已离线的主机。一旦 Cloudflare 报告这些隧道宕机或从未连接超过 7 天,RELAY_LEGACY_TUNNEL_CLEANUP_MODE 就会删除它们。它独立于 RELAY_TUNNEL_CLEANUP_MODE,所有其他检查仍然适用。

被删除的旧版隧道会保留其分配,因此其主机名得以保留。当主机恢复时:

  • 在带恢复的构建上,连接器会被拒绝,主机会请求在同一主机名处替换隧道。
  • 在带 CLI 链接的旧构建上,启动时会配置一个新隧道。
  • 在通过 web 或移动设备链接的旧构建上,主机会保持离线,直到该计算机上的 T3 Code 更新。

在启用旧版清理之前,发布显示离线原因的 web 和移动构建,以便受影响主机的用户知道该怎么做。relay 在包含此更改的首次部署中添加 tunnel_released_at 分配列;旧版开关保持 off,直到你设置它。

  1. 使用只读的 Cloudflare token 运行 vp run --filter t3code-relay tunnels:census。它会统计每个 relay 阶段中的隧道。清理器只能看到自己阶段的隧道,因此其余的需要手动清理。
  2. 将旧版模式设置为 dry-run,部署,并读取清扫 span 上的 wouldDeleteLegacy、legacyOver30Days、totalDown 和 totalInactive 一天。wouldDeleteLegacy 只统计一次清扫检查过的隧道,每个状态最多 500 个。totalDown 和 totalInactive 是 Cloudflare 对本阶段隧道中宕机超过五分钟和从未连接超过一小时的计数。它们包含清理器跳过的隧道,因此是积压量的上限。wouldDeleteLegacy 在每次清扫的 scanned 中所占的份额可估算该总量中有多少符合条件。
  3. 运行下面的一次性主机金丝雀测试中的旧版步骤。
  4. 在启用之前,确认显示“在该计算机上更新 T3 Code”消息的 web 和移动构建已上线。如果没有它们,旧主机丢失隧道的用户只会看到其处于离线状态。
  5. 将旧版模式设置为 enabled。一次清扫最多删除 100 个隧道,每次 4 个,并在 90 秒后停止启动新的删除。如果每次清扫完成其 100 个删除,那么 20,000 的积压大约需要 17 小时。留意 deletedLegacy、attempted、failed 和 truncated。如果 attempted 远低于 100 且设置了 truncated,则说明清扫提前停止:要么时间预算用完,要么 Cloudflare 对删除进行了速率限制。计数器不会说明是哪一种;relay 会为每次失败的删除记录一条包含 Cloudflare 错误的警告日志。

在 Axiom 中,按 name == "relay.managed_endpoint_reaper.sweep" 过滤 relay traces 数据集,并随时间绘制 attributes.custom.relay.managed_endpoint_reaper.* 字段的图表。

如果发生以下任一情况,请将旧版模式设置回 off 并部署:

  • failed 在每次清扫中持续高于几个。阅读警告日志以了解 Cloudflare 错误。
  • 用户报告某个环境在更新了该计算机上的 T3 Code 并重启后,仍显示离线并带有更新消息。
  • 清扫运行期间 relay 请求错误上升。删除操作与请求处理程序共享 Postgres 连接池。

关闭旧版模式会停止新的旧版删除;RELAY_TUNNEL_CLEANUP_MODE 在其为 enabled 时继续删除具有恢复功能的主机的隧道。已删除的隧道保持删除状态;其主机将按照上文所述恢复。

一次性主机金丝雀测试

此测试尚未针对真实的 Cloudflare 账户运行过。请针对一次性 relay 阶段、测试 Cloudflare 账户、一次性主机和一次性 T3 home 运行它。在生产环境清理通过之前,保持 off 或 dry-run。不要停止日常使用的 T3 服务器。

  1. 使用清理 dry-run 部署一次性阶段。通过 web 或移动设置链接第一个一次性环境,并确认其隧道健康且已注册恢复。
  2. 停止该主机,并在不同的本地端口上重启同一个 T3 home。确认公共主机名能访问新端口,并且不会向旧端口发送任何内容。
  3. 使用早于恢复注册的服务器构建链接第二个一次性环境。捕获其托管的 cloudflared 子进程 PID,确认它属于该主机,然后用 kill -STOP <legacy-pid> 仅暂停该子进程。等待 Cloudflare 报告其宕机超过五分钟。
  4. 从其服务器日志中捕获第一个环境的 cloudflared 子进程 PID,确认归属,然后用 kill -STOP <first-pid> 暂停它。等待 Cloudflare 报告其宕机超过五分钟。
  5. 确认 dry-run 将第一个隧道计入 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 项目的暂存生产部署,并在发布公开后使用 vercel promote 将其提升。稳定发布不会部署营销站点,因为它们可以提升较早的 Nightly 提交。

该任务使用现有的 VERCEL_TOKEN 和 VERCEL_ORG_ID 密钥查找 t3code-marketing 项目。它也会遵循可选的 VERCEL_TEAM_SLUG 变量。Vercel 项目的根目录必须为 apps/marketing。Git 部署在 apps/marketing/vercel.ts 中保持禁用。

移动端 TestFlight 与商店发布

合入 main 的提交会发布兼容的 OTA,并使用 testflight profile 以版本 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 构建进行外部 beta 审核。后续构建可能仍需要审核;仅上传并不保证获得外部批准。TestFlight 构建会在 90 天后过期。

发布版和 TestFlight 二进制文件都会收到兼容的生产通道 OTA,并按各自的主版本指纹区分。手动 mode=update 使用所选 profile 的版本。在向 App Store 发布 3.0.0 之前,请将 apps/mobile/eas.json 中的 build.testflight.env.T3CODE_MOBILE_VERSION 推进到下一个未发布的主版本,并让其第一个 beta 构建获得批准。

Android APK 镜像

.github/workflows/mobile-android-apk.yml 每小时检查 Google Play Alpha 封闭测试轨道。每个新的 beta 会获得一个包含其 APK 的 android-beta-<build-number> 预发布版本。未发生变更的检查不会发布任何内容,中断的草稿上传会在下一次检查时继续。

常规 Nightly 发布会包含发布时最新已处理的内部测试 APK。内部构建不会创建单独的 Android 发布。如果 Play 凭据或已处理的 APK 不可用,或下载耗时超过五分钟,Nightly 仍可发布而不附带 APK。Android 提交通常继续进入内部测试;将构建提升到 Alpha 即可将其作为 beta 发布。

该 APK 是 Play 根据上传的 bundle 生成并签名的通用 APK,因此 GitHub 中不存放签名密钥。在 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON 持有具有“Release to production, exclude devices, and use Play App Signing”权限的 Play 服务账号密钥之前,该任务会跳过自身;下载生成的 APK 需要该权限。该任务无需打开 Play 编辑会话即可读取轨道,因此它可以共享 EAS Submit 所使用的服务账号。

托管 Web 应用发布部署

托管应用有意不通过 Vercel 的 Git 集成来部署。Web 项目在 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 作用域。
  • 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:用户打开的路由域名,由稳定发布更新。
  • latest.app.t3.codes:通道别名,由稳定发布更新。
  • nightly.app.t3.codes:通道别名,由 Nightly 发布更新。

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

发布部署任务会在上传前重写发布包版本,以便托管应用的 About 面板显示发布版本。稳定部署会将同一部署同时别名到 latest 通道和路由域名,从而使路由规则保持最新。Nightly 部署只别名到 nightly 通道。该任务还会传入 VITE_HOSTED_APP_CHANNEL=latest|nightly,从而在 About 面板中渲染托管更新轨道选择器。更改选择器会通过路由域名上的 /__t3code/channel 进行导航,以便在重定向到托管应用根路径之前更新用户的通道 cookie。

一次性 Vercel 控制台设置:

  1. 确认 Web 项目根目录仍为 apps/web。
  2. 将上述三个域名添加到 Web 项目中。
  3. 如需,可在仪表盘中禁用自动 Git 部署;已提交的 vercel.ts 设置是唯一事实来源,但在仪表盘中断开 Git 连接同样是安全的。
  4. 执行一次稳定版部署,或手动为当前稳定版部署设置别名,使 app.t3.codes 指向包含 apps/web/vercel.ts 中路由规则的部署。未来的稳定版发布会持续更新此别名。

夜间构建

  • 工作流:.github/workflows/release.yml
  • 触发条件:
    • 每 30 分钟定时检查
    • 手动 workflow_dispatch,且 channel=nightly
  • 自动夜间构建要求有新的提交,且距离上次夜间构建发布(包括手动夜间构建)至少已过去六小时。
  • 手动夜间构建会绕过时间与变更检查。夜间构建运行仍然串行化。定时运行会等待当前夜间构建结束后,再检查发布间隔,然后才开始构建。
  • 运行与标签发布流程相同的桌面质量门禁和工件矩阵。
  • 仅发布 GitHub 预发布版本:
    • 当前标签格式:vX.Y.Z-nightly.YYYYMMDD.<run_number>
    • 仅接受 nightly-v... 作为旧版 previous-nightly 标签
    • 发布名称中包含短提交 SHA
    • make_latest 始终为 false
  • 使用下一个稳定补丁版本作为夜间构建的基础版本。例如,0.0.17 会生成基于 0.0.18-nightly.* 的夜间构建。
  • 将 Electron 自动更新元数据发布到专用的 nightly 更新通道,以便桌面用户可以独立于稳定版选择加入该轨道。
  • 使用相同的夜间版本将 t3 npm 启动器发布到 nightly npm dist-tag。
  • 不会将版本号提升提交回 main。

服务器自更新发布不变量

已连接的服务器会更新到与客户端完全相同的版本,而不是某个 npm dist-tag。自行更新的服务器会从 GitHub Release 下载该版本的 CLI 归档;通过 npm 运行的服务器则会获得一条 t3@<version> 命令来执行。因此,每个已发布的客户端版本都必须先提供其发布归档,然后提供其 npm 启动器,用户才能接收到该客户端。

该工作流强制执行以下顺序:

  1. release 发布包含 CLI 归档和 SHA256SUMS 的 GitHub Release。每个通道都上传到草稿,并且只有所有资产附加完毕后才发布,因此桌面更新源中的新版本已经拥有其归档。Update everything 依赖此机制在 npm 获得该版本之前更新后台服务主机。发布后重新运行 release 会替换公开发布版中的资产,因此在上传完成之前,服务更新可能会失败。
  2. publish_cli 在每个通道上发布 t3@<version> 启动器,该启动器会从该发布版下载。
  3. deploy_web 依赖上述两者,之后才会将托管通道迁移到新客户端。build_web 更早时使用 vercel deploy --prod --skip-domain 构建该客户端,这不会改动自定义域名,但会移动项目自身的 *.vercel.app 生产主机名。该主机名位于 Vercel SSO 之后,因此用户只能通过自定义域名访问该客户端。

桌面自动更新直接读取 GitHub Release,因此桌面客户端可以在 t3@<version> 发布到 npm 之前几分钟就看到新版本。只有针对 npm 运行服务器的复制手动命令依赖于 npm,并且该命令在 publish_cli 完成后即可使用。

在更改发布图时,请保留这些依赖关系。如果先发布客户端,Update server 操作就会指向一个尚不存在的包版本。

进行一次发布冒烟测试:确认 npm view t3@<version> version 返回预期版本,然后将新客户端连接到运行上一版本的服务器,并验证更新操作会重新连接到匹配的服务器。当该发布新增数据库迁移时,请验证远程更新会应用这些迁移并重新连接。如果试用失败,必须恢复数据库快照并重启之前的服务器。如果已安装的启动器不支持目标协议,请验证更新会在重启前停止,并在服务器机器上运行一次 npx t3@<version> service update。在这些环境可用时,还要测试手动或桌面管理的操作指引。

桌面自动更新说明

  • 更新器运行时:apps/desktop/src/updates/DesktopUpdates.ts。
  • electron-updater 适配器:apps/desktop/src/electron/ElectronUpdater.ts。
  • apps/desktop/src/main.ts 仅将更新器各层接入桌面运行时。
  • 更新用户体验:
    • 后台检查按启动延迟 + 时间间隔运行。
    • 不自动下载或安装。
    • 当有可用更新时,桌面 UI 会显示一个火箭更新按钮;点击一次开始下载,下载完成后再次点击即可重启/安装。
  • 提供方:在构建时配置为 GitHub Releases(provider: github)。
  • 仓库 slug 来源:
    • 如果设置了 T3CODE_DESKTOP_UPDATE_REPOSITORY(格式为 owner/repo),则使用它。
    • 否则使用来自 GitHub Actions 的 GITHUB_REPOSITORY。
  • 更新器所需的发布资产:
    • 平台安装包(.exe、.dmg、.AppImage、.deb,以及用于 Squirrel.Mac 更新负载的 macOS .zip)
    • 通道元数据:稳定版使用 latest*.yml,夜间版使用 nightly*.yml
    • *.blockmap 文件(用于差异下载)
  • macOS 元数据说明:
    • electron-updater 在稳定版读取 latest-mac.yml,在夜间版读取 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 附带文件: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 头部中任何标记为解包的文件在 resources/server.asar.unpacked 中缺失。
  • 在相同架构的 Windows 构建中,打包后的主程序无法通过其 .unpacked 同级目录,从 server.asar 内部加载 fff 原生库。
  • 隔离的、解压后的 sidecar 程序无法使用纯 Node 加载服务端入口。
  • 指定了 --wsl-runtime 的 Windows 构建省略了 WSL 归档或 SHA-256 sidecar 文件,或者 sidecar 摘要与生成的归档不匹配。
  • 生成的 WSL 归档不是 Linux CLI 发布归档:它必须解包到一个单独的 t3-<version>-linux-<arch> 目录,其中包含 t3、client/ 和 node_modules/,并带有 Linux node-pty 二进制文件,且不得带有松散的 server bundle(bin.mjs)。
  • 外部 Windows 资源监视器缺失。
  • 解包后的 Windows 应用包含超过 80 个文件。

跨架构的 Windows 构建保留所有结构和解压 sidecar 检查,但跳过执行目标 Electron 二进制文件。每个发布目标的同架构构建必须执行主程序的原生加载探测。

NSIS 差分打包保持启用。sidecar 布局转换可能会产生一次较大的下载;后续的小版本发布会保留其 blockmap,具有代表性的 sidecar 到 sidecar 更新的最大值上限为 60 MB。

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

build_bundle 使用 node scripts/build-npm-launcher.ts 构建版本匹配的 t3.tgz,并将其作为 npm-launcher 产物上传。GitHub Release 发布后,publish_cli 只检出设置项目 Node.js 所需的内容,按 build_bundle 输出的产物 ID(绝不按名称)下载该确切的上传物,检查 tarball 版本,然后对其运行 npm publish --provenance。这是每次发布唯一发布的包。

检查清单:

  1. 确认 npm 组织拥有包 t3。
  2. 对于 t3,在 npm 包设置中配置受信发布者:
    • 提供商:GitHub Actions
    • 仓库:本仓库
    • 工作流文件:.github/workflows/release.yml
    • 环境(如果使用):与你的 npm 受信发布配置匹配
  3. 确保 npm 账户和组织策略允许该包进行受信发布。
  4. 创建发布标签 vX.Y.Z 并推送;工作流发布 GitHub Release,然后发布带有 npm dist-tag latest 的启动器。
  5. 夜间运行使用 npm dist-tag nightly 发布;预览运行使用 preview 发布。

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

不存在 dry-run 标签路径。推送任何被接受的非 nightly 标签(包括 v0.0.0-test.1)都会将运行归类为稳定频道。它会以 npm dist-tag latest 发布 t3,创建真实的 GitHub Release,将托管应用别名指向 latest.app.t3.codes 和 app.t3.codes,并可能在 finalize 作业中提交版本提升到 main。不要推送测试标签来验证工作流。

该工作流没有不发布的 workflow_dispatch 模式。使用普通 CI 或本地质量门来验证检查和构建,而无需发布。为了在较低的稳定风险下演练完整的发布图,手动调度 channel=nightly;这仍然会发布真实的 nightly npm 包、GitHub 预发布、桌面更新器发布、托管 nightly 别名和营销站点,但不会更新稳定应用别名,也不会向 main 提交版本提升。只在可以接受真实 nightly 发布时才运行它。

手动 channel=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 publishable key 派生域。

检查清单:

  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 密钥(团队密钥)。
  9. 添加 API 密钥值:
    • APPLE_API_KEY:下载的 .p8 的内容
    • APPLE_API_KEY_ID:密钥 ID
    • APPLE_API_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 将其作为托管功能授予:账户持有人通过 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. 为服务主体创建客户端机密。
  6. 在 GitHub Actions 机密中添加上述 Azure 机密。
  7. 重新运行 tag 发布,并确认 Windows 安装程序已签名。

4) 持续发布检查清单

  1. 选择最新的 nightly 并验证:对其构建产物运行上述冒烟测试,并检查 nightly 频道中是否存在回归问题。
  2. 使用 channel=stable 触发 Release 工作流。除非版本应与 nightly 预览的版本不同,否则将 version 留空。
  3. 确认 Resolve release commit 通知指明的是你已验证的 nightly tag 和提交。如果在此期间发布了更新的 nightly,则该运行将改为构建那个版本。
  4. 验证工作流步骤:
    • preflight 通过
    • 发布质量检查通过
    • build_bundle 及所有平台构建通过
    • publish_cli 在 release 作业之后发布确切的发布版本
    • release 作业上传预期文件
  5. 对下载的构建产物运行冒烟测试。

5) 故障排查

  • 预期应签名时 macOS 构建未签名:
    • 检查所有 Apple 机密及 APPLE_TEAM_ID 均已填充且非空。
    • 确认预置描述文件属于 APPLE_TEAM_ID.com.t3tools.t3code 且包含 Associated Domains。
  • 预期应签名时 Windows 构建未签名:
    • 检查所有 Azure ATS 和身份验证机密均已填充且非空。
  • 构建因签名错误失败:
    • 移除机密后重试,以确认未签名路径仍然可用。
    • 重新检查证书/配置文件名称以及租户/客户端凭据。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code