GitHub Actions: ghcr.io push "denied: permission_denied: write_package"
The registry accepted the token and then refused to write to that package. A successful login says nothing about the package. There are two questions, in this order: does the job's token have packages: write, and is this repository allowed to write to a package that already exists?
What it looks like
Login Succeeded! ... ERROR: failed to build: failed to solve: failed to push ghcr.io/octo-org/web:1.4.0: denied: permission_denied: write_package
That is docker/build-push-action; docker push and helm push end the same way (mcp-context-forge #7074). For a package of an organization the message can instead be denied: installation not allowed to Write organization package. In October reports that form came mostly from a token with no write at all, and write_package mostly from a package that refused the repository, but both turn up for both causes, so check in the same order.
1. The job's token has no packages: write
- The restricted default. "By default, when you create a new repository in your personal account,
GITHUB_TOKENonly has read access for thecontentsandpackagesscopes", and organizations can set the same. After one organization switched, every workflow without apermissionsblock lost the push (restate-cloud-tunnel-client #28). - A
permissionsblock that leavespackagesout sets it tonone. A workflow-levelcontents: readdid that in books_downloader (#70). - A reusable workflow cannot get more than its caller grants, so both files need it (restate-cloud-tunnel-client #28).
- A pull request from a fork gets a read-only token, and the
permissionskey cannot raise it. Build those without pushing (shutterbase #128).
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/octo-org/web:${{ github.sha }}
2. The package exists, and this repository may not write to it
With packages: write already set, the package is the problem; godot-mcp had both, logged in with GITHUB_TOKEN, and was refused only at the push (godot-mcp #706). The Container registry documentation says the GITHUB_TOKEN "will not have permission to push the package if you have previously pushed a package to the same namespace, but have not connected the package to the repository". Open the package's settings page and look at "Manage Actions access" and the linked repository. The usual stories:
- Someone pushed it first from a laptop or with a personal token. A fork's packages created that way refused the fork's own
GITHUB_TOKEN(armada-rp6 #59). - Another repository published it first.
ghcr.io/unipept/unipept-webbelonged tounipept/docker-images, so the v6.5.5 release ofunipept/unipeptfailed (unipept #1894). A monorepo that replaced split repositories hit it on eight images at once (myota #53). - The repository was renamed or rebuilt, and the package still belongs to the old one (iozy #14).
- The repository was transferred to another account. For the Container registry the link is removed, and "Any codespaces or GitHub Actions workflows associated with the repository will lose access to the package."
- The organization turned off "Inherit access from source repository" for new packages, so a new package does not give the linked repository's workflows access.
The ways out
- Give the repository access. On the package's settings page, under "Manage Actions access", use Add Repository and give it the Write role. You need admin on the package. This keeps the name, as myota did.
- Push under a name nobody owns yet. A package published from a workflow with
GITHUB_TOKENis linked to that repository automatically, which is how unipept and iozy fixed it. Anything that pulls the old name has to follow. - For a name that is new but will first be pushed from elsewhere, add the
org.opencontainers.image.sourcelabel with this repository's URL. The package inherits the repository's access only if the label is there before the first publish; connecting an existing package from its settings keeps its current permissions unless you choose to inherit. - Use a personal access token with
write:packagesas a secret, as adcombi/ruby-ci-image did (#3). It works for any package that person can write, but the documentation recommendsGITHUB_TOKENfor this registry, the token belongs to one person, and secrets do not reach fork pull requests either.
Telling it apart
- "unauthorized: unauthenticated: User cannot be authenticated with the token provided": the token was not accepted at all, so the token is empty, wrong or expired.
- "repository name must be lowercase": the image name has a capital letter, often from
${{ github.repository }}with a mixed-case owner. - A
git pushrefused with "Permission to ... denied to github-actions[bot]" is the same kind of missing write, forcontents(git push and github-actions[bot]).
See failed jobs from IntelliJ IDEA
Pipeline Inbox for GitHub Actions Workflows, a plugin by Lesstep for JetBrains IDEs 2024.1 or later, lists the runs of all your repositories in an IDE tool window, with their jobs and steps, and your current branch's runs are in the status bar. Reading is free; re-running failed jobs, cancelling, running workflows and reviewing deployments are Pro (US$1.50 a month, with a 30-day trial).
GitHub, GitHub Actions and GitHub Packages are trademarks of GitHub, Inc.; Docker is a trademark of Docker, Inc.; IntelliJ IDEA is a trademark of JetBrains s.r.o. This page is not affiliated with, sponsored or endorsed by GitHub, Inc., Docker, Inc. or JetBrains.
More: "Resource not accessible by integration" · "Permission to ... denied to github-actions[bot]" on git push · "No space left on device" · GitHub Actions plugins for IntelliJ IDEA compared · 日本語