Binary release 與 installer 模式
Binary release 是一條直接把預編譯執行檔掛在 GitHub Release 下供使用者下載的發版通道,跳過 package registry。它解決的問題是:當套件不是函式庫而是 CLI binary,下游不需要重新編譯、也不一定有對應語言的 toolchain 時,需要一條「平台無關、即拿即用」的安裝路線。本篇用 zhtw-mcp 為陪跑案例,公開協作軌跡可直接對照 issue #35 與 PR #40。
為什麼需要這條通道
CLI binary 跟函式庫的下游使用脈絡不同。函式庫需要被同語言專案 import,自然走 registry(npm install、pip install、cargo add)。CLI binary 的目標讀者是「只想跑這個工具」的人,他們不一定有對應 toolchain、不想花時間編譯,也不會接受「先裝開發環境才能用」的入場門檻。
Binary release 的契約是:上游負責編譯、下游負責下載。這條契約成立需要三個前提同時滿足:
- CI 能在多平台 cross-compile 出可執行檔(macOS x64/arm64、Linux x64/arm64、Windows x64)
- 編譯產物有穩定 URL,下游可以用一行 shell 命令取得
- 安裝過程不依賴開發環境(不需要 git clone、不需要 build toolchain)
達成這三點需要一個 release 工具鏈把 build matrix、artifact 上傳、installer script 產生包成一個 tag-driven 的 workflow。Rust 生態用 cargo-dist、Go 生態用 goreleaser、語言中性的方案則是手刻 GitHub Actions matrix。三者觸發條件相同(push semver tag)、產物落點相同(GitHub Release assets),只在 build pipeline 細節有差。
Tag-driven release 的鏈路
Tag-driven 的核心設計:push tag 是發版意圖的唯一訊號。這條因果鏈每一環都要實作起來才會通:
1維護者 push tag vX.Y.Z ↓
2 → release.yml workflow 觸發(tag pattern 匹配)
3 → cross-compile to N platforms(GitHub Actions matrix)
4 → 打包成 <pkg>-x86_64-apple-darwin.tar.xz 等 N 個 archive
5 → 產生 <pkg>-installer.sh / .ps1(內嵌指向上述 archive 的 download URL)
6 → 建立 GitHub Release vX.Y.Z
7 → 上傳所有 archive + installer 為 release assets
8 → GitHub 自動把 vX.Y.Z 的 assets 也鏡射到 /releases/latest/download/這條鏈路上每個節點都是一塊要設定的工作:
- Tag pattern:cargo-dist 預設匹配
**[0-9]+.[0-9]+.[0-9]+*,符合 semver 才會觸發 - Build matrix:在
Cargo.toml的[workspace.metadata.dist]宣告targets = [...],cargo-dist 會展開成對應的 GitHub Actions runners - Pre-build hooks:如果編譯前需要產生程式碼或下載資料,要透過
github-build-setup注入(zhtw-mcp 的案例就是要先跑gen-s2t-tables.py產生s2t_data.rs) - Installer 範本:cargo-dist 內建
shell/powershell/homebrew/npm等多種 installer 產生器,在installers = [...]設定 /releases/latest/download/alias:GitHub 自動提供,指向 latest non-prerelease release 的 asset;prerelease 不會更新這個 alias
這也解釋了為什麼 git tag dev 或單純 commit 到 main 都不會發版 — 那不符合 tag pattern、不是發版意圖。
第一次搭 cargo-dist 的實作步驟
從零開始的維護者視角,Rust binary 專案要搭 cargo-dist 大致是這幾步:
- 裝 cargo-dist CLI:
cargo install cargo-dist(或從它自家的 installer 裝) - 跑
dist init:互動式問答,選 targets、installers、CI provider(GitHub Actions),它會在Cargo.toml寫入[workspace.metadata.dist]並產生.github/workflows/release.yml - 檢查產出:
release.yml是 auto-generated、開頭會標# This file was autogenerated by dist,不要手改,下次dist generate會被覆蓋 - 設定 pre-build hook(如果需要):在
Cargo.toml加github-build-setup = "build-setup.yml",把編譯前要跑的步驟寫在.github/build-setup.yml(這個檔不會被dist generate覆蓋) - 設定 preflight gate(重要):把現有的 main CI workflow 加上
workflow_calltrigger,在Cargo.toml設plan-jobs = ["./.github/workflows/main.yml"],讓 release pipeline 在 cross-compile 前先確認測試全綠 - 推第一個 prerelease tag 試水溫:
git tag v0.1.0-alpha.1 && git push origin v0.1.0-alpha.1,看 release.yml 跑出來的 matrix 是不是全綠 - 確認 installer script 可用:在乾淨機器上跑
curl ... /releases/download/v0.1.0-alpha.1/<pkg>-installer.sh | sh(注意 prerelease 要用完整 tag URL、不是latest) - 推第一個正式 tag:跑
v0.1.0,這時/releases/latest/download/alias 才會生效 - 更新 README:把 installer 安裝命令寫上去;正式版發出後就能用
latestURL,prerelease 階段要寫完整 tag URL - 後續維護:bump version → tag → push,cargo-dist 自動處理;只有改
[workspace.metadata.dist]時才需要重跑dist generate
第 5 步的 preflight gate 是新手最容易漏的關。沒有它的話、main 紅燈時你還是能 push tag、cargo-dist 還是會跑 cross-compile、爛 binary 還是會推到所有人。workflow_call 反向 reuse 這個 pattern 在 CI gate 與 workflow 邊界 有更完整討論。
Installer script 模式的契約
curl ... | sh 是這條通道的常見下游入口。這個入口要成立,前提是上游提供可驗證產物、下游執行前有最小安全檢查。
cargo-dist 產生的 installer 命令長這樣:
1curl --proto '=https' --tlsv1.2 -LsSf \
2 https://github.com/<owner>/<repo>/releases/latest/download/<pkg>-installer.sh | sh逐項拆解 curl 的 flag:
| 片段 | 用途 |
|---|---|
--proto '=https' | 限制只走 HTTPS,避免被中間人 downgrade 到 HTTP |
--tlsv1.2 | 拒絕舊版 TLS |
-L | 跟隨 redirect(GitHub 的 latest alias 是 302) |
-sS | 安靜但保留錯誤訊息 |
-f | HTTP 錯誤時 curl 自己 exit non-zero(不把 404 HTML 當內容 pipe 進 sh) |
| sh | 把腳本內容餵給 shell 執行 |
-f 那個 flag 是這條鏈路的安全點:沒有它的話、如果 release URL 暫時 404,GitHub 的 404 HTML 會被 pipe 到 sh 然後爆出一堆語法錯誤;有 -f 時 curl 會直接 exit 22、sh 不會被呼叫,使用者看到的是清楚的錯誤碼。這就是為什麼 cargo-dist 產生的範本預設帶 -f、不能省。
PowerShell 版本(irm | iex)的等價契約相同 — Invoke-RestMethod 對 404 也會丟 exception、不會把 HTML 餵給 Invoke-Expression。
Installer script 自己的內部行為:偵測平台、下載對應 archive、解壓、放到 ~/.local/bin 或 ~/.cargo/bin、視需要更新 PATH。這部分由 cargo-dist 範本生成、跨專案幾乎一致、維護者不需要手寫。
最小安全基線(教學案例版)
教學案例可以示範 curl | sh,但可維護版本要同時提供「下載、驗證、執行」路徑,讓使用者在高風險環境可切換到可審計流程。
1# 1) 下載 installer 與 checksum
2curl --proto '=https' --tlsv1.2 -LsSf \
3 -o /tmp/<pkg>-installer.sh \
4 https://github.com/<owner>/<repo>/releases/download/vX.Y.Z/<pkg>-installer.sh
5curl --proto '=https' --tlsv1.2 -LsSf \
6 -o /tmp/<pkg>-checksums.txt \
7 https://github.com/<owner>/<repo>/releases/download/vX.Y.Z/<pkg>-checksums.txt
8
9# 2) 驗證 checksum(sha256sum 或 shasum 擇一)
10sha256sum -c /tmp/<pkg>-checksums.txt --ignore-missing
11# shasum -a 256 -c /tmp/<pkg>-checksums.txt
12
13# 3) 執行 installer
14sh /tmp/<pkg>-installer.sh這條路徑的責任分工是:
- 上游:發布 installer 與對應 checksum(或 provenance)。
- 下游:先驗證再執行。
- 文件:同時提供快速路徑與可審計路徑,並標明適用情境。
Pre-release(early adopter)通道
第一個正式 release 之前,pipeline 本身需要先被驗證。這時 prerelease tag(v0.1.0-alpha.1、v0.1.0-rc1 之類)就派上用場:
- 作為 pipeline 自身的測試:tag 推下去能跑出多平台 binary,代表 cargo-dist 設定正確
- 給 early adopter 試用:願意當先驅者的使用者可以用完整 tag URL 取得 binary
- 不污染 latest alias:GitHub 的
releases/latest/download/只指向 non-prerelease,所以 prerelease 不會「假發版」
代價是 prerelease 沒有 stable URL — 每個版本要寫完整 tag、不能用 latest。所以 README 安裝段落在 v0.1.0 出來之前要寫:
1# Pre-release example(給 early adopter)
2curl --proto '=https' --tlsv1.2 -LsSf \
3 https://github.com/<owner>/<repo>/releases/download/v0.1.0-alpha.1/<pkg>-installer.sh | sh正式 v0.1.0 出來之後再切回 latest URL。這是 zhtw-mcp issue #35 討論裡 hydai 提的折衷方案,能讓社群在 pipeline 完備前先試用、又不誤導不明就裡的使用者以為正式版已就位。
zhtw-mcp 案例:社群協作把 release pipeline 搭起來
zhtw-mcp 的 issue #35 跟 PR #40 是這條搭建過程的活案例。整個討論的時間軸:
- dlackty 提 issue #35:建議導入 cargo-dist + Homebrew、列出建議 targets、指出
s2t_data.rs需要 pre-build hook - 作者 jserv 回應:認同方向,但坦承自己 Rust 經驗有限、這個專案部分目的就是為了學 Rust 生態,邀請社群提 PR 推進
- hydai 開 PR #40:第一次用 cargo-dist,自己也在學,誠實表示「想知道方向對不對,希望熟手能接手」,並引用自己之前用 knope 手刻 release 的另一個 repo 作為對照
- jserv 提到 installer URL 失效:README 已經寫了
releases/latest/download/...,但還沒有正式 release,建議用 pre-release 給 early adopter - hydai 提議
v0.1.0-alpha.1:作為 early adopter 通道、提醒 prerelease 沒有 latest alias、要用完整 tag URL
這個討論留下幾個值得學的點:
- 公開承認還在學是好事:jserv 直接說「我 Rust 經驗有限、我也在學」、hydai 說「我第一次用 cargo-dist」,這比假裝專家有效率多了。社群協作的核心是大家都看到同一個未完成狀態、一起補。
- README 先寫安裝命令再補 release 是常見順序:把 release 路線當作目標釘出來、再倒推實作,是刻意的設計。先寫文件再補 pipeline 的順序也讓 issue #35 / PR #40 更容易聚焦。
- 特殊 build hook 是 cargo-dist 的明確支援點:zhtw-mcp 需要在編譯前跑
gen-s2t-tables.py產生s2t_data.rs,這正好是github-build-setup設計給的場景。如果你的 repo 有類似「編譯前要產生程式碼/下載資料」的需求、不必為此放棄 cargo-dist。 - Pre-release 是 pipeline 學習期的合理工具:先用
v0.1.0-alpha.1把 pipeline 跑通、把問題暴露出來,比等到一切完美才發版更有效率。
跟著這個 issue 串看完一輪、可以得到一個從零搭 cargo-dist 的真實參照框架,比官方文件更貼近實際遇到的問題。
Homebrew 通道:cargo-dist 怎麼幫你出 formula
brew install 是 macOS 使用者最熟的安裝路線,但 Homebrew 有兩種發版形式:
| 形式 | 怎麼裝 | 維護成本 |
|---|---|---|
| Homebrew core | brew install <pkg> | 高 — 要過 homebrew-core 的 PR review,門檻嚴 |
| Homebrew tap | brew install <user>/<tap>/<pkg> | 低 — 在自己的 GitHub repo homebrew-<tap> 放 formula |
cargo-dist 預設支援的是後者(tap)。設定方式是在 [workspace.metadata.dist] 加:
1installers = ["shell", "powershell", "homebrew"]
2tap = "<your-github-username>/homebrew-<tap-name>"然後在 GitHub 開一個叫 homebrew-<tap-name> 的 repo(命名規則是 Homebrew 強制的),cargo-dist 會在每次 release 自動 push 一個更新過的 formula 到那個 repo。下游使用者只要:
1brew tap <your-github-username>/<tap-name>
2brew install <pkg>要走 homebrew-core 是另一個層級的事 — 需要套件夠成熟、有穩定使用者基數、有清楚的 license、過 homebrew-core maintainer 的 review。多數新專案先做 tap、累積使用者跟成熟度後再考慮 core。
上線前的最後檢查
第一個正式 v0.1.0 推出去之前最後跑一遍:
- Prerelease tag(
v0.1.0-alpha.1之類)跑過 release.yml、cross-compile matrix 全綠 - 從乾淨機器跑 README 寫的 installer 命令、從下載到執行整條順
- Pre-build hook(如果有)在所有 platform 都能跑、不依賴特定 OS
- Preflight gate 的
workflow_callreuse 確實 block 住紅燈 main - README 的 installer URL 跟實際 asset 命名規則一致(cargo-dist 會用
<pkg>-installer.sh、不要寫成install.sh) - Changelog 跟 tag 對齊(cargo-dist 會把 changelog 抓進 release notes)
- 有提供可審計安裝路徑(下載 + checksum/provenance 驗證 + 執行)
第一條 v0.1.0 推出去後 releases/latest/download/... alias 才會生效、那時就能把 README 改成 latest URL、徹底完成這條通道的搭建。
來源與規格
- cargo-dist 官方文件:https://opensource.axo.dev/cargo-dist/
- cargo-dist GitHub Action / 生成流程:https://github.com/axodotdev/cargo-dist
- GitHub Releases 與 latest 行為:https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
- zhtw-mcp 案例 issue:https://github.com/sysprog21/zhtw-mcp/issues/35
- zhtw-mcp 案例 PR:https://github.com/sysprog21/zhtw-mcp/pull/40
下一步路由
- 想理解整體 release 類型分類:回 Package / Library Release CI/CD。
- 想理解 workflow_call 的反向 reuse:讀 CI gate 與 workflow 邊界。
- 想理解 release workflow 紅燈時的處理:讀 CI 失敗到修復發布流程。
- 想理解 artifact 可信度:讀 Artifact Provenance。