# deploy-pages: publishing an Actions artifact to GitHub Pages

> The deploy-pages GitHub Action takes an artifact that a build job has already uploaded and turns it into a live GitHub Pages site, with the deployment authorised through an OIDC token rather than a long lived credential. Its configuration surface is small, its security model is the interesting part, and it only ever publishes to GitHub Pages.

**actions/deploy-pages** — GitHub Action to publish artifacts to GitHub Pages for deployments

- Repository: https://github.com/actions/deploy-pages
- Website: https://pages.github.com
- Stars: 967 · Forks: 145
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/actions-deploy-pages

## The handoff between building and publishing

deploy-pages is the second half of a two step deployment. A build job produces a site, uploads it as an artifact, and this action takes that artifact and publishes it as a GitHub Pages site. The README describes the action as deploying a Pages site previously uploaded as an artifact, for example one uploaded with actions/upload-pages-artifact, and it points to the action.yml file for the full list of inputs.

That split is the design. Building and publishing are separate concerns with separate trust properties, and the artifact is the contract between them. The security considerations make it explicit: the artifact being deployed must have been uploaded in a previous step, either in the same job or in a separate job that does not execute until the upload is complete.

The action is owned by the actions organisation, which is GitHub's own organisation for the first party actions, and the package metadata gives the author as GitHub, the homepage as the pages.github.com site and the licence as MIT. The release history shows v5.0.0 in March 2026 and v5.0.1 in September 2026, and the repository is not archived with the last push on 2026-09-07.

The other thing the README points you to is the starter workflows repository, which has examples for a variety of frameworks. That is the fastest way to see the intended shape of a workflow that uses this action, and it matters because the shape has constraints, in particular the job separation and the permissions, that are easy to get subtly wrong.

One input is described as being for you by default. The token input defaults to the github.token expression, so in the common case you do not configure a credential at all, and the action creates an authenticated client with the token the job already has.

## A dedicated deploy job with two permissions

The README recommends using the action in a dedicated job, and the example workflow shows what that means. The build job comes first and is elided for brevity, with a comment that at a minimum it should upload artifacts using actions/upload-pages-artifact. The deploy job then depends on it:

```yaml
  # Deploy job
  deploy:
    # Add a dependency to the build job
    needs: build

    # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
    permissions:
      pages: write      # to deploy to Pages
      id-token: write   # to verify the deployment originates from an appropriate source
```

The permissions block is the part that matters, and both entries are required. The pages write permission gives the GITHUB_TOKEN the ability to create Pages deployments when calling the GitHub API. The id-token write permission is what allows the job to request an OIDC token at all.

The rest of the job wires the deployment to an environment and takes the resulting URL:

```yaml
    # Deploy to the github-pages environment
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    # Specify runner + deployment step
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action
```

The environment block has two jobs to do. Naming the github-pages environment is what binds the deployment to whatever protection rules that environment carries, and the url key publishes the deployed address as an environment URL, using the page_url output of the step that runs this action.

The comment on the uses line is a small piece of advice worth taking seriously. A major version tag moves as the action is updated, and a specific version tag does not, and in an action that can publish a site, a pinned version is the difference between a controlled upgrade and a change that lands on your website without you choosing it.

## The inputs: timeouts, error counts and the reporting interval

The input table is short, and every entry is a knob for how long the action waits and how it behaves while it waits.

The token input is required and defaults to the github.token expression, so in normal use you never set it. The timeout input defaults to 600000 milliseconds, which the table describes as ten minutes after which the deployment is cancelled. The error_count input defaults to 10, the maximum number of status report errors before the deployment is cancelled. The artifact_name input defaults to github-pages, the name of the artifact to deploy, which has to match what the upload step produced.

The reporting_interval input is the one with a real description behind it. It defaults to 5000 milliseconds and is the initial time between deployment status reports. Successful non-terminal polls use exponential backoff up to thirty seconds, or the configured interval when that is higher, with plus or minus twenty percent jitter, and error backoff is added separately. That is a description of a polling loop that is careful about two things: it does not hammer the API, and it does not synchronise across jobs, since the jitter is there to spread out the requests of many deployments happening at once.

