錯write_package的完整修復(fù)指南)
但凡用 GitHub Actions 推過 Docker 鏡像的人大概率都撞到過這個(gè)報(bào)錯denied: permission_denied: write_package或者是推送過程中突然冒出一句unexpected status from POST request to https://ghcr.io/v2/xxx/xxx/blobs/uploads/: 403 Forbidden第一次遇到的時(shí)候我盯著日志看了半天login 明明成功了build 也沒問題偏偏到 push 那一步就給你卡死而且返回的還是“權(quán)限不足”這種讓人摸不著頭腦的話。后來翻了不少資料、踩了幾個(gè)坑才把這里的權(quán)限鏈路徹底捋順。這篇文章就把 write_package 這個(gè)報(bào)錯的來龍去脈、權(quán)限模型、完整修復(fù)方案和排查套路一次性講清楚。無論你是剛接觸 GitHub Actions 的新手還是已經(jīng)推過一段時(shí)間鏡像但偶爾被它絆一下的老手按下面的步驟走一遍基本能徹底擺脫這個(gè)問題。1. 現(xiàn)象還原write_package 報(bào)錯到底是什么1.1 報(bào)錯現(xiàn)場與最小復(fù)現(xiàn)先看一個(gè)最典型的、能穩(wěn)定復(fù)現(xiàn)這個(gè)錯誤的 workflow 配置。很多項(xiàng)目一開始就是這么寫的name: build-and-push on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Log in to GitHub Container Registry run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build and push run: | docker build -t ghcr.io/${{ github.repository_owner }}/demo:v1 . docker push ghcr.io/${{ github.repository_owner }}/demo:v1這段配置看著沒毛病實(shí)際上跑起來就會在docker push那一步報(bào)權(quán)限錯誤。報(bào)錯通常長這樣ERROR: failed to push ghcr.io/xxx/demo:v1: denied: permission_denied: write_package還有另一種形態(tài)是 buildx 在推多個(gè)層的時(shí)候報(bào)ERROR: failed to push ghcr.io/xxx/demo:v1: unexpected status from POST request to https://ghcr.io/v2/xxx/demo/blobs/uploads/: 403 Forbidden兩種報(bào)錯的根因其實(shí)一樣當(dāng)前使用的憑據(jù)對目標(biāo)鏡像所在的 GitHub Packages 包沒有寫入權(quán)限。1.2 為什么“看起來沒問題”卻一直失敗很多人的第一反應(yīng)是“Docker Hub 都能推ghcr.io 怎么就不行”。這其實(shí)是把 GitHub Packages 和 Docker Hub 的鑒權(quán)模型搞混了。Docker Hub 的 push 權(quán)限是跟著你的賬號走的只要登錄的是有權(quán)限的賬號推哪個(gè)倉庫基本都能通過。但 GitHub Packages 的權(quán)限校驗(yàn)比它多了一層它不僅要驗(yàn)證“你是誰”還要驗(yàn)證“你對這個(gè)包有沒有寫入權(quán)限”。這里的“包”指的是 ghcr.io 上的鏡像倉庫package它的歸屬、可見性、寫入權(quán)限和 GitHub 倉庫的權(quán)限體系是深度綁定的。也就是說如果你用的是GITHUB_TOKEN它的權(quán)限范圍由 workflow 的permissions設(shè)置決定如果沒有顯式聲明packages: write默認(rèn)情況下這個(gè) token 只能讀不能寫一旦 push 請求發(fā)出去GitHub Packages 發(fā)現(xiàn) token 沒有寫權(quán)限就直接返回write_package這個(gè)錯誤碼。所以問題根本不是 docker login 失敗而是登錄成功后token 的權(quán)限標(biāo)簽不夠。這就像你拿著門禁卡進(jìn)了大樓但電梯權(quán)限沒開按了樓層照樣報(bào)警。2. 權(quán)限模型拆解GitHub Packages 的三種身份與寫權(quán)限門檻2.1 GITHUB_TOKEN 與 PAT 的權(quán)限差異GitHub Actions 里有兩種常見的 token一類是自動生成的GITHUB_TOKEN。每個(gè)倉庫執(zhí)行 workflow 時(shí)都會臨時(shí)生成一個(gè)它的默認(rèn)權(quán)限范圍取決于倉庫的配置。在較老或未調(diào)整過的倉庫里這個(gè) token 默認(rèn)是只讀的能 checkout 代碼能讀包的信息但推不了鏡像。要給它寫權(quán)限必須在 workflow 里顯式聲明。另一類是個(gè)人訪問令牌PATPersonal Access Token這是你自己在賬號設(shè)置里創(chuàng)建的。它不屬于某個(gè)倉庫而是屬于你這個(gè)賬號。你需要手動勾選訪問范圍比如write:packages、read:packages。把 PAT 放到倉庫的 Secrets 里在 workflow 里調(diào)用就能用它來登錄 ghcr.io。這兩者的關(guān)系可以簡單理解成GITHUB_TOKEN是“臨時(shí)工”權(quán)限由配置決定workflow 結(jié)束后自動失效PAT 是“長期員工”創(chuàng)建時(shí)定好權(quán)限只要不手動撤銷就一直有效。寫 workflow 時(shí)優(yōu)先用GITHUB_TOKEN更安全因?yàn)樗辉谛枰牡胤脚R時(shí)生效。但如果 workflow 需要跨倉庫推送、或者目標(biāo)鏡像不歸當(dāng)前倉庫所有PAT 反而是更靈活的選擇。2.2 鏡像命名空間、倉庫歸屬與權(quán)限校驗(yàn)規(guī)則權(quán)限報(bào)錯還和一個(gè)容易被忽略的細(xì)節(jié)有關(guān)鏡像名的 owner 部分。ghcr.io 的鏡像地址結(jié)構(gòu)是這樣的ghcr.io/owner/image-name:tagowner可以是用戶名也可以是組織名。GitHub 在校驗(yàn)權(quán)限時(shí)會檢查當(dāng)前 token 是否對owner下的這個(gè)包有寫權(quán)限。舉個(gè)例子。假如你的倉庫是alice/my-projectgithub.repository_owner是alice鏡像名是ghcr.io/alice/my-image。這種情況下只要GITHUB_TOKEN有 packages 寫權(quán)限就能正常推送。但如果是 fork 場景就要小心了。fork 出來的倉庫里github.repository_owner會變成 fork 后的屬主如果 workflow 里把鏡像寫死了比如ghcr.io/alice/my-image但當(dāng)前 token 實(shí)際對應(yīng)bob的倉庫那么即使是alice倉庫里定義的公開包用bob的 token 去推alice的命名空間一樣會報(bào)write_package。還有一種情況是同一個(gè)倉庫內(nèi)如果之前用別的賬號或錯誤的命名空間創(chuàng)建過同名包GitHub Packages 會把這個(gè)包歸到那個(gè)命名空間下后續(xù)再用新 token 推就會遇到權(quán)限沖突。這類問題排查起來非常隱蔽因?yàn)槟憧赡芤詾樽约涸谕?A 包實(shí)際上 GitHub 把它識別成了另一個(gè) B 包而當(dāng)前 token 對 B 包確實(shí)沒有寫權(quán)限。2.3 workflow permissions 的兩種配置路徑解決寫權(quán)限核心就是讓GITHUB_TOKEN擁有packages: write。配置方式有兩種建議兩個(gè)地方都確認(rèn)一遍。第一種是在 workflow 文件里顯式聲明。可以在文件頂層設(shè)置也可以在 job 級別設(shè)置permissions: contents: read packages: write放在 job 級別會更精確不會把多余權(quán)限擴(kuò)散到其他 job。比如jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write第二種是在倉庫設(shè)置里修改默認(rèn) Workflow permissions。路徑是倉庫 Settings - Actions - General - Workflow permissions把選項(xiàng)從 “Read repository contents and packages permissions” 改成 “Read and write permissions”。需要注意的是倉庫級設(shè)置只是默認(rèn)值。如果 workflow 文件里顯式寫了permissions那以 workflow 文件為準(zhǔn)。所以我習(xí)慣的做法是倉庫設(shè)置允許讀寫同時(shí) workflow 文件里也顯式聲明雙保險(xiǎn)。3. 完整修復(fù)方案從零到一推送 ghcr.io 鏡像3.1 方案 A用 GITHUB_TOKEN 的最小配置如果你只想讓當(dāng)前倉庫的 workflow 把鏡像推到當(dāng)前所有者的 ghcr.io 命名空間用GITHUB_TOKEN就夠了不需要額外創(chuàng)建任何密鑰。完整可用的 workflow 如下name: push-to-ghcr on: push: branches: [main] jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkoutv4 - name: Set up Docker Buildx uses: docker/setup-buildx-actionv3 - name: Log in to ghcr.io uses: docker/login-actionv3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-actionv6 with: context: . push: true tags: | ghcr.io/${{ github.repository_owner }}/demo:latest ghcr.io/${{ github.repository_owner }}/demo:${{ github.sha }}這里有幾個(gè)值得留意的細(xì)節(jié)docker/login-action的username用github.actor也就是觸發(fā) workflow 的用戶名這是官方示例的常見寫法。重要的是password必須是secrets.GITHUB_TOKEN而不是任何明文密碼。docker/build-push-action的push: true表示構(gòu)建完成后直接推送它會復(fù)用前面 login-action 生成的 Docker 配置不需要再單獨(dú)寫docker push。tags 里建議同時(shí)打latest和 commit SHA 的標(biāo)記方便后續(xù)回溯。如果項(xiàng)目用的是docker build加docker push命令的方式也可以只要在 build 之前確認(rèn)登錄成功即可- name: Build and push with docker CLI run: | docker build -t ghcr.io/${{ github.repository_owner }}/demo:v1 . docker push ghcr.io/${{ github.repository_owner }}/demo:v1這種方式對于單階段構(gòu)建來說足夠不過多平臺構(gòu)建或需要緩存時(shí)用 build-push-action 會更順手。3.2 方案 B使用 PAT 推送的完整步驟如果你的場景屬于下面幾種用 PAT 更合適鏡像名 owner 不是當(dāng)前倉庫 owner比如要從用戶倉庫推到同一個(gè)組織下的另一個(gè)包需要跨倉庫復(fù)用同一個(gè)推送憑據(jù)workflow 里除了推 ghcr.io還需要調(diào)用其他需要更高權(quán)限的 API。創(chuàng)建 PAT 的路徑是GitHub 右上角頭像 - Settings - Developer settings - Personal access tokens - Tokens (classic)點(diǎn)擊 Generate new token在 scopes 里勾選write:packages注意勾選這個(gè)會自動帶上read:packagesdelete:packages如果需要刪除包按需勾選repo如果要推送的倉庫是 private且 workflow 需要訪問倉庫源碼則需要這個(gè) scope如果你用的是 Fine-grained token權(quán)限設(shè)置會更細(xì)需要在 Account permissions 里找到 Packages設(shè)置成 Read and write。同時(shí)要選擇一個(gè)目標(biāo)賬號或組織并授權(quán)對應(yīng)的倉庫。生成 token 后把它添加到倉庫的 secrets 中。路徑是倉庫 Settings - Secrets and variables - Actions - New repository secretName 填GHCR_TOKENValue 粘貼剛才生成的 token。然后 workflow 里改成- name: Log in to ghcr.io uses: docker/login-actionv3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GHCR_TOKEN }}這里有個(gè)坑使用 PAT 時(shí)github.actor是觸發(fā) workflow 的用戶名而 PAT 是你自己創(chuàng)建的那個(gè)賬號的憑據(jù)。如果觸發(fā)用戶和 PAT 不是同一個(gè)賬號可能會導(dǎo)致登錄失敗因?yàn)?ghcr.io 會拿用戶名去匹配 token。更穩(wěn)妥的做法是把用戶名也寫死在 secrets 里或者直接用 PAT 所屬賬號的 username。比如你的賬號是deploy-bot可以用with: registry: ghcr.io username: deploy-bot password: ${{ secrets.GHCR_TOKEN }}這樣能減少很多“為什么 username 明明對但登錄失敗”的疑惑。3.3 組織倉庫與私有包的特殊處理如果你的倉庫屬于某個(gè)組織推 ghcr.io 時(shí)還有一處容易忽略的配置。組織管理員可能對 “Package creation” 做了限制。需要去組織設(shè)置里確認(rèn)組織 Settings - Packages - Package creation確保允許 Actions 創(chuàng)建或更新容器鏡像否則即使 workflow 的 permissions 沒問題也會被組織層面的策略攔下來。另外第一次推送到 ghcr.io 后新創(chuàng)建的包默認(rèn)是私有的。如果你是構(gòu)建完鏡像其他環(huán)境要用需要手動調(diào)整包的可見性。調(diào)整路徑倉庫主頁 - Packages - 選擇對應(yīng)鏡像 - Package settings - Danger Zone - Change visibility改成 public 后其他人或服務(wù)器才能免登錄拉取。如果保持 private那拉取端也需要先登錄 ghcr.io并具備對應(yīng)包的讀權(quán)限。私有包的拉取配置常見做法是在服務(wù)器上維護(hù)一個(gè)只讀 token用 docker login 登錄后再 docker pull。具體權(quán)限只需要read:packages不需要寫權(quán)限這樣即使 token 泄露最壞情況也只是能拉取鏡像不能篡改。4. 常見問題與排查套路4.1 排查清單遇到write_package報(bào)錯我建議按這個(gè)順序逐項(xiàng)排查不要一上來就懷疑 token 泄露或者 workflow 寫錯確認(rèn) workflow 文件的permissions里是否包含packages: write。如果是在 job 級別設(shè)置的確認(rèn)當(dāng)前執(zhí)行 push 的 job 是哪一個(gè)。登錄用的 password 是secrets.GITHUB_TOKEN還是secrets.XXX。如果用 PAT確認(rèn) secret 名稱沒有拼寫錯誤。手動在本地跑一次 docker login 做驗(yàn)證。比如用 PAT 登錄 ghcr.io然后嘗試 push 一個(gè)測試鏡像看是不是同樣報(bào)錯。這一步能幫你區(qū)分問題是出在 GitHub Actions 環(huán)境還是出在 token 本身。檢查鏡像名的 owner 是否和 token 的歸屬一致。用戶倉庫推到用戶命名空間沒問題但推到組織命名空間時(shí)token 必須擁有該組織的權(quán)限。查看目標(biāo)包是否已經(jīng)存在且歸屬是否異常。如果包之前被推到別的 owner 下需要先把舊包刪除或調(diào)整權(quán)限。4.2 高頻報(bào)錯對照表為了讓你排查方便我整理了一張對照表列幾個(gè)最常見的情況報(bào)錯信息可能原因解決辦法denied: permission_denied: write_packagetoken 沒有 packages 寫權(quán)限在 workflow 中設(shè)置permissions: packages: write或改用有write:packages權(quán)限的 PATdenied: permission_denied: read_packagetoken 連讀權(quán)限都沒有檢查 token 是否至少勾選read:packagesunexpected status ... 403 Forbiddenpush 請求被拒絕通常是寫權(quán)限不足或命名空間不匹配檢查 owner 命名空間、token 權(quán)限、組織包設(shè)置denied: requested access to the resource is denied推送的 owner 不在 token 授權(quán)范圍內(nèi)如果使用 fine-grained token確認(rèn)已授權(quán)目標(biāo)賬號/組織login attempt to https://ghcr.io/v2/ failed with status: 401 Unauthorized登錄憑據(jù)錯誤或 token 無效檢查 secret 名稱、PAT 是否過期、用戶名是否匹配構(gòu)建成功但推送后拉取時(shí)報(bào) 401包是 private 狀態(tài)登錄后拉取或?qū)梢娦愿臑?public4.3 經(jīng)驗(yàn)與避坑最后分享幾個(gè)我在實(shí)際項(xiàng)目中積累的經(jīng)驗(yàn)。第一個(gè)是不要把permissions寫在整個(gè) workflow 頂層就完事。如果你同時(shí)存在多個(gè) job頂層 permissions 會作用于所有 job這其實(shí)沒問題但有些團(tuán)隊(duì)會有安全審計(jì)要求希望最小化權(quán)限。我建議把packages: write只加在真正需要推送的 job 上其他 job 保持只讀這樣即使某個(gè) job 被惡意注入也沒法拿 token 去污染 ghcr.io。第二個(gè)是關(guān)于secrets.GITHUB_TOKEN和自定義 secret 的選擇。有人覺得 PAT 一勞永逸就一直用 PAT但 PAT 一旦泄露影響范圍是整個(gè)賬號名下的所有包。GITHUB_TOKEN的優(yōu)勢在于每個(gè) workflow 都是獨(dú)立的臨時(shí) token自動過期即使日志里被打印出來別人也無法復(fù)用。所以能用GITHUB_TOKEN就優(yōu)先用它除非遇到它確實(shí)覆蓋不了的場景。第三個(gè)是如果你使用了docker/build-push-action要注意它和docker/login-action之間的執(zhí)行順序。login-action 必須在 build-push-action 之前執(zhí)行因?yàn)?build-push-action 會讀取 docker 的認(rèn)證配置。順序反了即使 login 成功push 時(shí)依然會提示未認(rèn)證。第四個(gè)是如果報(bào)錯信息指向buildx不要慌。buildx 是 Docker 的構(gòu)建插件它本身不負(fù)責(zé)權(quán)限校驗(yàn)。最終 403 還是 ghcr.io 返回的只是錯誤信息被包了一層多讀了那一行就容易被誤導(dǎo)。真正要看的是報(bào)錯里有沒有permission_denied或denied這種關(guān)鍵詞。第五個(gè)是遇到write_package報(bào)錯時(shí)可以先檢查 GitHub Packages 頁面。有時(shí)候 ghcr.io 上已經(jīng)有一個(gè)同名包但是歸屬在另一個(gè)賬號下。你可以打開鏡像的 Package settings 頁面看看 Owner 是誰。如果 owner 和你預(yù)期的不一致最快的修復(fù)方式是把舊包刪掉再重新推送一次。第一次推送會重新在正確的命名空間下創(chuàng)建包。關(guān)于鏡像拉取慢或者鏡像源配置這類問題限于篇幅這里不展開。如果你已經(jīng)能成功推送 ghcr.io那說明整個(gè) CI 鏈路已經(jīng)通了剩下的就是鏡像加速、緩存策略這些優(yōu)化層面的事按需調(diào)整就行。我在實(shí)際工作中遇到最多次的反而是最初級的問題workflow 文件里忘記寫permissions。GitHub 出于安全考慮在很多新建倉庫里默認(rèn)把GITHUB_TOKEN設(shè)成了只讀。這個(gè)設(shè)計(jì)很合理但也確實(shí)讓不少初次接入 ghcr.io 的團(tuán)隊(duì)卡在write_package上。只要記住一條任何操作 ghcr.io 的 job都必須顯式聲明packages: write然后像檢查密碼一樣去檢查你的登錄憑據(jù)來源這類問題基本都能在五分鐘內(nèi)定位。