Skip to content
Latchkey LogoLatchkey home

actions/deploy-pages failed to create deployment

deploy-pages failed to create deployment is a wrapper around whatever the Pages API returned, and the action appends one sentence per status code. The sentence is the diagnosis, and knowing which branch of the action wrote it saves you from changing the setting the other branch is about.

One catch block in the action mapping each HTTP status onto the sentence it appends
One catch block writes all of these. Quoted from src/internal/deployment.js: `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}.`

What this error means

The build job uploaded its artifact and the deploy job failed inside actions/deploy-pages. The step log runs the same way every time: the action fetches artifact metadata, prints the payload it is about to send, then prints Creating Pages deployment failed followed by the underlying error and a stack trace through its own files. The final line is the one that matters, because it carries both the HTTP status and a sentence the action chose from that status. A 403 is the common one and its sentence names a permission, which is why the fix belongs in the job's permissions block rather than in the Pages settings. The frames in the log below name deploy-pages/v4, the major that run used; this page recommends v5.

Actions log, quoted from j143/quest#3; each ... marks an elision
...
Creating Pages deployment with payload:
...
Error: Creating Pages deployment failed
Error: HttpError: Resource not accessible by integration
    ...
    at createPagesDeployment (/home/runner/work/_actions/actions/deploy-pages/v4/src/internal/api-client.js:125:1)
    at Deployment.create (/home/runner/work/_actions/actions/deploy-pages/v4/src/internal/deployment.js:74:1)
    at main (/home/runner/work/_actions/actions/deploy-pages/v4/src/index.js:30:1)
Error: Error: Failed to create deployment (status: 403) with build version f840928706dd3d3ead87378c6355e134bc49c5b6. Request ID BC00:1E7164:A2C100:14CF1AE:68845E0B Ensure GITHUB_TOKEN has permission "pages: write".

The status decides the sentence

Every one of these messages comes out of a single catch in Deployment.create, in src/internal/deployment.js. It starts from one template, Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}., adds the request id when the response carried one, and then appends exactly one more sentence chosen by the status. The table is that branch, in source order.

The 400 row is the one that says least on its own, because what the action appends there is whatever the Pages API replied. One reply is common enough to name: Responded with: Missing environment. Ensure your workflow's deployment job has an environment. Example:, followed by a YAML fragment showing environment: name: github-pages on the deploy job. It fires when the deploy job has no environment: key at all, so the fix is to add the key, and no setting in the repository is involved. Naming an environment the repository does not have is a different thing entirely and does not fail at all, because the run creates it: Missing environment production covers that.

Status from the Pages APISentence the action appendsWhere the fix lives
400 Responded with: and the API's own messageThe deploy job's environment:, or the Pages source setting
403 Ensure GITHUB_TOKEN has permission "pages: write".The job's permissions block
404 Ensure GitHub Pages has been enabled: and a link to the repository's Pages settingsThe repository Pages settings
500 or above Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.Nowhere; re-run it
Anything elsenothing is appendedRead the underlying error above the line

Common causes

The deploy job has no pages: write

The 403, and the most common cause by a distance. It appears the moment someone replaces a workflow-level permissions block with a narrower job-level one and forgets that the deploy job needed two entries. The job setup summary lists the permissions the token actually got, which is worth comparing against the two the README names.

The job has no id-token: write, or is a fork run where it cannot have one

This fails earlier, before the create call, with the id-token sentence. It has two quite different origins: the permission was never granted, or the run is a pull request from a fork, where write scopes are downgraded no matter what the file asks for. In the second case granting the permission cannot help.

Pages is not enabled on the repository

The 404 branch, and the one that names its own fix by printing a link straight to the repository's Pages settings. It is what a brand new repository does when the workflow was copied in before Pages was turned on.

The Pages service returned a server error

The 500-and-above branch, and the only one the action tells you to wait out rather than fix. Its sentence points at githubstatus.com for a reason: there is nothing in the repository to change, and a re-run is the entire remedy.

How to fix it

Read the last line of the step, not the stack trace

  1. Scroll to the bottom of the deploy step and find the line beginning Failed to create deployment.
  2. Take the status out of the parentheses and look it up in the table above; the sentence after it is the action telling you which branch it took.
  3. If the failure is the id-token sentence instead, the create call never ran and the problem is one step earlier.

Grant both permissions on the deploy job

Add pages: write and id-token: write to the job that runs actions/deploy-pages, keeping contents: read for the checkout. Put them on the job rather than the workflow so the build job does not carry them too.

Stop asking configure-pages for a permission it does not use

If your workflow sets permissions on the configure-pages step or its job because a guide said the pages: write message came from there, move them. The sentence is written in the deploy action's source, and the API call it guards is the one the deploy job makes.

