Skip to content

Deploy to GitHub Pages with GitHub Actions

Lesson 7 of 7Beginner → Intermediate4 min readGitHub Actions & CI/CD · Continuous DeliveryVerified: actions/configure-pages v6, actions/upload-pages-artifact v5, actions/deploy-pages v5, August 2026

GitHub Pages is the gentlest deployment in this cluster — no cloud account, no credentials, no infrastructure. It is also a genuinely good illustration of a pattern the harder pages depend on: splitting a deployment into an untrusted build and a privileged publish.

The complete workflow is at examples/github-actions/cd-github-pages/deploy.yml, validated by npm run check:workflows.

Older guidance has a workflow push the built site to a gh-pages branch, which Pages then serves. That still works, and it has drawbacks: the deployment needs contents: write — the ability to rewrite the repository — the build output pollutes git history, and there is no deployment record.

The current mechanism uploads an artifact and calls a deployment API. It needs no write access to the repository at all.

- id: deployment
uses: actions/deploy-pages@v5

What it doesPublishes a previously uploaded Pages artifact through the Pages deployment API.

Why we run itThe deployment needs only `pages: write` and `id-token: write`, not the ability to write to the repository — so a compromised deploy step cannot rewrite your code or history.

Expected resultA deployment recorded against the `github-pages` environment, with the live URL as a job output.

Enable it once in Settings → Pages → Build and deployment → Source → GitHub Actions. Until that is set, the workflow runs and the deploy step fails with an error about Pages not being configured for Actions — a confusing message that has nothing to do with the workflow file.

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: ./dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- id: deployment
uses: actions/deploy-pages@v5

The split is the interesting part, and it is not stylistic. The build job runs your dependencies — hundreds of packages and their install scripts — and holds no Pages permissions. The deploy job holds the publishing permissions and runs exactly one first-party action, with no project code and no dependencies.

This is the same separation the harder pipelines in this cluster use for the same reason: the step that runs untrusted-ish code and the step that holds the credential should not be the same step. See least-privilege permissions.

Note that permissions is declared on the deploy job, not at workflow level. Declaring it at the top would hand pages: write to the build job too, which defeats the point.

id-token: write appears here for a reason worth knowing: the Pages deployment API verifies the deployment’s provenance using an OIDC token, so the platform can confirm which workflow and repository produced it. It is the same mechanism as cloud OIDC with GitHub on both sides.

concurrency:
group: pages
cancel-in-progress: false

Pages allows one deployment at a time; a second concurrent one is rejected. cancel-in-progress: false queues instead of cancelling, so two quick merges both publish, in order.

Some published examples use cancel-in-progress: true here. That is defensible for a docs site where only the newest content matters, and it means a merge can cancel a deployment that was mid-publish. For a site whose content changes meaningfully between commits, queueing is the safer default.

A project site is served from https://OWNER.github.io/REPO/, not from the domain root. A build that emits absolute paths like /assets/app.css produces a site whose CSS 404s — the browser requests https://OWNER.github.io/assets/app.css.

Every static site generator has a setting for this: base in Astro and Vite, basePath in Next.js, baseurl in Jekyll, --base-href in Angular. actions/configure-pages can supply it:

- uses: actions/configure-pages@v6
id: pages
- run: npm run build
env:
BASE_PATH: ${{ steps.pages.outputs.base_path }}

A user or organisation site — the repository named OWNER.github.io — is served from the root and needs no base path. Moving a project site to a custom domain also moves it to the root, which is why “it broke when we added the domain” is a common report: the base path that was correct is now wrong.

There is no rollback button. Pages serves the most recent successful deployment, so recovering means publishing again from a known-good commit:

on:
workflow_dispatch:
inputs:
ref:
description: "Commit or tag to publish"
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
{/* …build and upload exactly as above… */}

This rebuilds rather than restoring a stored artifact, so it depends on the old commit still building — which it may not if a dependency has since disappeared or a build tool has moved on. For a site where that matters, keep the built artifact from each release with a long retention and publish from it directly.

  1. Set Settings → Pages → Source to GitHub Actions.

  2. Copy examples/github-actions/cd-github-pages/deploy.yml into a repository with a static build and push to main.

  3. Confirm two jobs ran, and that the deploy job’s summary shows the live URL.

  4. Check the build job’s permissions in the run log. Confirm it does not have pages: write — only the deploy job does.

  5. Break the base path deliberately: build without the base setting and confirm the CSS 404s on a project site. Then fix it with configure-pages.

  6. Add a workflow_dispatch publish-from-ref workflow and use it to republish an earlier commit.

You have now seen all seven delivery targets in this cluster.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Want production-ready workflow templates? The Professional Toolkit has five, with permissions set correctly.