Skip to content

Build Runtime 相容性

本頁說明維持本範本 build chain 在所支援 Node 版本上順利執行所需的執行環境要求與驗證步驟。契約規範於 openspec/specs/build-runtime-compatibility/spec.md,本頁為其人類可讀的對應版本。

必備 Node.js 版本範圍

約束來源
下限>=22.0.0package.json engines.node
建議22(當前 LTS 主版本).nvmrc 首行
一同驗證Node 24(apply 階段的 current Active LTS)本頁 + change fix-build-blockers 的 audit

repo 同時宣告 engines.node(由 pnpm 強制檢查)與 .nvmrc(由 nvm 強制切換),因此:

  • 在 Node 20 或更舊版本上跑 pnpm install 會被 engines 拒絕,連相依套件都不會下載。
  • 在 repo 根目錄跑 nvm use 會自動切換到相容的 Node 主版本。

Node 20 與更舊版本在支援範圍。如果你的 fork 想下修,請先把 Canonical 驗證指令鏈 全部重跑一次再決定。

系統 wasm-pack 必裝

wasm-pack 不再透過 npm 散布。原本由 npm 提供的 wasm-pack@0.14.0 內含 minizlib shim,在 Node 24 載入時會拋 Class extends value is not a constructor。本範本已移除該 devDependency,改為要求 wasm-pack 二進位安裝在你的 PATH 上。

安裝步驟(透過 rustup):

bash
# 1. 安裝 Rust toolchain(只需一次)
curl https://sh.rustup.rs -sSf | sh

# 2. 安裝 wasm-pack
# 選項 A — 透過官方 installer 腳本
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 選項 B — 透過 cargo
cargo install wasm-pack

安裝完成後,which wasm-pack 應該指到真正的 binary(通常是 ~/.cargo/bin/wasm-pack)。若你在 PATH 上沒有 wasm-pack 而執行 pnpm qa:build-prereqs,腳本會在其他任何檢查之前以 non-zero exit code 結束,並印出上面的安裝連結。

Canonical 驗證指令鏈

本範本必須在乾淨環境下(無 node_modules、無 simnet-engine/target/、無 .vitepress/dist、無 .vitepress/cache/、系統有 wasm-pack)跑通以下指令鏈,且需在 Node 22 LTS 與 Node 24 上分別驗過:

bash
pnpm install --frozen-lockfile
pnpm test                  # 基線 ≥ 16 個測試檔 / 430 tests 通過
pnpm validate:challenges    # exit 0
pnpm wasm:build             # 產生 docs/public/wasm/
pnpm docs:build             # 產生 .vitepress/dist/
pnpm build                  # 端到端(flags:ensure + wasm:build:release + docs:build)

跑完 build chain 後,繼續執行雙語 smoke:

bash
pnpm qa:site-smoke:dev      # /、/challenges/...、/zh-TW/、/zh-TW/challenges/... 皆 exit 0
pnpm qa:site-smoke:preview  # 同樣 routes,對著 build 產物

乾淨環境跑完之後,git status --porcelain 不應有 tracked 區改動(僅允許預期內的未追蹤 build 產物)。

VitePress 版本的 fallback 機制

vitepress@2.0.0-alpha.17 帶有一個 server-side rendering bug(vitepress data not properly injected in app),會讓下游 fork 的 pnpm docs:build 直接掛掉。本 change fix-build-blockersdependencies.vitepress 釘到當下最新的 1.x stable(^1.6.4),因為升版當下並沒有任何 2.x stable 存在。