The last input is preview, which defaults to false and asks whether this is deploying a pull request as a GitHub Pages preview site. The parenthetical in the table is the important part: the feature is only in alpha currently and is not available to the public. So the input exists in the action, the code path exists, and the door is closed to ordinary users. Anyone planning a pull request preview flow has to look for another route.

On the output side there is exactly one, page_url, the URL of the deployed Pages site, which is what the environment url key consumes.

## OIDC, and why two permissions are needed

The OIDC section is the clearest explanation of the action's security model, and it is worth following closely because it explains a behaviour that otherwise looks like an unnecessary requirement.

When GitHub Actions invokes a job, the job requests an OIDC token from GitHub's OIDC provider, which responds with a JSON web token. Each token is unique to each workflow job. These tokens are used to form a trust relationship that validates properties of the workflow run against a third party, cloud providers such as AWS or Azure being the example given.

For GitHub Pages the point is branch protection. The OIDC token includes a claim about which branch or ref is executing the workflow, and the token is passed to the Pages deployment API as part of the request payload, where it is decoded internally to validate the claims and to verify whether that workflow is allowed to deploy to Pages. In other words, the deployment does not prove itself with a credential, it proves itself with a statement about where it came from, and the decision about whether that statement is acceptable is made by the environment and branch rules rather than by the workflow file.

That is why both permissions are needed, and the README answers this as a common question. The pages permission relates to the GITHUB_TOKEN and gives it the ability to create Pages deployments when calling the GitHub API. The id-token permission is necessary to request the OIDC JWT token in the first place. One authorises the API call, the other produces the proof of origin. Asking for only the first would leave the deployment unable to demonstrate where it came from, and asking for only the second would leave it unable to make the call.

The practical consequence for a team is that environment protection rules become enforceable. A protected github-pages environment with a deployment branch rule means a workflow on an untrusted branch cannot publish, regardless of what its YAML says.

## The security checklist the action expects you to satisfy

The README lists five considerations, and together they describe the whole deployment contract.

First, the artifact must have been uploaded in a previous step, either in the same job or in a separate job that does not execute until the upload completes, with the expected format documented by actions/upload-pages-artifact. This is the ordering guarantee that stops a deployment from running against a half written artefact.

Second, the job that executes the deployment must have at minimum the pages write and id-token write permissions, which the previous section covered.

Third, the deployment should target the github-pages environment. A different environment name is allowed, but the README says plainly that this is not recommended, and the reason is that the standard environment is where the protection rules live.

Fourth, if the Pages site is using a source branch, the deployment must originate from that source branch, unless the environment is protected, in which case the environment protection rules take precedence over the source branch rule. That precedence matters in practice: if you have both, the environment protection rules take precedence over the source branch rule.

Fifth, if the Pages site is using GitHub Actions as the source, then while it is not required the README highly recommends protecting the environment, and notes that the configuration will be set up by default for you.

There is also an environment variable worth knowing about, GITHUB_PAGES, which is created and set to the string value true so that framework build tools can differentiate their output based on the intended target hosting platform. That is a small courtesy to the build step, and it means a framework that emits different output for Pages can detect that case from an environment variable rather than from a guess.

## The engineering: a checked in bundle, coverage thresholds and a linter plugin

The repository is a JavaScript action, and its layout follows the conventions GitHub first party actions settled on. The src directory holds the source, and the dist directory holds the bundled output that the action actually runs, with the manifest's main field pointing at ./dist/index.js. The build is done with ncc, and the prepare script runs ncc build src/index.js -o dist with a source map and a licences file. The distributable is checked into the repository, and there is a workflow for checking the distributables, so a pull request that changes the source without rebuilding the bundle fails CI.

That convention has a clear reason. A GitHub action is downloaded and run by the runner, and pulling dependencies at run time in a deployment job is both slow and a supply chain risk, so the bundle is shipped as source in the repository. The cost is that every change has two artefacts to keep in step, which is why the check-dist workflow exists.

