十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GitHub Actions推送镜像到ghcr.io报错write_package的完整修复指南

GitHub Actions推送镜像到ghcr.io报错write_package的完整修复指南 但凡用 GitHub Actions 推过 Docker 镜像的人大概率都撞到过这个报错denied: permission_denied: write_package或者是推送过程中突然冒出一句unexpected status from POST request to https://ghcr.io/v2/xxx/xxx/blobs/uploads/: 403 Forbidden第一次遇到的时候我盯着日志看了半天login 明明成功了build 也没问题偏偏到 push 那一步就给你卡死而且返回的还是“权限不足”这种让人摸不着头脑的话。后来翻了不少资料、踩了几个坑才把这里的权限链路彻底捋顺。这篇文章就把 write_package 这个报错的来龙去脉、权限模型、完整修复方案和排查套路一次性讲清楚。无论你是刚接触 GitHub Actions 的新手还是已经推过一段时间镜像但偶尔被它绊一下的老手按下面的步骤走一遍基本能彻底摆脱这个问题。1. 现象还原write_package 报错到底是什么1.1 报错现场与最小复现先看一个最典型的、能稳定复现这个错误的 workflow 配置。很多项目一开始就是这么写的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这段配置看着没毛病实际上跑起来就会在docker push那一步报权限错误。报错通常长这样ERROR: failed to push ghcr.io/xxx/demo:v1: denied: permission_denied: write_package还有另一种形态是 buildx 在推多个层的时候报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两种报错的根因其实一样当前使用的凭据对目标镜像所在的 GitHub Packages 包没有写入权限。1.2 为什么“看起来没问题”却一直失败很多人的第一反应是“Docker Hub 都能推ghcr.io 怎么就不行”。这其实是把 GitHub Packages 和 Docker Hub 的鉴权模型搞混了。Docker Hub 的 push 权限是跟着你的账号走的只要登录的是有权限的账号推哪个仓库基本都能通过。但 GitHub Packages 的权限校验比它多了一层它不仅要验证“你是谁”还要验证“你对这个包有没有写入权限”。这里的“包”指的是 ghcr.io 上的镜像仓库package它的归属、可见性、写入权限和 GitHub 仓库的权限体系是深度绑定的。也就是说如果你用的是GITHUB_TOKEN它的权限范围由 workflow 的permissions设置决定如果没有显式声明packages: write默认情况下这个 token 只能读不能写一旦 push 请求发出去GitHub Packages 发现 token 没有写权限就直接返回write_package这个错误码。所以问题根本不是 docker login 失败而是登录成功后token 的权限标签不够。这就像你拿着门禁卡进了大楼但电梯权限没开按了楼层照样报警。2. 权限模型拆解GitHub Packages 的三种身份与写权限门槛2.1 GITHUB_TOKEN 与 PAT 的权限差异GitHub Actions 里有两种常见的 token一类是自动生成的GITHUB_TOKEN。每个仓库执行 workflow 时都会临时生成一个它的默认权限范围取决于仓库的配置。在较老或未调整过的仓库里这个 token 默认是只读的能 checkout 代码能读包的信息但推不了镜像。要给它写权限必须在 workflow 里显式声明。另一类是个人访问令牌PATPersonal Access Token这是你自己在账号设置里创建的。它不属于某个仓库而是属于你这个账号。你需要手动勾选访问范围比如write:packages、read:packages。把 PAT 放到仓库的 Secrets 里在 workflow 里调用就能用它来登录 ghcr.io。这两者的关系可以简单理解成GITHUB_TOKEN是“临时工”权限由配置决定workflow 结束后自动失效PAT 是“长期员工”创建时定好权限只要不手动撤销就一直有效。写 workflow 时优先用GITHUB_TOKEN更安全因为它只在需要的地方临时生效。但如果 workflow 需要跨仓库推送、或者目标镜像不归当前仓库所有PAT 反而是更灵活的选择。2.2 镜像命名空间、仓库归属与权限校验规则权限报错还和一个容易被忽略的细节有关镜像名的 owner 部分。ghcr.io 的镜像地址结构是这样的ghcr.io/owner/image-name:tagowner可以是用户名也可以是组织名。GitHub 在校验权限时会检查当前 token 是否对owner下的这个包有写权限。举个例子。假如你的仓库是alice/my-projectgithub.repository_owner是alice镜像名是ghcr.io/alice/my-image。这种情况下只要GITHUB_TOKEN有 packages 写权限就能正常推送。但如果是 fork 场景就要小心了。fork 出来的仓库里github.repository_owner会变成 fork 后的属主如果 workflow 里把镜像写死了比如ghcr.io/alice/my-image但当前 token 实际对应bob的仓库那么即使是alice仓库里定义的公开包用bob的 token 去推alice的命名空间一样会报write_package。还有一种情况是同一个仓库内如果之前用别的账号或错误的命名空间创建过同名包GitHub Packages 会把这个包归到那个命名空间下后续再用新 token 推就会遇到权限冲突。这类问题排查起来非常隐蔽因为你可能以为自己在推 A 包实际上 GitHub 把它识别成了另一个 B 包而当前 token 对 B 包确实没有写权限。2.3 workflow permissions 的两种配置路径解决写权限核心就是让GITHUB_TOKEN拥有packages: write。配置方式有两种建议两个地方都确认一遍。第一种是在 workflow 文件里显式声明。可以在文件顶层设置也可以在 job 级别设置permissions: contents: read packages: write放在 job 级别会更精确不会把多余权限扩散到其他 job。比如jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write第二种是在仓库设置里修改默认 Workflow permissions。路径是仓库 Settings - Actions - General - Workflow permissions把选项从 “Read repository contents and packages permissions” 改成 “Read and write permissions”。需要注意的是仓库级设置只是默认值。如果 workflow 文件里显式写了permissions那以 workflow 文件为准。所以我习惯的做法是仓库设置允许读写同时 workflow 文件里也显式声明双保险。3. 完整修复方案从零到一推送 ghcr.io 镜像3.1 方案 A用 GITHUB_TOKEN 的最小配置如果你只想让当前仓库的 workflow 把镜像推到当前所有者的 ghcr.io 命名空间用GITHUB_TOKEN就够了不需要额外创建任何密钥。完整可用的 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 }}这里有几个值得留意的细节docker/login-action的username用github.actor也就是触发 workflow 的用户名这是官方示例的常见写法。重要的是password必须是secrets.GITHUB_TOKEN而不是任何明文密码。docker/build-push-action的push: true表示构建完成后直接推送它会复用前面 login-action 生成的 Docker 配置不需要再单独写docker push。tags 里建议同时打latest和 commit SHA 的标记方便后续回溯。如果项目用的是docker build加docker push命令的方式也可以只要在 build 之前确认登录成功即可- 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这种方式对于单阶段构建来说足够不过多平台构建或需要缓存时用 build-push-action 会更顺手。3.2 方案 B使用 PAT 推送的完整步骤如果你的场景属于下面几种用 PAT 更合适镜像名 owner 不是当前仓库 owner比如要从用户仓库推到同一个组织下的另一个包需要跨仓库复用同一个推送凭据workflow 里除了推 ghcr.io还需要调用其他需要更高权限的 API。创建 PAT 的路径是GitHub 右上角头像 - Settings - Developer settings - Personal access tokens - Tokens (classic)点击 Generate new token在 scopes 里勾选write:packages注意勾选这个会自动带上read:packagesdelete:packages如果需要删除包按需勾选repo如果要推送的仓库是 private且 workflow 需要访问仓库源码则需要这个 scope如果你用的是 Fine-grained token权限设置会更细需要在 Account permissions 里找到 Packages设置成 Read and write。同时要选择一个目标账号或组织并授权对应的仓库。生成 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 }}这里有个坑使用 PAT 时github.actor是触发 workflow 的用户名而 PAT 是你自己创建的那个账号的凭据。如果触发用户和 PAT 不是同一个账号可能会导致登录失败因为 ghcr.io 会拿用户名去匹配 token。更稳妥的做法是把用户名也写死在 secrets 里或者直接用 PAT 所属账号的 username。比如你的账号是deploy-bot可以用with: registry: ghcr.io username: deploy-bot password: ${{ secrets.GHCR_TOKEN }}这样能减少很多“为什么 username 明明对但登录失败”的疑惑。3.3 组织仓库与私有包的特殊处理如果你的仓库属于某个组织推 ghcr.io 时还有一处容易忽略的配置。组织管理员可能对 “Package creation” 做了限制。需要去组织设置里确认组织 Settings - Packages - Package creation确保允许 Actions 创建或更新容器镜像否则即使 workflow 的 permissions 没问题也会被组织层面的策略拦下来。另外第一次推送到 ghcr.io 后新创建的包默认是私有的。如果你是构建完镜像其他环境要用需要手动调整包的可见性。调整路径仓库主页 - Packages - 选择对应镜像 - Package settings - Danger Zone - Change visibility改成 public 后其他人或服务器才能免登录拉取。如果保持 private那拉取端也需要先登录 ghcr.io并具备对应包的读权限。私有包的拉取配置常见做法是在服务器上维护一个只读 token用 docker login 登录后再 docker pull。具体权限只需要read:packages不需要写权限这样即使 token 泄露最坏情况也只是能拉取镜像不能篡改。4. 常见问题与排查套路4.1 排查清单遇到write_package报错我建议按这个顺序逐项排查不要一上来就怀疑 token 泄露或者 workflow 写错确认 workflow 文件的permissions里是否包含packages: write。如果是在 job 级别设置的确认当前执行 push 的 job 是哪一个。登录用的 password 是secrets.GITHUB_TOKEN还是secrets.XXX。如果用 PAT确认 secret 名称没有拼写错误。手动在本地跑一次 docker login 做验证。比如用 PAT 登录 ghcr.io然后尝试 push 一个测试镜像看是不是同样报错。这一步能帮你区分问题是出在 GitHub Actions 环境还是出在 token 本身。检查镜像名的 owner 是否和 token 的归属一致。用户仓库推到用户命名空间没问题但推到组织命名空间时token 必须拥有该组织的权限。查看目标包是否已经存在且归属是否异常。如果包之前被推到别的 owner 下需要先把旧包删除或调整权限。4.2 高频报错对照表为了让你排查方便我整理了一张对照表列几个最常见的情况报错信息可能原因解决办法denied: permission_denied: write_packagetoken 没有 packages 写权限在 workflow 中设置permissions: packages: write或改用有write:packages权限的 PATdenied: permission_denied: read_packagetoken 连读权限都没有检查 token 是否至少勾选read:packagesunexpected status ... 403 Forbiddenpush 请求被拒绝通常是写权限不足或命名空间不匹配检查 owner 命名空间、token 权限、组织包设置denied: requested access to the resource is denied推送的 owner 不在 token 授权范围内如果使用 fine-grained token确认已授权目标账号/组织login attempt to https://ghcr.io/v2/ failed with status: 401 Unauthorized登录凭据错误或 token 无效检查 secret 名称、PAT 是否过期、用户名是否匹配构建成功但推送后拉取时报 401包是 private 状态登录后拉取或将包可见性改为 public4.3 经验与避坑最后分享几个我在实际项目中积累的经验。第一个是不要把permissions写在整个 workflow 顶层就完事。如果你同时存在多个 job顶层 permissions 会作用于所有 job这其实没问题但有些团队会有安全审计要求希望最小化权限。我建议把packages: write只加在真正需要推送的 job 上其他 job 保持只读这样即使某个 job 被恶意注入也没法拿 token 去污染 ghcr.io。第二个是关于secrets.GITHUB_TOKEN和自定义 secret 的选择。有人觉得 PAT 一劳永逸就一直用 PAT但 PAT 一旦泄露影响范围是整个账号名下的所有包。GITHUB_TOKEN的优势在于每个 workflow 都是独立的临时 token自动过期即使日志里被打印出来别人也无法复用。所以能用GITHUB_TOKEN就优先用它除非遇到它确实覆盖不了的场景。第三个是如果你使用了docker/build-push-action要注意它和docker/login-action之间的执行顺序。login-action 必须在 build-push-action 之前执行因为 build-push-action 会读取 docker 的认证配置。顺序反了即使 login 成功push 时依然会提示未认证。第四个是如果报错信息指向buildx不要慌。buildx 是 Docker 的构建插件它本身不负责权限校验。最终 403 还是 ghcr.io 返回的只是错误信息被包了一层多读了那一行就容易被误导。真正要看的是报错里有没有permission_denied或denied这种关键词。第五个是遇到write_package报错时可以先检查 GitHub Packages 页面。有时候 ghcr.io 上已经有一个同名包但是归属在另一个账号下。你可以打开镜像的 Package settings 页面看看 Owner 是谁。如果 owner 和你预期的不一致最快的修复方式是把旧包删掉再重新推送一次。第一次推送会重新在正确的命名空间下创建包。关于镜像拉取慢或者镜像源配置这类问题限于篇幅这里不展开。如果你已经能成功推送 ghcr.io那说明整个 CI 链路已经通了剩下的就是镜像加速、缓存策略这些优化层面的事按需调整就行。我在实际工作中遇到最多次的反而是最初级的问题workflow 文件里忘记写permissions。GitHub 出于安全考虑在很多新建仓库里默认把GITHUB_TOKEN设成了只读。这个设计很合理但也确实让不少初次接入 ghcr.io 的团队卡在write_package上。只要记住一条任何操作 ghcr.io 的 job都必须显式声明packages: write然后像检查密码一样去检查你的登录凭据来源这类问题基本都能在五分钟内定位。
返回列表