如果未來再次升 VitePress 又踩到同樣的 SSR 失敗,請依照下列 fallback 程序:

  1. 開一個新的 Spectra change,把 dependencies.vitepress 釘回最新的 1.x stable。
  2. pnpm install 更新 pnpm-lock.yaml,再依照 Canonical 驗證指令鏈 在 Node 22 LTS 與 Node 24 上各跑一次。
  3. pnpm qa:site-smoke:* 兩個模式對 //zh-TW/ 都重跑一次。
  4. 審視 .vitepress/theme/**.vitepress/config.mts 在兩個 major 版本之間的 breaking change,採取最小修補。
  5. 在新 change 的 audit/upgrade-notes.md 紀錄 registry 快照(npm view vitepress dist-tags --json)與選版決策。

原始升版的 fallback 依據見 openspec/changes/fix-build-blockers/audit/upgrade-notes.md

CI quality gates

.github/workflows/test.yml 在每一次以 main 為目標的 pull request 與每次推送到 main 時強制執行 canonical 指令鏈。工作流刻意拆成六個 job,讓每一條契約失敗時都能在 pull request 上顯示獨立的 status check:

JobMatrix守備的 spec 契約
lint-validateNode 22quality-assurance-gatespnpm validate:challenges + pnpm qa:build-prereqs
rust-test(無)quality-assurance-gatessimnet-engine/cargo test
ts-testNode 22、24quality-assurance-gatespnpm test;Node 版本範圍來自 build-runtime-compatibility
wasm-buildNode 22、24quality-assurance-gatespnpm wasm:build;per-version artifact simnet-wasm-node<version> 來自 build-runtime-compatibility
docs-buildNode 22、24quality-assurance-gatespnpm docs:build 與雙語入口檔案存在性驗證
site-smokeNode 22、24quality-assurance-gatespnpm qa:site-smoke:dev + :preview;master key 來源規則來自 flag-secrets-management

帶 matrix 的 job 在顯示名稱裡含 Node 版本(例如 TypeScript Tests (Node 22)),GitHub branch protection 設定畫面因此能逐一勾選每個 matrix 變數作為 required check。乾淨推到 main 的工作流總共會產生十個 required status check:兩個單版本 job 加上四個 matrix-aware job 在兩個 Node 版本下各跑一次。

docs-buildsite-smoke 透過 needs: wasm-build 宣告依賴並 download 與自身 Node matrix 變數對應的 artifact;wasm-build 成功後沒有任何 job 會再次重編 wasm-pack

docs-buildsite-smoke 為什麼需要 SIMNET_MASTER_KEY

SIMNET_MASTER_KEY 是 build pipeline 派生每個 challenge flag 的 HMAC 根。派生動作在 build 時跑於兩個位置:

  1. scripts/ensure-flag-secrets.mjspnpm build 的 prebuild step)用 HMAC-SHA256(masterKey, "flag:v1:" + challengeSlug) 為每個 challenge 算出 flag plaintext,寫進 docs/challenges/flags.secret.yaml(gitignore)。
  2. .vitepress/theme/loaders/challenge.data.ts 在 VitePress SSR 階段執行,把每條 flag plaintext 的 SHA-256 hash 嵌入渲染後的 challenge 頁面。學生瀏覽器隨後 hash 提交內容,與嵌入值比對驗證。

scripts/ensure-flag-secrets.mjs 與 data loader 兩處都依 NODE_ENV 分流。Dev 模式(pnpm dev)下若 SIMNET_MASTER_KEY 未設,會 fallback 到公開常量 simnet_dev_only_key_32bytes_xx!! 並印 stderr 警告——本地開發可接受,因為這把 dev-only key 不會跑到真實使用者環境。Production 模式(NODE_ENV=productionpnpm docs:build 預設套用)下沒有任何 fallback:未設 key 直接 exit 1 並印 openssl rand -hex 16 指引。

docs-buildsite-smoke 兩個 job 都會跑 production build(docs-build 直接呼叫 pnpm docs:buildsite-smoke --mode=preview 內部 chain 到 pnpm run docs:build),所以兩個 job 都需要 SIMNET_MASTER_KEY。其他四個 job 不需要:lint-validate 跑 schema 驗證、rust-testcargo testts-testjsdomvitestwasm-build 只呼叫 wasm-pack,這四條 path 都不會碰到 loader 或 prebuild flag 派生流程。

Dev-only fallback 常量是 repo 內刻意公開的字串,正因如此它不能進到 CI run:任何人看到 CI build log 裡用 simnet_dev_only_key_32bytes_xx!! 算出的 flag hash,都能反向算出該 build 對應的所有 flag plaintext。CI 專用的 SIMNET_MASTER_KEY_CI secret 切斷這條線:它與任何 production key 無關、也與 dev-only 常量無關,即使 CI artifact 外洩也不會危及部署中的真實環境。

docs-buildsite-smoke 的 master key 取得方式

smoke job 透過工作流 step 上的 if: 條件,從以下兩條路徑之一取得 SIMNET_MASTER_KEY

  • 內部 pull request 或推送到 main:step 透過 GitHub Actions ${{ secrets.SIMNET_MASTER_KEY_CI }} 表示式注入 SIMNET_MASTER_KEY_CI repository secret。這條 secret 是 repository 維護者另行透過 gh secret set SIMNET_MASTER_KEY_CI 指令(或 GitHub Settings 介面)建立的 CI 專用值,必須與任何 production / staging master key 完全獨立。可用 openssl rand -hex 16 產生,懷疑外洩時立即 rotate。
  • 來自 fork repository 的 pull request:GitHub 不會把 repository secret 暴露給 fork 的工作流。對應 step 改為在執行期內以 openssl rand -hex 16 產生一把 ephemeral 隨機 key,整輪 workflow 結束就丟棄。Fork 的 smoke check 仍會跑、仍會產生 status;SIMNET_MASTER_KEY_CI 不會進入 fork 的 job 環境。

scripts/qa/site-smoke.mjsscripts/qa/build-prereqs.mjs 不再保留任何 module-level 的 master key fallback。若執行時 SIMNET_MASTER_KEY 沒設定,script 會在啟動 Playwright 之前以 exit code 1 結束,並在 stderr 印出含 openssl rand -hex 16 的提示。本地貢獻者跑 smoke 必須自行 export 該變數;pnpm dev 走另一條 flags:ensure 路徑,不受影響。

GitHub Actions 全部釘到 commit SHA

test.yml 內每一條 uses: 引用都釘到完整 40-char commit SHA,後面用 # vN.x.y 註解標記語意版本。如果改用 mutable tag(例如 @v4),action 上游維護者或攻擊者就能讓該 tag 指向新 commit、在 CI 內執行未審閱的程式碼。要升版時用 gh api repos/<owner>/<repo>/commits/<tag> --jq '.sha' 查出新 SHA,並把 SHA 與註解版本一次替換。

下游 fork 啟用 CI 的設定步驟

新 fork 會直接繼承本工作流,但 SIMNET_MASTER_KEY_CI secret 預設不存在。內部 pull request 要能跑綠 site-smoke job,維護者需先做以下兩件一次性動作:

  1. 建立 CI master key secret。 在已對 fork 認證的 shell 內執行:
    bash
    gh secret set SIMNET_MASTER_KEY_CI --body "$(openssl rand -hex 16)"
    或在 Settings → Secrets and variables → Actions → New repository secret 介面設定。同一把 key 不要跨環境重複使用。
  2. 鎖定 matrix-aware required check。Settings → Branches → Branch protection rule 把以下十條 status check 加進 "Status checks that are required" 清單,任何 matrix 變數失敗就擋住 merge:
    • Lint and ValidateRust Tests
    • TypeScript Tests (Node 22)TypeScript Tests (Node 24)
    • WASM Build (Node 22)WASM Build (Node 24)
    • Docs Build (Node 22)Docs Build (Node 24)
    • Site Smoke (Node 22)Site Smoke (Node 24)

Fork pull request 不需要任何額外動作;它們會自動走 ephemeral key 路徑(如上節所述)。