Static Site Deployment via GitHub Actions¶
This document covers every method available to deploy a static site (MkDocs, Hugo, Docusaurus, plain HTML, etc.) to GitHub Pages using GitHub Actions. These are not MkDocs-specific — they work for any static site generator.
Three deployment methods exist. The right choice depends on whether you need PR previews, multi-job pipelines, or simplicity.
Method Comparison at a Glance¶
| Feature | Method 1: mkdocs gh-deploy | Method 2: OIDC (deploy-pages) | Method 3: Branch (peaceiris) |
|---|---|---|---|
| Complexity | Minimal | Moderate | Moderate |
gh-pages branch created | Yes | No | Yes |
| Separate build + deploy jobs | No (single step) | Yes | Yes |
| PR preview support | No | No | Yes (via rossjrw) |
| Multi-job artifact sharing | No | No (internal pipe) | Yes (standard artifact) |
| Deployment history visible | Yes (branch commits) | No (internal) | Yes (branch commits) |
| GitHub Pages source setting | Deploy from branch | GitHub Actions | Deploy from branch |
| Configure Pages before first run | No | Yes — mandatory | No — configure after first run |
| Best for | Quick personal projects | Clean modern pipelines | Production + PR previews |
Method 1 — mkdocs gh-deploy (Built-in, Simplest)¶
How it works¶
MkDocs has a built-in command that builds and deploys in one step. It pushes the generated /site directory directly to a gh-pages branch. No separate deploy action is needed.
push to main → single job → mkdocs gh-deploy --force → gh-pages branch updated
When to use¶
- Personal or hobby projects
- No QA steps needed (no HTML validation, no link checking)
- You want the simplest possible workflow (under 20 lines of YAML)
- MkDocs-only projects (this command is not available for Hugo/Docusaurus)
When NOT to use¶
- You need to run tests before deploying
- You want PR previews
- You need to share the build artifact between jobs
- Non-MkDocs static site generators
GitHub Pages setting¶
Settings → Pages → Source: Deploy from a branch → gh-pages / / (root)
Full workflow example¶
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # required for git-revision-date plugin
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build and deploy
run: mkdocs gh-deploy --force
# This single command: builds the site → pushes to gh-pages branch
Method 2 — OIDC / actions/deploy-pages (Modern GitHub-Native)¶
How it works¶
This is GitHub's official modern approach. The build job uploads the /site directory as a special internal artifact using upload-pages-artifact. A separate deploy job then uses actions/deploy-pages to push it to GitHub Pages through GitHub's internal OIDC pipeline — no gh-pages branch is created.
build job → upload-pages-artifact → [GitHub internal pipeline]
deploy job → actions/deploy-pages → site goes live
The artifact handoff between jobs is internal to GitHub Pages infrastructure — only actions/deploy-pages can consume an artifact uploaded by upload-pages-artifact. No other job can read it.
When to use¶
- You want a clean repo with no extra branches
- You do not need PR previews
- You prefer GitHub's managed infrastructure
- Works with any static site generator (not MkDocs-only)
When NOT to use¶
- You need PR preview deployments (the
gh-pagesbranch is required for that) - You need multiple jobs to consume the same build artifact
- You want deployment history visible as branch commits
Mandatory pre-requisite — configure GitHub Pages BEFORE running the workflow¶
This step is required before you run this workflow for the first time. If you skip it, the workflow will fail with a Pages deployment error.
Go to your repository Settings → Pages and set the source to GitHub Actions:
Repository → Settings → Pages → Build and deployment
Source: GitHub Actions ← select this, NOT "Deploy from a branch"
This tells GitHub to expect deployments from the OIDC pipeline (actions/deploy-pages). Without this setting, GitHub Pages does not know where to receive the deployment and rejects it.
Order of operations for Method 2:
1. Go to Settings → Pages → set Source to "GitHub Actions" ← do this FIRST
2. Push your code to main (or run workflow_dispatch)
3. Workflow runs: build job → deploy job
4. Site goes live at https://<username>.github.io/<repo>/
GitHub Pages setting¶
Settings → Pages → Source: GitHub Actions
Required permissions¶
permissions:
pages: write
id-token: write
Full workflow example¶
name: Deploy Docs
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: pages
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Build Site
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build site
run: mkdocs build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./site
# Note: this is a SPECIAL artifact — only actions/deploy-pages can consume it
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4
Method 3 — peaceiris/actions-gh-pages + PR Previews (Production-Grade)¶
How it works¶
The build job uploads /site as a standard workflow artifact (not the special Pages artifact). Two downstream jobs can both consume it:
- Preview job — runs only on PRs, deploys a live temporary URL at
/previews/pr-<number>/usingrossjrw/pr-preview-action, and posts a comment on the PR with the preview link. Automatically cleaned up when the PR is merged or closed. - Deploy job — runs only on pushes to
main, deploys the production site by pushing to thegh-pagesbranch viapeaceiris/actions-gh-pages.
build job ──(artifact)──┬──► preview job (PR only) → /previews/pr-5/
└──► deploy job (main only) → production site
Because both jobs share the same general-purpose artifact, the site is built exactly once regardless of which downstream job runs.
Why peaceiris is required for PR previews¶
rossjrw/pr-preview-action works by writing preview folders into the gh-pages branch alongside the main site. It needs a real, writable branch to do this. The OIDC method has no such branch — GitHub manages it internally and it cannot be written to by other actions. Therefore, PR previews require the peaceiris branch-based method.
When to use¶
- Production documentation sites with external contributors
- You want reviewers (including yourself) to see a live preview before merging a PR
- Multi-stage CI/CD with build, QA, preview, and deploy as separate jobs
- Works with any static site generator
When NOT to use¶
- Tiny personal projects with no contributors (Method 1 is simpler)
- You specifically want no extra branches in your repo
GitHub Pages setting — configure AFTER the first workflow run¶
Do NOT configure GitHub Pages before the first run. The
gh-pagesbranch does not exist yet. You cannot point GitHub Pages at a branch that has not been created. Run the workflow first.
Order of operations for Method 3:
1. Push your code to main (or run workflow_dispatch) ← do this FIRST
2. The deploy job runs and creates the gh-pages branch automatically
3. Go to Settings → Pages → set Source to "Deploy from a branch"
Branch: gh-pages / Folder: / (root) ← do this AFTER
4. GitHub Pages publishes the site
5. All future pushes to main redeploy automatically — no further settings changes needed
This is the opposite order from Method 2. You run first, configure second — because there is nothing to configure until the gh-pages branch exists.
Required permissions¶
# deploy job
permissions:
contents: write # push to gh-pages branch
# preview job
permissions:
contents: write # push preview folder to gh-pages branch
pull-requests: write # post preview URL comment on the PR
PR preview flow (from a contributor's perspective)¶
1. Contributor forks your repo
2. Makes changes to a .md file
3. Opens a Pull Request to your repo
4. GitHub Actions triggers automatically:
- Build job runs (mkdocs build, HTML validation, link check)
- Preview job deploys to: https://your-site.com/previews/pr-5/
- A bot comments on the PR with the live preview URL
5. You click the link, review the rendered docs
6. If happy → merge the PR
7. Deploy job runs → production site updated
8. Preview URL at /previews/pr-5/ is automatically deleted
Full workflow example¶
name: Docs — Build & Deploy
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# ──────────────────────────────────────────────
# JOB 1 — Build + QA (runs on push AND PR)
# ──────────────────────────────────────────────
build:
name: Build Site
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build site
run: mkdocs build
- name: Upload site artifact
uses: actions/upload-artifact@v4
with:
name: site
path: ./site
retention-days: 1
# Standard artifact — can be consumed by ANY downstream job
# ──────────────────────────────────────────────
# JOB 2 — PR Preview (runs ONLY on pull_request)
# Live URL: https://your-site.com/previews/pr-<number>/
# Bot posts the URL as a comment on the PR.
# Deleted automatically when PR is closed.
# ──────────────────────────────────────────────
preview:
name: Deploy PR Preview
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download built site
uses: actions/download-artifact@v4
with:
name: site
path: ./site
- name: Deploy PR preview
uses: rossjrw/pr-preview-action@v1
with:
source-dir: ./site
umbrella-dir: previews
action: auto
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ──────────────────────────────────────────────
# JOB 3 — Production Deploy (runs ONLY on push to main)
# ──────────────────────────────────────────────
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
if: github.event_name != 'pull_request'
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download built site
uses: actions/download-artifact@v4
with:
name: site
path: ./site
- name: Write CNAME (if using a custom domain)
run: echo "your-site.com" > ./site/CNAME
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
publish_branch: gh-pages
force_orphan: true
# force_orphan: keeps gh-pages branch as a single flat commit
# (no history bloat from thousands of doc build commits)
Key Concept — upload-pages-artifact vs upload-artifact¶
This is the most common point of confusion when switching between methods.
upload-pages-artifact (Method 2) | upload-artifact (Method 3) | |
|---|---|---|
| Consumer | Only actions/deploy-pages | Any job via download-artifact |
| Visibility | Internal GitHub pipeline | Visible in workflow run artifacts |
| Multiple consumers | No — single deploy job only | Yes — preview job AND deploy job can both use it |
| Use case | OIDC single-deploy pipelines | Multi-job pipelines, PR previews |
In Method 3, the build job uploads once, and both the preview job and deploy job download the same artifact. This ensures the exact same build is previewed and then deployed — no risk of drift between what you reviewed and what went live.
Are These Methods MkDocs-Only?¶
No. The deployment methods (Methods 2 and 3) are completely generic and work with any static site generator:
| Generator | Build command | Output dir |
|---|---|---|
| MkDocs | mkdocs build | ./site |
| Hugo | hugo | ./public |
| Docusaurus | npm run build | ./build |
| Jekyll | bundle exec jekyll build | ./_site |
| Plain HTML | (no build step) | ./ |
The only thing that changes between generators is the build command and the output directory you pass to the upload/deploy action. Everything else in the workflow is identical.
Method 1 (mkdocs gh-deploy) is MkDocs-only because it is a built-in MkDocs CLI command.
Decision Guide¶
Do you need PR previews?
├── Yes → Method 3 (peaceiris + rossjrw)
└── No
├── Do you need QA steps (HTML validation, link checks)?
│ ├── Yes → Method 2 (OIDC) or Method 3 (peaceiris)
│ └── No
│ └── Is it a MkDocs project?
│ ├── Yes → Method 1 (mkdocs gh-deploy) — simplest
│ └── No → Method 2 (OIDC) — clean and modern
└── Do you want a gh-pages branch for transparency?
├── Yes → Method 3 (peaceiris)
└── No → Method 2 (OIDC)