The rest of the tooling is what you would expect from a project maintained by GitHub itself. ESLint with the github plugin, so the lint rules come from the same organisation, Prettier for formatting with separate check and write scripts, and Jest for the tests with a coverage threshold of seventy percent for lines and statements, collected from the src directory. There is a make-coverage-badge script that regenerates the coverage badge in the repository root, and a .node-version file pinning the Node version used for development.

The runtime dependencies are small and specific: @actions/artifact for reading the uploaded artifact, @actions/core for the action inputs and outputs, @actions/github for the API client, @octokit/request-error for error handling, and http-status-messages to turn status codes into readable text. Everything else is a development dependency, which is the shape you want from an action that runs in other people's workflows.

## Where the action does not reach

The first limit is obvious but worth stating plainly. This action deploys to GitHub Pages and nothing else. It does not publish to a web server, a static host, an object store or a CDN, and it does not run your build. If you want the site somewhere other than Pages, the artifact handoff is still a good pattern, but the publishing step is something else, and the OIDC trust relationship described here is specific to the Pages deployment API.

The second is the pull request preview. The input exists, the documentation describes it, and the table says the feature is alpha and not available to the public. A team that wants every pull request to render its own preview URL is not served by this action today.

The third is the timing envelope. The default timeout of ten minutes and the default ceiling of ten status report errors are generous for a static site, and they are the right defaults. They are also hard ceilings, so a deployment that is slow because of a large artifact or a busy Pages service will be cancelled rather than retried indefinitely. A team with a site big enough to hit that should think about the size of the artifact it is uploading.

The fourth is the platform. The README states the action is primarily designed for use with GitHub.com Actions workflows and Pages deployments, and that certain releases should also be compatible with GitHub Enterprise. That is a careful sentence, and it means Enterprise support depends on the release rather than being guaranteed across the line.

The fifth is the trust chain itself. The action matters precisely because it can publish, and it is protected precisely because publishing requires an OIDC token tied to a branch and an environment. Any workflow that gives the deploy job the right permissions on an unprotected environment has given up the protection, and the README's own advice, to protect the environment, is the thing to do before anything else.

## Conclusion

Adopt deploy-pages when you build a static site in GitHub Actions and want the deployment to be authorised by OIDC against branch and environment rules rather than by a stored token, since that is what makes environment protection meaningful. It is the wrong tool for hosting outside GitHub Pages, for a job that has to build and deploy as one step without an artifact handoff, and for a workflow that needs the preview site feature, which is described as alpha and not available to the public. Before adopting, confirm the artifact is produced by actions/upload-pages-artifact, protect the github-pages environment, and pin the action to a version tag you have read.

## FAQ

### What does the deploy-pages GitHub Action do?

It deploys a Pages site that was previously uploaded as an artifact, for example with actions/upload-pages-artifact, publishing it as a GitHub Pages site. The README recommends running it in a dedicated job that depends on the build job.

### Which permissions does a deploy-pages job need?

At minimum pages: write, which lets the GITHUB_TOKEN create Pages deployments through the API, and id-token: write, which is required to request the OIDC JWT token that proves where the workflow run came from.

### What inputs does the deploy-pages action accept?

A token defaulting to the github.token expression, a timeout in milliseconds defaulting to 600000, an error_count defaulting to 10, a reporting_interval defaulting to 5000, an artifact_name defaulting to github-pages, and a preview flag defaulting to false that is described as an alpha feature not available to the public.

### What is the GITHUB_PAGES environment variable for?

It is created and set to the string value true so that framework build tools can differentiate their output based on the intended target hosting platform.

### Why is an OIDC token used when deploying to GitHub Pages?

The token is minted per job and includes a claim about which branch or ref is running the workflow, and it is passed to the Pages deployment API, which decodes it to verify that the workflow is allowed to deploy. That is what makes environment protection and branch rules enforceable.

## Sources

- [actions/deploy-pages on GitHub](https://github.com/actions/deploy-pages)
- [License: MIT](https://github.com/actions/deploy-pages/blob/main/LICENSE)
- [Project website](https://pages.github.com)
- [README](https://github.com/actions/deploy-pages/blob/main/README.md)
- [Releases](https://github.com/actions/deploy-pages/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/actions-deploy-pages
