An actions/deploy-pages deployment failed at one of three gates, and the line in the log tells you which. Two of them reject the ref you are deploying from and one of them rejects what you uploaded, so the fix is a repository setting in two cases out of three and a rebuild in the third.

Gate one leaves no step log. Quoted from the deployments reference: "All deployment protection rules must pass before a job referencing the environment is sent to a runner."
What this error means
The build job is green and the deploy job is not. Which gate stopped it is visible from how much of the deploy job ran. No steps at all, with the failure as an annotation on the job itself, means the environment refused the ref before the job reached a runner. A deploy step that ran and failed while creating the deployment means the Pages API refused the call, and the action quotes its response. A step that created the deployment and then failed while polling means the deployment reached a terminal state, and the action prints one fixed sentence for it. Three gates, three repairs, and the first leaves no step log at all, which is why it gets mistaken for a broken action.
```Job annotation, quoted from actions/deploy-pages#151
Branch "v1.1.2" is not allowed to deploy to github-pages due to environment protection rules.
## Common causes
### The github-pages environment does not allow the ref
The loudest of the three and the one with no step log. It fires on a tag, on a release-triggered run, and on any ref that does not match the environment's deployment branch and tag rule. Because the failure is an annotation on the job rather than a step, the run looks like the action never started, which it did not.
### The Pages source is still a branch
A repository that published from `gh-pages` before it moved to a workflow keeps that setting until someone changes it. The API refuses the create call with a 400 and says which branch it will accept, and the action passes that sentence through. In our experience it is the most common 400 on a repository that used to deploy a Jekyll site.
### The deployment reached a terminal status after it was created
The create call succeeded, so permissions and the ref were both fine, and the deployment failed on the Pages side. The sentence in the log names which terminal status it was, and only `deployment_content_failed` points at your artifact rather than at the service.
### The artifact carries content Pages will not publish
Hard links, symlinks and total size are the three the action names. A build that copies a tree with `cp -al`, or one that ships a full `node_modules`, produces exactly this. It is the only cause here whose fix is in the build job.
## How to fix it
### Work out which gate stopped the deploy before changing anything
1. Open the deploy job. If it has no steps, the environment refused the ref and the answer is in the environment settings.
2. If the deploy step ran and the message starts `Failed to create deployment`, the API refused the call and the status code in that line is the diagnosis.
3. If the step created a deployment and then printed one of the fixed sentences above, the deployment failed after creation and the sentence names which way.
### Allow the ref you are actually deploying from
In the repository settings, open the `github-pages` environment and look at deployment branches and tags. Either choose no restriction, or add a pattern that matches the run's ref. Remember the matching is against `GITHUB_REF`, so a workflow triggered by a release needs a tag pattern and not a branch name. GitHub notes that wildcards will not match a slash, so a pattern of `release/*` does not cover `release/2026/09`.
### Switch the Pages source to GitHub Actions
This is the repair for the 400 that names a branch. In the repository's Pages settings, set the build and deployment source to GitHub Actions. Until that is done, the API will keep telling you which branch it expects, no matter what the workflow file says.
```.github/workflows/pages.yml (illustrative)
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- id: deployment
uses: actions/deploy-pages@v5
Fix the artifact when the status is deployment_content_failed
- Check the upload step for symlinks and hard links;
find <dir> -type landfind <dir> -links +1 -type fboth print quickly. - Check the size of the directory you are uploading, and compare it against the warning line the action prints before it creates the deployment.
- Rebuild, re-upload, and re-run the deploy job on its own rather than the whole workflow.
How to prevent it
- Decide once whether the
github-pagesenvironment restricts refs, and write the pattern for the trigger you actually use. - Move the Pages source to GitHub Actions the day you add the workflow, not the day the deploy first fails.
- Keep the upload step pointed at a build output directory, never at the repository root.
- Pin
actions/deploy-pagesto a major tag and read its release notes, since the status strings and messages live in its source.
Gate one: the environment refuses the ref
A job with environment: github-pages is checked against that environment before it is scheduled. GitHub's reference is unambiguous about the ordering: "All deployment protection rules must pass before a job referencing the environment is sent to a runner." When the deployment branch and tag rule does not match, the job fails as an annotation with the line quoted above and no step ever runs. Nothing in actions/deploy-pages produced that message, so reading its source or bumping its version will not move it.
The rule is matched against the run's ref, not against your branch name in the abstract: "The deployment branch or tag rule is matched against the GITHUB_REF of the workflow run." The options GitHub documents are "No restriction", "Protected branches only", and "Selected branches and tags", and the third is where tags trip people up. A release-triggered deploy has a tag ref, so a policy that lists main rejects it. GitHub also warns that "Deployment workflow runs triggered by tags with the same name as a protected branch and forks with branches that match the protected branch name cannot deploy to the environment."
Gate two: the Pages API refuses the create call
If the job did start and the deploy step ran, the next gate is POST /repos/{owner}/{repo}/pages/deployments. The action logs Creating Pages deployment failed from src/internal/api-client.js and then builds a message from the status code. For a 400 it appends the API's own words, which is how this shape reads in a real run:
```Actions log, quoted from actions/deploy-pages#108
Error: Failed to create deployment (status: 400) with build version efba898182be4068f5a83d356f1a5bf241229025. Responded with: Invalid deployment branch and no branch protection rules set in the environment. Deployments are only allowed from gh-pages
> That is a different rule from gate one. It is the Pages **source** setting, which still names a branch, and the repair is to switch the Pages source to GitHub Actions in the repository's Pages settings. The other status codes have their own page: [deploy-pages failed to create deployment](https://latchkey.dev/learn/github-actions/github-actions-deploy-pages-action-create-failed).
## Gate three: the deployment is created and then reports failure
Once the deployment exists, the action polls `GET /repos/{owner}/{repo}/pages/deployments/{deploymentId}` and reads a status string. `src/internal/deployment.js` holds two maps of those strings to sentences, one it treats as terminal and one it treats as temporary, and the sentence in your log tells you which map it came from. This is the table to read when the deploy step ran for a while and then gave up.
| Status from the API | Sentence the action prints | What the action does next |
| --- | --- | --- |
| `deployment_failed` | `Deployment failed, try again later.` | Fails the step |
| `deployment_content_failed` | `Artifact could not be deployed. Please ensure the content does not contain any hard links, symlinks and total size is less than 10GB.` | Fails the step |
| `deployment_cancelled` | `Deployment cancelled.` | Fails the step |
| `deployment_lost` | `Deployment failed to report final status.` | Fails the step |
| `deployment_attempt_error` | `Deployment temporarily failed, a retry will be automatically scheduled...` | Warns and keeps polling |
| `not_found` | `Deployment not found.` | Warns and keeps polling |
| `unknown_status` | `Unable to get deployment status.` | Warns and keeps polling |
> The last three rows share one map, and the poll loop warns on all three and keeps going until it times out. `Deployment not found.` and `Unable to get deployment status.` do also reach a failure, but only from two guard clauses at the top of the same `check()` method, which fire when no deployment was created at all. Under this table's own header, a status the API returned, all three are warnings.
## The content gate is about the artifact, not the ref
The `deployment_content_failed` sentence is the odd one in that table, because it names something you fix in the build rather than in a setting. The action warns before it gets there: its `create` method checks the artifact size against one gigabyte and logs that the uploaded size "exceeds the allowed size of 1 GB. Deployment might fail." The gigabyte in that warning and the ten in the terminal sentence are two thresholds rather than a contradiction, and `actions/upload-pages-artifact` states both: "The GitHub Pages officially supported maximum size limit is 1GB, so the subsequent deployment of larger tarballs are not guaranteed to succeed", and "there is also an unofficial absolute maximum size limit of 10GB, which Pages will not even attempt to deploy". GitHub's published Pages limits carry the same supported figure and the mechanism behind it: a site "may be no larger than 1 GB", and deployments "will timeout if they take longer than 10 minutes". So one gigabyte is the limit, stated softly because a larger site sometimes still lands inside the timeout, and ten is where Pages stops trying. The repair is in whatever produced the site: a symlink into `node_modules`, a hard link left by a copy step, or an output larger than you think. Environment settings will not move it, because this gate is downstream of both ref checks.
## Why there is no recorded run on this page
Gate one never reaches a runner, so there is no job to record. Gate two and gate three are answers from the Pages service about a specific repository's settings and a specific artifact, and a run of ours on our own repository would only prove that our settings are configured, which is not the reader's question. Both log excerpts here are quoted from issues on the action's own repository, and the mapping table is read from the action's source rather than reproduced.
## FAQ
### Why does my deploy job fail with no steps at all?
Because the environment refused the ref before the job was scheduled. GitHub documents that all deployment protection rules must pass before a job referencing the environment is sent to a runner, so a failed rule produces an annotation on the job and no step log. The message names the branch or tag and the environment it was refused from.
### What does "Deployments are only allowed from gh-pages" mean?
It is the Pages API refusing the create call because the repository still publishes from a source branch. It arrives inside a `Failed to create deployment (status: 400)` line, with the API response quoted after `Responded with:`. The fix is in the repository Pages settings, where the build and deployment source becomes GitHub Actions.
### Is "Deployment failed, try again later." worth retrying?
Only once. The action already keeps polling on its own for the three statuses it treats as temporary, `deployment_attempt_error`, `not_found` and `unknown_status`, the first of which prints `Deployment temporarily failed, a retry will be automatically scheduled...` instead. `Deployment failed, try again later.` comes from the map of statuses the action treats as final, so it has stopped polling deliberately.
### Can I deploy Pages from a tag or a release?
Yes, but the environment has to allow it. The deployment branch and tag rule is matched against the run's `GITHUB_REF`, and a release-triggered run carries a tag ref. Add a tag pattern to the `github-pages` environment, or choose no restriction, and keep in mind that a wildcard will not match a slash.
## References
- [GitHub Actions: deployments and environments, deployment branches and tags](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments#deployment-branches-and-tags)
- [actions/deploy-pages: src/internal/deployment.js, the status maps and the create catch](https://github.com/actions/deploy-pages/blob/main/src/internal/deployment.js)
- [actions/deploy-pages#151: a tag refused by the github-pages environment](https://github.com/actions/deploy-pages/issues/151)
- [actions/deploy-pages#108: a 400 quoting the Pages source branch rule](https://github.com/actions/deploy-pages/issues/108)
- [actions/upload-pages-artifact: the artifact contract, 1 GB supported and 10GB absolute](https://github.com/actions/upload-pages-artifact#artifact-validation)
Top comments (0)