發版檢查清單
這一頁是寫給維護者看的。如果你是 T3 Code 的使用者,請看 docs/user。
這份文件說明 stable 與 nightly 桌面版發版共用的同一套 release workflow。
這個 workflow 做了什麼#
- Workflow:
.github/workflows/release.yml - 觸發方式:
- 手動
workflow_dispatch並指定channel=stable,這是發 stable 的一般做法。Stable 和 nightly 的 dispatch 必須選main;preview 可以選任何分支。Channel 的預設值是 preview,所以就算漏選,也不會發出 stable 版本。 - push 符合
v*.*.*的 tag,用來針對某個明確指定的 commit 發 stable 版本 - 排程的 nightly 檢查,每 30 分鐘一次
- 手動
workflow_dispatch並指定channel=nightly - 手動
workflow_dispatch並指定channel=preview,這是維護者的測試列車(test train)。它會針對一個絕對不可以讓終端使用者收到的 commit,把整個發版流程(build、簽章、notarize、smoke、發佈)實際跑一遍;還沒 merge 的分支或有風險的變更,就是靠這個方式在進main之前先跑過一次真正的發版。它用 nightly 的版號規則來 build 觸發它的那個 commit,prerelease 識別字是preview(0.0.41-preview.<date>.<run>),並且發佈一個 GitHub prerelease,以及掛在previewdist-tag 底下的 npm 套件。Preview 不在排程裡,沒有任何預設的 npm dist-tag 指向它,它的桌面版 build 不帶更新來源(update feed),也不會附上任何 updater manifest(latest*.yml、nightly*.yml、blockmap),所以 stable 或 nightly 的安裝不會被提示更新到它。要裝到 preview 只有這幾種方式:手動下載該 release、npx t3@preview、對安裝腳本設定T3CODE_CHANNEL=preview,或是在終端機執行t3 update --channel preview;每一種都會印出警告,而且當目前執行中的 build 本身不是 preview 時,CLI 會要求確認。這個 release 本身的名稱會標明是維護者測試 build,內文是一段警告而不是自動產生的 release notes:未 merge 分支的歷史所組成的 changelog 不算是 changelog;nightly 和 stable 的 notes 不受影響,因為每個系列都只在自己的 channel 裡找前一個 tag。Hosted web app、AUR 和 Discord 公告都會跳過。請保留這個機制;閒置時它不花任何成本。
- 手動
- 手動發 stable 時,build 的是「最新一個已發佈 nightly」的那個 commit,而不是
main的 HEAD。Nightly 就是 release candidate:先驗證 nightly,再把它升級(promote)成 stable。在你驗證的期間,merge 會繼續進到main,但絕對不會混進 stable 的 build。- 版號預設是 nightly 預告的那個版號(
0.0.39-nightly.*會以0.0.39發出)。要覆寫的話傳入versioninput,例如要升 minor 版號時。 - Stable tag 會在 GitHub Release 發佈時,建立在 nightly 的那個 commit 上。
- 手動 push 一個
vX.Y.Ztag 的做法仍然可用,而且 build 的就是被 tag 的那個 commit。當要發的 commit 不是最新的 nightly 時用這個方式,例如 release 分支上 cherry-pick 進來的修正。
- 版號預設是 nightly 預告的那個版號(
- 在 build artifact 的同時執行 lint、typecheck 和測試。發佈會等所有檢查都完成。
- 在打包各個 client 之前,先讀取共用的 production T3 Connect relay URL 與 Clerk client 設定。
- 與平台無關的 JS(server bundle、web client、Electron main)只在
build_bundlejob 裡 build 一次,然後以js-bundleartifact 的形式交給每個平台 job;平台 job 只負責打包,所以沒有任何 runner 會重新 build 它。 - 兩個 channel 都會平行 build 六個桌面版 artifact,每一個都是獨立的 job(
desktop_<platform>_<arch>,各呼叫一次release-desktop.yml),跑在與目標相同架構的硬體上,而且只需要等 bundle 完成。Windows 的 job 會把相同架構的 Linux CLI archive 內嵌進去當作 WSL runtime,它們在執行到一半時等的是那個 artifact,而不是整個 Linux job:- macOS
arm64DMG - macOS
x64DMG - Linux
x64與arm64的 AppImage 和.deb,由同一次 electron-builder 執行產生。.deb透過 electron-updater 在 app 內更新,electron-updater 會用dpkg安裝它。 - Windows
x64與arm64的 NSIS 安裝程式
- macOS
- 發佈一個 GitHub Release,包含所有產生出來的檔案。
- 在
X.Y.Z後面帶有後綴的 stable tag(例如1.2.3-alpha.1)會發佈成 GitHub prerelease。 - 只有單純的 stable
X.Y.Z版本會被標記為 repository 的 latest release。 - Nightly 一律是 GitHub prerelease,絕對不會被標記為 latest。
- 自動產生的 release notes 會固定以同一個 channel 的前一個 tag 為比較基準,所以 stable 是跟前一個 stable tag 比,nightly 是跟前一個 nightly tag 比。
- 在
- Release 的檔案裡包含 Electron 自動更新的 metadata(例如
latest*.yml、nightly*.yml和*.blockmap)。 - 每個平台各 build 一個自給自足的 CLI archive(
t3-<version>-<platform>-<arch>.tar.gz,Windows 上是.zip),和該目標的桌面版 artifact 在同一個 job 裡 build,並連同一個SHA256SUMS檔案附加到 GitHub Release 上。每個 channel 都會做,共五個目標:macOS arm64、Linux x64 與 arm64、Windows x64 與 arm64。每個 archive 都是在與自己相同架構的硬體上 build、簽章並做 smoke test。沒有 macOS x64 的 archive:x64 macOS 不支援 Node single-executable(SEA 文件只把 macOS 列為 arm64),而且那個 binary 一啟動就會 segfault;x64 的桌面 app 是 Electron,不受影響。- 這個 archive 裡的 server 是一個 Node single-executable(
scripts/build-cli-archive.ts),所以解開它不需要 Node、npm,也不需要編譯器。T3 Code 管理 runtime 時只用這一種形式:桌面版的 SSH environment、開機服務(boot service)、t3 update和安裝腳本,全都是下載這個 archive 並對照SHA256SUMS驗證。npm 套件是給自己執行npx t3或npm install -g t3的人用的,內容與 archive 相同;產品本身沒有任何地方是從 npm 安裝的。curl | sh的安裝程式是scripts/install.sh和scripts/install.ps1;marketing 網站在 build 時把它們複製到自己的public/(apps/marketing/scripts/stage-install-scripts.mjs),並在t3.codes/install.sh和/install.ps1提供下載。 - 這個執行檔是用支援
--build-sea的 Node 來 build 的(VP_NODE_VERSION=26.8.2,與apps/server/vite.config.ts裡的SEA_NODE_VERSION保持一致),而 repo 本身仍然維持在engines.node指定的版本。 - 有 Apple secrets 時,macOS 的 archive 會用 Developer ID 憑證簽章並 notarize(沒有的話就用 ad hoc 簽章,透過
curl/tar安裝時仍然可以執行)。Windows 的執行檔使用和安裝程式相同的 Azure Trusted Signing 設定。macOS archive 裡的每一個原生 addon 也都會簽章,因為 hardened runtime 會拒絕載入未簽章的函式庫。 - 每個 archive 在上傳之前,都會先在 build 它的 runner 上解開並實際執行(
scripts/smoke-cli-archive.ts)。
- 這個 archive 裡的 server 是一個 Node single-executable(
- 在同一個 workflow 檔案裡,以 OIDC trusted publishing 把 CLI 發佈到 npm,內容與 GitHub Release 上的是完全相同的位元組:
scripts/build-npm-platform-packages.ts把五個 CLI archive 解開成@t3code/t3-<platform>-<arch>套件(每個都設定了os/cpu,所以 npm 只會安裝符合的那一個),並產生t3launcher;launcher 的bin/t3.js把這些套件列為optionalDependencies,並 exec 已安裝的執行檔。因此npx t3只需要 Node 來執行 launcher,執行 server 完全不需要。node apps/server/scripts/cli.ts publish會先發佈各平台套件,最後才發佈 launcher;在這之前會先對全部套件跑一輪--dry-run,這樣驗證或 scope 的錯誤會在任何東西上線之前就失敗。- stable 版本發佈到 npm dist-tag
latest - nightly 版本發佈到 npm dist-tag
nightly - preview 版本發佈到 npm dist-tag
preview,除非指名要求,否則沒有任何東西會解析到它 - 一次性設定:npm 上必須有
@t3code這個 scope(org),而且t3和每一個@t3code/t3-<platform>-<arch>套件都要為這個 workflow 檔案註冊一個 trusted publisher(見下方)。
- stable 版本發佈到 npm dist-tag
- 在桌面版 job 執行的同時,於 Vercel 上 build hosted web app,並且只在 release 發佈之後才讓它上線:
- stable 版本會被 alias 到 hosted app 的
latestchannel - nightly 版本會被 alias 到 hosted app 的
nightlychannel
- stable 版本會被 alias 到 hosted app 的
- 簽章是選用的,會依各平台的 secrets 自動偵測。
Pull request 的 macOS 預覽版#
在 PR 加上 preview:mac label,會發佈一個已簽章、已 notarize、啟用 T3 Connect 的 Apple Silicon DMG 到滾動更新的 desktop-preview prerelease,fork 來的 PR 也適用。這個 label 是針對「加上 label 當下的那個 commit」的一次性請求:受信任的 workflow 拿到 build 之後就會移除它,之後的 push 不會再 build,直到維護者重新加上 label。因此每一個已簽章的預覽版,都是維護者針對單一 commit 做的決定;這一點很重要,因為產出物帶有 Developer ID 簽章。為某位貢獻者 vouch,代表他被加上 label 的 commit 可以被簽章;這並不是長期有效的授權。Build 被拆成兩段,讓 Developer ID 憑證絕對不會和 PR 的程式碼出現在同一個 job 裡:
.github/workflows/desktop-macos-preview.yml在pull_request上執行,不帶任何 secrets,只從 PR build 出 JS bundle(和release.yml產生的js-bundleartifact 相同)。.github/workflows/desktop-macos-preview-publish.yml在workflow_run上從main執行。除非 PR 仍然開啟、仍然帶著 label、它的 head 就是被 build 的那個 commit,而且作者是 bot、collaborator,或列在.github/VOUCHED.td裡(從預設分支讀取,所以 PR 不能替自己 vouch),否則它會拒絕執行。接著它透過在maincheckout 出來的release-desktop.yml來打包並簽章這個 bundle,所以打包流程、原生 helper,以及 Electron/桌面版的相依套件都來自main,而不是 PR。只有版號和.env.example裡公開的 T3 Connect 識別字是從 PR 的 commit 讀取的,而且是當作資料來讀,這樣簽章後 app 的 passkey entitlement 才會和 bundle 相符。會改到打包流程的 PR,必須改用上面提到的channel=preview發版列車。
把 bundle 交給負責簽章的 runner 之前,受信任的 workflow 會先驗證它的 ZIP 項目,只接受 server/dist 和 desktop/dist-electron 底下的一般檔案,以及通往這兩個根目錄的目錄項目。這個 artifact 無法覆寫打包程式碼或已安裝的相依套件。在簽章 runner 上,bundle 只會被複製進 app,絕對不會被執行。當 PR 關閉時,或是 label 在 build 用到它之前就被手動移除時,publish workflow 裡的 pull_request_target 清理 job 會移除下載檔;這個 job 絕對不會 checkout PR 的程式碼。
發版必要的憑證#
Stable 發版除了下面各節記載的平台與部署憑證之外,還需要這些 GitHub Actions secrets:
RELEASE_APP_IDRELEASE_APP_PRIVATE_KEY
Finalize job 用它們以 Release App 的身分,把對齊後的套件版號 commit 並 push 到 main。GitHub Release 的發佈使用的是 repository 範圍的 workflow token,這樣它的 rate-limit 配額就和共用的 Release App installation 分開。
T3 Connect relay 部署#
Relay 是共用的 control plane,版本與 client 的發版分開管理。Stable 和 nightly 的 client build 必須指向同一個 relay,使用者切換 release channel 時才會看到相同的已連結 environment。
.github/workflows/deploy-relay.yml 會在每次 push 到 main 時部署 Alchemy 的 prod stage。Release workflow 在 build 桌面版、CLI 或 hosted web 的 artifact 之前,會從既有的 production GitHub Actions environment 讀取 relay URL 和 Clerk client 設定。
Relay 部署共用的必要 repository variables:
CLOUDFLARE_ACCOUNT_IDPLANETSCALE_ORGANIZATIONAXIOM_ORG_ID
Relay 部署共用的必要 repository secrets:
CLOUDFLARE_API_TOKENPLANETSCALE_API_TOKEN_IDPLANETSCALE_API_TOKENAXIOM_TOKEN
production environment 必要的 variables:
RELAY_API_ZONE_NAMERELAY_TUNNEL_ZONE_NAMECLERK_PUBLISHABLE_KEYCLERK_JWT_AUDIENCECLERK_JWT_TEMPLATECLERK_CLI_OAUTH_CLIENT_IDAPNS_ENVIRONMENTAPNS_TEAM_IDAPNS_KEY_IDAPNS_BUNDLE_ID
production environment 選用的 variables:
RELAY_DOMAIN:要覆寫推導出來的relay.<RELAY_API_ZONE_NAME>網域時使用RELAY_TUNNEL_CLEANUP_MODE:值為off、dry-run或enabled。沒設定或空白時使用off。
production environment 必要的 secrets:
CLERK_SECRET_KEYAPNS_PRIVATE_KEY
Relay Worker 是在部署時讀取這些 variables 和 secrets 的。當只有其中一個值改變時,Alchemy 不會重新部署 Worker(alchemy-run/alchemy#1831),所以一次沒有改到 relay 程式碼的 main push,會讓舊的值繼續留著。改了其中任何一個之後,請從 main 手動執行 Deploy T3 Connect relay workflow,並勾選 force。
帳號範圍的 repository 憑證是 Alchemy 在佈建 relay stage 時使用的;它們不會被綁進 relay Worker。Production 部署使用的是 Axiom 的 personal access token,所以 AXIOM_ORG_ID 必須和 AXIOM_TOKEN 一起提供。prod stage 擁有那個被保留(retained)的 PlanetScale 資料庫。本機的個人 stage 會從它佈建出隔離的 branch,而且絕對不會由 CI 部署。Production 會把設定好的 relay API 與 tunnel DNS zone 接管(adopt)為被保留的 Cloudflare 資源。個人 stage 則是參照 production 所擁有的 zone。
開發者是在本機部署個人 stage,而不是透過 pull request 的自動化流程:
vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local
Managed tunnel 清理機制的上線步驟#
第一次 production 部署時,請讓 RELAY_TUNNEL_CLEANUP_MODE=off。這次部署會套用 nullable allocation 的 migration,並加入 recovery endpoint。Web 和 mobile client 不需要配合發版。CLI 和桌面版的 server build 則必須在啟用清理之前先送到使用者手上,因為這些 build 才會註冊 recovery,並且在喚醒之後替換掉已被刪除的 tunnel。
- 以 cleanup
off部署 relay 和 migration。 - 發佈 server build,並確認目前的 host 都有註冊 recovery。較舊的 host 會維持被標記為 legacy,絕對不會成為清理的候選對象。
- 設成
dry-run,執行一次強制的 relay 部署,然後觀察多次 sweep 的計數器(scanned、wouldDelete、skippedLegacy、skippedOrphan、failed、truncated)。每次 sweep 都會把這些計數器連同當下生效的mode,以relay.managed_endpoint_reaper.*屬性記錄在 Axiom 裡它自己的relay.managed_endpoint_reaper.sweepspan 上。 - 執行下面的拋棄式 host canary 測試。
- 只有在 canary 不需要重啟 server 就能復原之後,才設成
enabled。
這個 job 每五分鐘執行一次;對於失去 connector 的 tunnel 有五分鐘的寬限期,所以候選對象通常會在斷線後五到十分鐘被移除。從未連線過的 tunnel 會等一小時。一次 sweep 最多嘗試刪除 100 個,所以有積壓時會花更久。變更 RELAY_TUNNEL_CLEANUP_MODE(包括在事故期間把清理關掉)都需要一次強制的 relay 部署。請在下一個 sweep span 上確認新的 mode。
要 rollback 的話,先把 cleanup 設成 off 並執行一次強制的 relay 部署,之後才去降版任何 host。只要目前的 server build 還在使用中,就讓 recovery endpoint 繼續部署著。那些 nullable 欄位可以留著。
拋棄式 host canary 測試#
這個測試還沒有對真正的 Cloudflare 帳號執行過。請用拋棄式的 relay stage、測試用的 Cloudflare 帳號、拋棄式的 host 和拋棄式的 T3 home 來執行。在它通過之前,production 的 cleanup 請維持在 off 或 dry-run。不要停掉每天在用的 T3 server。
- 以 cleanup
dry-run部署拋棄式 stage。透過 web 或 mobile 的設定頁連結第一個拋棄式 environment,並確認它的 tunnel 狀態正常,而且已經註冊 recovery。 - 停掉那台 host,然後用同一個 T3 home 在另一個本機 port 上重新啟動。確認公開的 hostname 會連到新的 port,而且不會送任何東西到舊的 port。
- 用一個早於 recovery 註冊功能的 server build,連結第二個拋棄式 environment。記下它所管理的
cloudflared子行程 PID,確認這個行程屬於那台 host,然後只暫停那一個子行程:kill -STOP <legacy-pid>。等到 Cloudflare 回報它已斷線超過五分鐘。 - 從第一個 environment 的 server log 取得它的
cloudflared子行程 PID,確認歸屬,然後用kill -STOP <first-pid>暫停它。等到 Cloudflare 回報它已斷線超過五分鐘。 - 確認 dry-run 把第一個 tunnel 算進
wouldDelete,把第二個算進skippedLegacy。 - 在拋棄式 stage 上把 cleanup 設成
enabled,並用--force部署。在測試用的 Cloudflare 帳號裡確認第一個 tunnel 已被刪除,而 legacy tunnel 仍然存在。 - 用
kill -CONT <first-pid>恢復第一個子行程。確認執行中的 server 偵測到連續被拒絕、發出 recovery 請求,並且不需要重啟就能在同一個 hostname 上恢復連線。 - 用
kill -CONT <legacy-pid>恢復 legacy 子行程,並確認它的 tunnel 重新連上。 - 在大範圍上線之前,用一台拋棄式筆電做一次實體的睡眠與喚醒,重複以上測試。
Marketing 網站部署#
發 nightly 時,release workflow 會在桌面版 job 執行的同時,把同一個 commit build 成 marketing 網站 Vercel 專案的一個 staged production deployment,並在 release 發佈之後用 vercel promote 讓它上線。Stable 發版不會部署 marketing 網站,因為 stable 升級的可能是比較舊的 nightly commit。
這個 job 使用既有的 VERCEL_TOKEN 和 VERCEL_ORG_ID secrets 來查找 t3code-marketing 專案。它也會採用選用的 VERCEL_TEAM_SLUG variable。Vercel 專案的 root directory 必須是 apps/marketing。Git 部署在 apps/marketing/vercel.ts 裡維持停用。
Hosted web app 的發版部署#
Hosted app 刻意不透過 Vercel 的 Git 整合來部署。Web 專案在 apps/web/vercel.ts 裡用 git.deploymentEnabled: false 停用了自動 Git 部署。.github/workflows/release.yml 會在桌面版 job 執行的同時,用 Vercel CLI 把 web app build 成一個 staged production deployment(--skip-domain),並在 GitHub Release 成功之後,把各 channel 的網域 alias 到它。
必要的 GitHub Actions secrets:
VERCEL_TOKENVERCEL_ORG_IDVERCEL_PROJECT_ID
選用的 GitHub Actions variables:
VERCEL_TEAM_SLUG:偏好使用 team slug 而不是VERCEL_ORG_IDsecret 時,用它覆寫 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:使用者開啟的 router 網域,由 stable 發版更新。latest.app.t3.codes:channel alias,由 stable 發版更新。nightly.app.t3.codes:channel alias,由 nightly 發版更新。
Router 網域使用 apps/web/vercel.ts 裡的 routes。使用者造訪 /__t3code/channel?channel=latest 或 /__t3code/channel?channel=nightly 來選擇加入某個 channel;router 會存下 t3code_web_channel cookie,並把之後對 app.t3.codes 的請求 rewrite 到對應的 channel alias。
Release 的部署 job 會在上傳之前改寫 release 套件的版號,這樣 hosted app 的 About 面板才會顯示這次發版的版號。Stable 部署會把同一個 deployment 同時 alias 到 latest channel 和 router 網域,讓 router 規則保持在最新狀態。Nightly 部署只 alias nightly channel。這個 job 還會傳入 VITE_HOSTED_APP_CHANNEL=latest|nightly,它會讓 About 面板顯示 hosted 版的更新軌道(update track)選擇器。切換這個選擇器時,會先經過 router 網域上的 /__t3code/channel,這樣使用者的 channel cookie 會先被更新,然後才重新導向到 hosted app 的根路徑。
一次性的 Vercel dashboard 設定:
- 確認 web 專案的 root directory 仍然是
apps/web。 - 把上面三個網域加到 web 專案。
- 想要的話,可以在 dashboard 裡停用自動 Git 部署;commit 進 repo 的
vercel.ts設定才是唯一事實來源(source of truth),不過在 dashboard 裡中斷 Git 連結也是安全的。 - 執行一次 stable 發版部署,或是手動 alias 目前的 stable deployment,讓
app.t3.codes指向一個包含apps/web/vercel.ts裡 router 規則的 deployment。之後的 stable 發版會讓這個 alias 保持在最新狀態。
Nightly build#
- Workflow:
.github/workflows/release.yml - 觸發方式:
- 排程檢查,每 30 分鐘一次
- 手動
workflow_dispatch並指定channel=nightly
- 自動的 nightly 需要有新的 commit,而且距離上一個 nightly 發佈(包含手動的 nightly)至少六小時。
- 手動的 nightly 會略過時間與變更的檢查。Nightly 的執行仍然是序列化的。排程的執行會等進行中的 nightly 完成,然後在 build 之前檢查發佈間隔。
- 執行和 tag 發版流程相同的桌面版品質關卡與 artifact matrix。
- 只發佈 GitHub prerelease:
- 目前的 tag 格式:
vX.Y.Z-nightly.YYYYMMDD.<run_number> nightly-v...只會被當作舊格式的「前一個 nightly tag」來接受- release 名稱包含短的 commit SHA
make_latest一律是false
- 目前的 tag 格式:
- 用下一個 stable patch 版號當作 nightly 的基底。例如
0.0.17會產生0.0.18-nightly.*的 nightly。 - 把 Electron 自動更新的 metadata 發佈到專用的
nightlyupdater channel,這樣桌面版使用者可以獨立於 stable 之外,自行選擇加入這個軌道。 - 用相同的 nightly 版號,把 CLI 的 npm 套件(
t3和@t3code/t3-<platform>-<arch>)發佈到 npm 的nightlydist-tag。 - 不會把版號變更 commit 回
main。
Server 自我更新的發版不變條件#
已連線的 server 會更新到和 client 完全相同的版本,而不是更新到某個 npm dist-tag。因此每一個發出去的桌面版或 hosted client 版本,都必須先在 npm 上有對應的 t3@<version> 套件,使用者才可以收到那個 client。
Workflow 強制執行這個順序:
publish_cli在每個 channel 上,都把確切的 release 版本發佈到 npm。release相依於publish_cli,之後才在 GitHub Releases 公開桌面版 artifact。deploy_web相依於release,之後才把 hosted channel 切到新的 client。build_web會更早用vercel deploy --prod --skip-domainbuild 出那個 client,這個指令不會動到自訂網域,但會移動專案自己的*.vercel.appproduction hostname。那個 hostname 在 Vercel SSO 後面,所以使用者只會透過自訂網域拿到 client。
修改 release graph 時請保留這些相依關係。如果先發佈 client,Update server 這個動作就會指向一個還不存在的套件版本。
做發版 smoke test 時,先確認 npm view t3@<version> version 回傳預期的版本,然後把新的 client 連到一個還在前一版的 server,並驗證更新動作會重新連上對應版本的 server。當這次發版新增了資料庫 migration 時,要驗證遠端更新會套用它們並重新連線。試跑失敗時,必須還原資料庫 snapshot 並重新啟動前一版的 server。如果已安裝的 launcher 不支援目標 protocol,要驗證更新會在重啟之前停下來,然後在 server 那台機器上執行一次 npx t3@<version> service update。有對應的 environment 可用時,也請測試手動管理或由桌面版管理的引導流程。
桌面版自動更新的注意事項#
- Updater runtime:
apps/desktop/src/updates/DesktopUpdates.ts。 electron-updateradapter:apps/desktop/src/electron/ElectronUpdater.ts。apps/desktop/src/main.ts只負責把 updater 的 layer 接進桌面版 runtime。- 更新的使用體驗:
- 背景檢查在啟動後延遲一段時間執行,之後每隔一段時間執行一次。
- 不會自動下載或安裝。
- 有可用的更新時,桌面版 UI 會顯示一個火箭圖示的更新按鈕;按一下開始下載,下載完成後再按一下就會重啟並安裝。
- Provider:GitHub Releases(
provider: github),在 build 時設定。 - Repository slug 的來源:
- 有設定的話用
T3CODE_DESKTOP_UPDATE_REPOSITORY(格式owner/repo)。 - 否則用 GitHub Actions 的
GITHUB_REPOSITORY。
- 有設定的話用
- Updater 需要的 release 檔案:
- 各平台的安裝檔(
.exe、.dmg、.AppImage、.deb,另外還有 macOS 的.zip,供 Squirrel.Mac 當作更新 payload) - channel metadata:stable 版本是
latest*.yml,nightly 版本是nightly*.yml *.blockmap檔案(用於差異下載)
- 各平台的安裝檔(
- macOS metadata 的注意事項:
- 不論 Intel 或 Apple Silicon,
electron-updater在 stable 上讀的是latest-mac.yml,在 nightly 上讀的是nightly-mac.yml。 - Workflow 會在發佈 GitHub Release 之前,把各架構的 mac manifest 合併成一份該 channel 專用的 mac manifest。
- 不論 Intel 或 Apple Silicon,
Windows payload 的結構與更新驗證#
Windows 把 bundle 好的 server,以及它在 runtime 才外部載入的相依套件與原生相依套件的 closure(只有這些),打包在 resources/server.asar 裡。該 archive 宣告為 unpacked 的原生模組和 helper 執行檔,必須出現在 resources/server.asar.unpacked 底下的對應路徑。Windows 原生的 backend 透過 Electron 直接就地讀取這個 archive。打包後的 Windows build 還會附上 resources/wsl-runtime.tar.gz 和它的 SHA-256 sidecar:也就是 Linux CLI archive(t3-<version>-linux-<arch>.tar.gz,架構與 Windows host 相同),由 Linux 桌面版 job build 出來,再以 --wsl-runtime 交給 Windows 桌面版 build,原封不動複製進去,這樣 WSL 執行的就是和 Linux 使用者下載到的完全相同的位元組。WSL 會驗證這個 archive,並把它解開到所選 distro 裡的 ~/.t3/wsl-runtime/sha256-<archive-digest>,之後同一個更新版本再次啟動時就重複使用它。
Windows 把 JavaScript 和套件 metadata 留在 app.asar 裡面,只把原生函式庫和 helper 執行檔 unpack 出來。請避免啟用整個套件的 smart unpacking:每一個散落在外的檔案都會增加 NSIS 安裝的工作量,並且計入 payload 上限。
只要下列任何一項不變條件被破壞,artifact builder 就會拒絕這個 Windows 套件:
resources/server.asar不存在,或是裡面沒有 server 的進入點。- ASAR header 裡標記為 unpacked 的任何檔案,沒有出現在
resources/server.asar.unpacked裡。 - 在相同架構的 Windows build 上,打包後的 primary 無法從
server.asar內部,透過它旁邊的.unpacked目錄載入 fff 原生函式庫。 - 隔離並解開後的 sidecar,無法用單純的 Node 載入 server 的進入點。
- 有傳入
--wsl-runtime的 Windows build 漏掉了 WSL archive 或 SHA-256 sidecar,或是 sidecar 的 digest 和產出的 archive 不相符。 - 產出的 WSL archive 不是 Linux CLI 的 release archive:它必須解開成單一個
t3-<version>-linux-<arch>目錄,裡面有t3、client/和帶有 Linux node-pty binary 的node_modules/,而且不可以帶有散落在外的 server bundle(bin.mjs)。 - 外部的 Windows resource monitor 不存在。
- Unpack 出來的 Windows 應用程式包含超過 80 個檔案。
跨架構的 Windows build 會保留所有結構檢查和解開 sidecar 的檢查,但會跳過執行目標架構的 Electron binary。每個發版目標都必須有一個相同架構的 build 來實際跑過 primary 的原生載入探測(native-load probe)。
NSIS 的差異打包(differential packaging)維持啟用。Sidecar 配置方式的轉換可能會造成一次性的較大下載;之後的小改版仍然保有 blockmap,一次具代表性的 sidecar 到 sidecar 更新,上限是 60 MB。
0) npm OIDC trusted publishing 設定(CLI)#
Workflow 會對下載回來的 CLI archive 執行 node scripts/build-npm-platform-packages.ts,接著執行 node apps/server/scripts/cli.ts publish --packages-dir npm-packages;後者會對每一個 @t3code/t3-<platform>-<arch>.tgz 執行 npm publish,最後才對 launcher t3.tgz 執行。這個腳本發佈的是它自己 build 出來的 tarball,而不是目錄:不管 files 怎麼寫,npm publish <dir> 都會把 node_modules/ 從 tarball 裡去掉,而執行檔正是從那裡載入原生 addon 的。每次發版會發佈七個套件:t3、@t3code/t3-darwin-arm64、@t3code/t3-darwin-x64、@t3code/t3-linux-arm64、@t3code/t3-linux-x64、@t3code/t3-win32-arm64、@t3code/t3-win32-x64。
檢查清單:
- 確認 npm org 擁有
t3這個套件,而且 npm 上有@t3codescope(沒有的話就建立這個 org)。 - 針對
t3和每一個@t3code/t3-<platform>-<arch>套件,在 npm 的套件設定裡設定 Trusted Publisher(從未發佈過的套件,要先發佈一次或放一個佔位版本,這個設定才會出現;publish_cli裡的--dry-run步驟會回報哪些名稱仍然被拒絕):- Provider:GitHub Actions
- Repository:這個 repo
- Workflow file:
.github/workflows/release.yml - Environment(如果有用到):和你的 npm trusted publishing 設定一致
- 確保 npm 帳號和 org 的政策允許每一個套件使用 trusted publishing。
- 建立 release tag
vX.Y.Z並 push;workflow 會:- build 五個 CLI archive 並做 smoke test
- 用這些 archive build 出 npm 套件
- 以 npm dist-tag
latest發佈它們
- Nightly 的執行以 npm dist-tag
nightly發佈;preview 的執行則是preview。
1) 發版驗證與未簽章的 build#
沒有「試跑用的 tag」這條路。Push 任何會被接受的非 nightly tag(包括 v0.0.0-test.1),都會讓這次執行被歸類為 stable channel。它會以 npm dist-tag latest 發佈 t3、建立一個真正的 GitHub Release、把 hosted app alias 到 latest.app.t3.codes 和 app.t3.codes,而且 finalize job 可能會把版號變更 commit 到 main。不要為了驗證 workflow 而 push 測試用的 tag。
這個 workflow 沒有「不發佈」的 workflow_dispatch 模式。想在不發版的情況下驗證檢查和 build,請用一般的 CI 或本機的品質關卡。想把完整的 release graph 跑一遍、又要降低對 stable 的風險,可以手動 dispatch channel=nightly;這仍然會發佈真正的 nightly npm 套件、GitHub prerelease、桌面版 updater release、hosted 的 nightly alias 和 marketing 網站,但不會更新 stable 的 app alias,也不會把版號變更 commit 到 main。只有在可以接受真的發出一個 nightly 版本時才執行它。
手動的 channel=stable 同樣是一次真正的 stable channel 發版。不提供簽章用的 secrets 只會讓各平台的 artifact 沒有簽章;並不會阻止發佈。
2) Apple 簽章與 notarization 設定(macOS)#
Workflow 使用的必要 secrets:
CSC_LINKCSC_KEY_PASSWORDAPPLE_API_KEYAPPLE_API_KEY_IDAPPLE_API_ISSUERMACOS_PROVISIONING_PROFILE(base64 編碼的 provisioning profile,需包含 Associated Domains)
必要的 repository variables:
APPLE_TEAM_ID
選用的 repository variables:
CLERK_PASSKEY_RP_DOMAINS:以逗號分隔的 RP 網域覆寫值。預設情況下,build 會從 production 的 Clerk publishable key 推導出網域。
檢查清單:
- Apple Developer 帳號的存取權:
- Team 有權限建立 Developer ID 憑證。
- 為
com.t3tools.t3code建立一個 explicit App ID,並啟用 Associated Domains。 - 建立一張
Developer ID Application憑證,以及一個與該 App ID 相容、已啟用 Associated Domains 的 provisioning profile。 - 從 Keychain 把憑證和私鑰匯出成
.p12。 - 把
.p12做 base64 編碼,存成CSC_LINK。 - 把 provisioning profile 做 base64 編碼,存成
MACOS_PROVISIONING_PROFILE。 - 把
.p12的匯出密碼存成CSC_KEY_PASSWORD,並把APPLE_TEAM_ID設成 10 個字元的 Apple Developer Team ID。 - 在 App Store Connect 建立一把 API key(Team key)。
- 加入 API key 的各個值:
APPLE_API_KEY:下載回來的.p8的內容APPLE_API_KEY_ID:Key IDAPPLE_API_ISSUER:Issuer ID
- 完成 T3 Connect 設定裡的 Clerk Native API 與 AASA 設定。
- 重新跑一次 tag 發版,確認 macOS 的 artifact 已簽章並 notarize,而且包含預期的
com.apple.developer.associated-domainsentitlement。
備註:
APPLE_API_KEY在 secrets 裡是以 key 的原始文字儲存。- Workflow 在執行時把它寫到一個暫時的
AuthKey_<id>.p8檔案。 - Workflow 會解碼
MACOS_PROVISIONING_PROFILE,用security cms驗證它,再把它交給桌面版的 packager。
3) Azure Trusted Signing 設定(Windows)#
Workflow 使用的必要 secrets:
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRETAZURE_TRUSTED_SIGNING_ENDPOINTAZURE_TRUSTED_SIGNING_ACCOUNT_NAMEAZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAMEAZURE_TRUSTED_SIGNING_PUBLISHER_NAME
檢查清單:
- 建立 Azure Trusted Signing 帳號和 certificate profile。
- 記下 ATS 的各個值:
- Endpoint
- Account name
- Certificate profile name
- Publisher name
- 建立或選擇一個 Entra app registration(service principal)。
- 授予 service principal Trusted Signing 所需的權限。
- 為 service principal 建立一組 client secret。
- 把上面列出的 Azure secrets 加到 GitHub Actions secrets。
- 重新跑一次 tag 發版,確認 Windows 安裝程式已簽章。
4) 每次發版的檢查清單#
- 選定最新的 nightly 並驗證它:對它的 artifact 執行上面的 smoke test,並檢查 nightly channel 有沒有 regression。
- Dispatch Release workflow 並指定
channel=stable。除非版號要和 nightly 預告的不同,否則version留空。 - 確認
Resolve release commit的 notice 指出的,正是你驗證過的 nightly tag 和 commit。如果這段期間有更新的 nightly 發佈,這次執行 build 的會是那一個。 - 檢查 workflow 的各個步驟:
- preflight 通過
- release 的品質檢查通過
build_bundle和所有平台的 build 通過publish_cli在 release job 之前發佈了確切的 release 版本- release job 上傳了預期的檔案
- 對下載回來的 artifact 做 smoke test。
5) 疑難排解#
- macOS build 預期有簽章,結果沒有:
- 檢查所有 Apple secrets 以及
APPLE_TEAM_ID都有填寫,而且不是空的。 - 確認 provisioning profile 屬於
APPLE_TEAM_ID.com.t3tools.t3code,並且包含 Associated Domains。
- 檢查所有 Apple secrets 以及
- Windows build 預期有簽章,結果沒有:
- 檢查所有 Azure ATS 與驗證用的 secrets 都有填寫,而且不是空的。
- Build 因為簽章錯誤而失敗:
- 把 secrets 移除後重試,確認未簽章的流程仍然可以運作。
- 重新檢查憑證與 profile 的名稱,以及 tenant/client 的憑證。
本頁譯自 docs/operations/release.md(英文原文,版本 d0da615)。標示「本站補充」的區塊不在原文裡。