.github/workflows/pages.yml (illustrative)
  build:
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v7
      - uses: actions/configure-pages@v6
      - uses: actions/upload-pages-artifact@v5
        with:
          path: ./_site

Deploy from a trusted trigger when the pull request comes from a fork

A fork pull request cannot be granted id-token: write, so a preview deploy has to move. Build and upload on pull_request with a read-only token, and run the deploy in a second workflow on workflow_run, which does get write scopes. Treat the artifact as untrusted data, since a fork produced it.

Where the pages: write sentence actually lives

This matters because the advice attached to it is frequently misfiled. The sentence Ensure GITHUB_TOKEN has permission "pages: write". is written by actions/deploy-pages, in the 403 branch above. It is not written by actions/configure-pages, whose README contains no permissions section at all. Guidance that tells you to add pages: write to your configure-pages step is treating the two actions as interchangeable, and the permission belongs on the job that deploys.

The companion sentence is in a different file again. Before any of the above runs, src/index.js asks for an OIDC token, and if that request throws it calls core.setFailed with Ensure GITHUB_TOKEN has permission "id-token: write". and returns. So a run that fails with the id-token sentence never reached the create call, and a run that fails with the pages sentence got past the OIDC step. They are two different failures with near-identical wording, and the order in the log tells them apart.

Actions log, quoted from thomaspinder/GPJax#635
Error: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable
##[error]Ensure GITHUB_TOKEN has permission "id-token: write".

The permissions the action actually asks for

The action's own README states the requirement rather than leaving it to the error messages. Under its security considerations it says that "The job that executes the deployment must at minimum have the following permissions: pages: write, id-token: write". It also explains why both are needed, which is the part that makes the two messages make sense: "The pages permission relates to the GITHUB_TOKEN by giving it the permissions to create pages deployments when calling the GitHub API. The id-token permission is necessary to request the OIDC JWT token."

Both go on the deploy job. Putting them at the workflow level works too but widens them across every job in the file, including the build, which needs neither.

.github/workflows/pages.yml (illustrative)
  deploy:
    needs: build
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - id: deployment
        uses: actions/deploy-pages@v5

Two failures that look like this one and are not

The first is the artifact. Before it creates anything the action asks for artifact metadata, and when nothing matches it throws its own message rather than an HTTP error: No artifacts named "github-pages" were found for this workflow run. Ensure artifacts are uploaded with actions/upload-artifact@v4 or later. That is a build-job problem, and the deploy job is only where it surfaced. The artifact name is an input, defaulting to github-pages, so a custom name on the upload and not on the deploy produces exactly this.

The second is the polling phase, after a deployment has been created successfully. If the status checks keep erroring the action gives up with Too many errors, aborting! and fails with Failed with status code: and the last status it saw; if the clock runs out it prints Timeout reached, aborting!. Both cancel the deployment on the way out. Neither is a create failure, so neither is answered by a permission.

Why there is no recorded run on this page

Every branch in that table is a response from the Pages API about one repository's permissions and settings. A run on our repository could only produce the success path, because our settings are already correct, and deliberately breaking them would record a log that says what the source above already says, with our repository name in it. The two logs quoted here come from public repositories that hit the real thing, and the mapping is read from the action's source at the current major.

How to prevent it

  • Keep the two Pages permissions on the deploy job, and nowhere else in the file.
  • Use the same artifact name on the upload and the deploy, or leave both at the default.
  • Treat a 5xx sentence as a re-run and everything else as a change, so nobody edits settings during an outage.
  • Pin the action to a major tag, since these sentences and the statuses they map to live in its source.

Frequently asked questions

Which action prints "Ensure GITHUB_TOKEN has permission 'pages: write'"?
actions/deploy-pages, in the 403 branch of the catch inside Deployment.create in src/internal/deployment.js. It is not printed by actions/configure-pages, whose README has no permissions section at all. Add the permission to the job that deploys, not to the job that configures.
What is the difference between the pages: write and id-token: write errors?
They come from different files and different moments. The id-token sentence is set by src/index.js when the OIDC token request throws, before any deployment is created. The pages sentence is appended in src/internal/deployment.js when the create call itself returns 403. If you see the first, the create call never ran.
Why does the Pages deploy fail only on pull requests from forks?
Because a fork pull request gets a read-only GITHUB_TOKEN, so no OIDC token is issued and the action fails at its first step with the id-token sentence. Granting the permission in the file changes nothing, since the downgrade happens after the file is read. Move the deploy to a workflow_run workflow.
Should I retry a "Failed to create deployment" error?
Only when the status is 500 or above, which is the one branch whose sentence asks you to. It points at githubstatus.com and says to re-run later. A 400, 403 or 404 will return the same answer on every attempt, because each of those is a settled fact about the repository rather than a transient condition.

Related guides

References

The deploy is quick. Waiting for a runner is not. Latchkey starts the build job without the queue. Start free → 30-day trial · No credit card