# GitHub Actions Workflow Lockfiles：只鎖 uses@SHA，為什麼還不夠？

> Canonical: https://vxi.me/zh-tw/posts/github-actions-workflow-lockfiles-transitive-dependencies/
> Locale: zh-tw
> Published: 2026-10-18
> Updated: 2026-10-18
> Markdown: https://vxi.me/zh-tw/posts/github-actions-workflow-lockfiles-transitive-dependencies.md

# GitHub Actions Workflow Lockfiles：只鎖 uses@SHA，為什麼還不夠？

將 GitHub Actions 中的 `uses: some-org/action@v2` 改為完整 Commit SHA，的確能固定這個入口的程式碼。不過，如果它是一個 Composite Action，內部又引用 `example-org/upload@v1` 呢？外層 Commit 沒變，內層 Tag 卻仍可能移動。光看 Workflow YAML，未必看得出整條第三方 Action 依賴鏈。

GitHub 在 [Workflow Lockfiles 公開預覽公告](https://github.blog/changelog/2026-10-14-github-actions-workflow-lockfiles-in-public-preview)中推出 `.github/workflows/actions.lock`，讓直接與傳遞 Action 依賴都能記錄鎖定的 Commit 及來源儲存庫身分。

**已經全面使用 `uses@SHA`，還需要導入 Lockfile 嗎？先看依賴圖，再決定是否遷移。** 新機制真正補上的是傳遞依賴清單與儲存庫身分驗證，不是對同一個 SHA 重複加鎖。

| 目前的 CI 情況 | 建議 | 理由 |
| --- | --- | --- |
| 直接呼叫的 Action 已用完整 SHA 固定，引用層級簡單 | 先盤點，暫緩整個儲存庫切換 | 可能沒有足夠收益抵銷維護成本 |
| Composite Action 又呼叫 `@v1` 或 `@main` | 選一條非關鍵 Workflow 試點 | 可以鎖住傳遞 Action 依賴 |
| 多個儲存庫共用 Reusable Workflow | 先釐清呼叫端與被呼叫端的責任 | 內部 Action 由定義 Reusable Workflow 的儲存庫負責 |
| 組織已設定 Allowed Actions 或 SHA 固定策略 | 先核對政策，再進行試點 | 鎖定檔上線後可能以 Commit SHA 比對允許清單 |

這些情況可以重疊。以下以 [gh-actions-lock v0.2.0](https://github.com/github/gh-actions-lock/releases/tag/v0.2.0) 的固定原始碼與官方預覽文件為準。

## 直接固定 SHA，不等於固定整張 Action 依賴圖

假設 `ci.yml` 用完整 SHA 呼叫 `example-org/build`，但它的 `action.yml` 內部繼續執行 `example-org/upload@v1`。外層來源已固定，下游版本標籤仍可能更新。

| 依賴入口 | 直接 `uses@SHA` | 納入 Workflow Lockfile 後 |
| --- | --- | --- |
| Workflow 直接引用的第三方 Action | 固定該筆引用 | 記錄 Commit 及儲存庫身分 |
| Composite Action 內的其他 Action | 不會自動鎖住內層 Tag | 包含傳遞 Action 依賴 |
| 跨儲存庫 Reusable Workflow | 可固定呼叫入口 | 呼叫端鎖住呼叫；內層 Action 由被呼叫儲存庫管理 |
| 同儲存庫 `uses: $/…` | 跟隨目前儲存庫 Commit | 本身具備固定語意，不用額外 Pin |
| `run:` 下載腳本或可變容器 Image Tag | 不在該 Action Pin 範圍 | 仍無法僅靠 Action Lockfile 管控 |

這裡最重要的是跨儲存庫責任邊界。[GitHub 官方概念說明](https://docs.github.com/en/actions/concepts/security/workflow-dependency-lockfiles)規定，鎖定檔涵蓋該儲存庫所定義的 Workflow；外部 Reusable Workflow 裡使用的 Action，則由**定義該 Reusable Workflow 的儲存庫**自己的 Lockfile 負責。業務儲存庫固定共享部署流程的入口，不代表已經稽核了對方每一層內部 Action。

## actions.lock 裡有哪些內容

`workflows` 將 Workflow 路徑對應到依賴 Pin；`dependencies` 記錄 `ref`、`commit`、`owner_id`、`repo_id`，遇到 Composite Action 的下一層呼叫還可能有 `uses`。下面是 [官方格式儲存庫固定提交的 `benchV003` 測試樣本](https://github.com/github/actions-lockfile/blob/ac158834765237ee94c36606cfc7d87316aa49ab/go/pkg/lockfile/bench_test.go#L31-L58)，完整呈現 `v0.0.3` 的 YAML 結構。多次出現的 a/b 雜湊值是測試用佔位資料，不是正式 Runner 的鎖定結果：

```yaml
version: v0.0.3
workflows:
  .github/workflows/ci.yml:
    - actions/checkout@v4
    - actions/setup-go@v5
    - actions/cache@v4
dependencies:
  actions/checkout@v4:
    hostname: github.com
    ref: v4
    commit: sha1-11bd71901bbe5b1630ceea73d27597364c9af683
    owner_id: 44036562
    repo_id: 197814629
  actions/setup-go@v5:
    hostname: github.com
    ref: v5
    commit: sha1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    owner_id: 44036562
    repo_id: 249058325
  actions/cache@v4:
    hostname: github.com
    ref: v4
    commit: sha1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    owner_id: 44036562
    repo_id: 251882839
    uses:
      - actions/checkout@v4
```

這份樣本會明列 `hostname: github.com`；實際在預設 github.com 主站生成的鎖定檔可能省略該欄位。`dependencies` 底下的 `uses` 顯示內層 Action 關係，而 `workflows` 顯示各條 Workflow 需要的 Pin，不是同一種清單。

固定版 [`state_marshal.go` 原始碼](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/internal/lockfile/state_marshal.go)實際會寫出這些欄位：

```go
a := file.Dependencies[k]
entry := &yaml.Node{Kind: yaml.MappingNode}
// 此处省略对 hostname 的处理分支
if a.Ref != "" {
    addQuotedField(entry, "ref", a.Ref)
}
if a.Commit != "" {
    addQuotedField(entry, "commit", a.Commit)
}
addIntField(entry, "owner_id", a.OwnerID)
addIntField(entry, "repo_id", a.RepoID)
```

Commit SHA 指出 Git 物件，數字化的 `owner_id` 和 `repo_id` 則可確認名稱背後的儲存庫身分；即使發生更名或轉移，檢查也不只依賴可讀名稱。官方概念頁的示例仍標示 `version: v0.0.2`，但這次檢查的 [v0.2.0 Release](https://github.com/github/gh-actions-lock/releases/tag/v0.2.0) 與[格式 README](https://github.com/github/actions-lockfile/blob/ac158834765237ee94c36606cfc7d87316aa49ab/README.md)都對應 **`v0.0.3`**。CLI 版本與鎖定檔 Schema 版本並不相同。

## 遷移前，先檢查實際會改動哪些檔案

我會在測試分支先挑一條可回復的非關鍵 Workflow。假定 GitHub CLI 已安裝、登入，並有權讀取相關儲存庫，可用下列固定版本命令：

```bash
# 固定本文检查的扩展版本
gh extension install github/gh-actions-lock --pin v0.2.0
gh actions-lock .github/workflows/ci.yml

# 同时检查 workflow、本地 action.yml 的修改与新文件
git status --short
git diff
cat .github/workflows/actions.lock

# 只校验试点 Workflow，避免未迁移的其他文件干扰
gh actions-lock --verify .github/workflows/ci.yml
```

單檔驗證語法有固定原始碼支援：[root.go](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/root.go#L64-L70) 接受 Workflow 路徑；[verify.go](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/verify.go#L24-L40) 與上游測試確認校驗使用此範圍。共用 `actions.lock` 仍須是有效格式，但不會因驗證某一條 Workflow 就自動納入其他尚未遷移的檔案。

工具不只是新增 `actions.lock`，也可能把版本標籤縮至更精確的版本，或將有效的本地 Action `./actions/build` 改為 `$/actions/build`：

```diff
 steps:
-  - uses: ./actions/build
+  - uses: $/actions/build
```

`$/` 會綁定目前執行中的儲存庫 Commit。不合法的 `./` 路徑不會因此被修好。新建但尚未 `git add` 的 `actions.lock` 也不會出現在一般 `git diff` 裡，應一起檢查 `git status --short`。

組織的 Allowed Actions 政策也是遷移風險。Workflow 使用 Lockfile 後，相關允許清單會依**鎖定的 Commit SHA**比對；原先寫成 `actions/checkout@v6` 的規則可能失配。做法是對照實際核准 Commit 調整政策，而非直接開放全部第三方 Action。

離線與線上校驗要分開：

```bash
gh actions-lock --verify-local .github/workflows/ci.yml
gh actions-lock --verify .github/workflows/ci.yml
```

`--verify-local` 只檢查本地鎖定覆蓋率，不會查 GitHub 上的來源身分；`--verify` 才會進行線上信任驗證，適合用於 PR 門禁。全面遷移完成後再用不帶路徑的 `gh actions-lock --verify` 做整個儲存庫的檢查；過早執行可能因尚未納入的 Workflow 或無用 Pin 而報錯。

需要排查時，可加 `--json=valid,findings`。退出碼 1 表示仍有阻斷 Finding，2 表示命令、I/O 或網路錯誤。[原始碼定義的 Finding 類型](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/internal/pipeline/checks/category.go)包括 `not-pinned`、`stale`、`local-action`、`repo-replaced` 和 `unreachable-pin`。

**一次可回復的五步試點：**
1. **盤點**：列出直接 Action、內層 Composite Action，以及跨儲存庫 Reusable Workflow，確認被呼叫端的維護責任。
2. **生成**：在測試分支使用固定版本工具，檢查 Workflow、`action.yml`、`actions.lock` 的完整差異。
3. **核對政策**：以鎖定 Commit 逐一比對 Allowed Actions，正常進行 Code Review。
4. **兩層校驗**：先用 `--verify-local` 查覆蓋，再用 `--verify` 查來源身分。`not-pinned`、`stale`、`local-action` 回頭檢查路徑及 Diff；`repo-replaced`、`unreachable-pin` 先調查上游歷史，不要直接合併。
5. **保留回退路徑**：若解析或政策阻擋 CI，可撤銷測試分支的變更；試點穩定後才擴大到整個組織。

## Lockfile 建立後，怎麼更新依賴

首次鎖定的 Commit 取決於原始 `uses` 與解析結果。v0.2.0 的 **release cooldown 並沒有固定七天預設值**：有設定時優先使用 `.github/dependabot.yml` 中 `github-actions` 正數的 `cooldown.default-days`，否則採用使用者設定檔的 `cooldown_days`；都沒有時不進行發行年齡過濾，對三天內發行的新 Tag 僅產生非阻斷性的 `fresh-tag` 提醒。參照[設定解析](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/fresh_tag.go#L16-L32)與[新標籤提醒](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/fresh_tag.go#L48-L75)。因此要對照生成檔裡的 `ref`、`commit` 和 Workflow Diff，不能想當然耳。

工具採用 **sticky pins**：一般再次執行 `gh actions-lock` 不會因上游 `main` 或 `v3` 移動就自動覆寫既有 Commit。要接受更新應明確使用 `gh actions-lock --relock`，並審查每一筆變更。如果上游改寫歷史造成舊 Commit 不可達，`--relock --accept-moved` 屬於獨立的信任決策，不能只為了讓 CI 轉綠就跳過調查。

Dependabot 可為**已納入鎖定檔**的 Workflow 維護更新，但不會自動替新 Workflow 完成首次納入。[GitHub 組織政策指南](https://docs.github.com/en/actions/how-tos/administer/enforce-workflow-lockfiles)建議先小範圍試點，再以 CODEOWNERS 審查 `actions.lock`，最後才考慮 `Require lockfile`。請先確認帳戶計畫與權限；固定版 README 也註明此預覽尚不支援 GitHub Enterprise Server，不能與 Enterprise Cloud 混為一談。

Lockfile 能確認執行的程式碼身分，卻不能判定程式碼有無漏洞。`run:` 下載的腳本、未鎖 Digest 的容器、套件依賴與 CI Token 權限都要另外處理。一個值得推廣的試點，應該讓團隊能回答：究竟新增鎖住哪些內層 Action、誰審查了來源儲存庫身分、共享 Reusable Workflow 的內部依賴誰負責？這比單純新增一個產生檔更有價值。另一個掃描與阻擋合併的差異，可以參考 [GitHub AI Scan 技術分析](/posts/github-ai-scan-pr-security-checks/)。

---

*附註／證據時間：GitHub 於 2026 年 10 月 14 日宣布 Workflow Lockfiles Public Preview。本篇核對 `gh-actions-lock v0.2.0` 固定提交 `d5dca22a06ff8590d50b41612038996800cfb336` 及格式儲存庫提交 `ac158834765237ee94c36606cfc7d87316aa49ab`，檢查日期為 2026 年 10 月 18 日。YAML 是上游測試樣本，含合成雜湊值；本文並未實際執行 Runner、CLI 遷移或 Tag 重新指向實驗。預覽權限以實際帳戶方案與設定為準。*
