# actions/deploy-pages failed to create deployment

> deploy-pages failed to create deployment appends one sentence per HTTP status, and that sentence is the diagnosis. Here is the code that writes it.

Source: https://latchkey.dev/learn/github-actions/github-actions-deploy-pages-action-create-failed  
Updated: 2026-09-20

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.

## 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".
```

## 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.

> The rule behind that is on its own page: [GITHUB_TOKEN read-only on fork pull requests](/learn/github-actions/github-actions-token-push-403-fork).

## 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.

## 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](/learn/github-actions/github-actions-environment-missing-production) covers that.

| Status from the Pages API | Sentence the action appends | Where the fix lives |
| --- | --- | --- |
| 400 | ` Responded with: ` and the API's own message | The 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 settings | The 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 else | nothing is appended | Read the underlying error above the line |

> Note the request id has no colon after it. The template writes `Request ID ` and then the value, so a log that reads `Request ID: abc` did not come from this action.

## 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.

> What the deployment reports after it has been created is a separate page: [actions/deploy-pages deployment failed](/learn/github-actions/deploy-pages-deployment-failed-environment).

## 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.

## FAQ

### 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.

## References

- [actions/deploy-pages: src/internal/deployment.js, the create catch and its status branches](https://github.com/actions/deploy-pages/blob/main/src/internal/deployment.js)
- [actions/deploy-pages: src/index.js, the OIDC pre-flight and its message](https://github.com/actions/deploy-pages/blob/main/src/index.js)
- [actions/deploy-pages: README security considerations, the two required permissions](https://github.com/actions/deploy-pages#security-considerations)
- [j143/quest#3: a 403 quoting the full message with its request id](https://github.com/j143/quest/issues/3)
- [xDuinoRails/xDuinoRails_Thor#3: a 400 quoting the Missing environment reply in full](https://github.com/xDuinoRails/xDuinoRails_Thor/issues/3)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
