Skip to content

Deploying a Static Site to Github Pages


GitHub Pages is the easiest place on the internet to put a static file, and after the last chapter’s talk of gates and reviewers, that’s exactly the point of starting here: no protection rules, no cloud account, no secrets to generate. Just a workflow, an environment named github-pages, and a page that’s live in under a minute.

The Site You’re Deploying

You’ll deploy the same tiny page in this chapter and the next two, to three different real destinations, so you can compare how the deployment mechanics differ while the app itself stays trivial. Create a new repository (or a fresh branch in an existing one) with this single file:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>devstuffs CI/CD practice</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #0B0E14; color: #E4E2DC; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
    .card { text-align: center; }
    code { color: #E8A33D; }
  </style>
</head>
<body>
  <div class="card">
    <h1>Hello from devstuffs 👋</h1>
    <p>This page was deployed by GitHub Actions.</p>
    <p><code id="target">deployment target: unknown</code></p>
  </div>
</body>
</html>

Notice the deployment target: unknown line. You’ll never edit that by hand — the workflow itself stamps in the real destination right before it deploys, with a one-line sed replace. It’s a small touch, but it means you can open the live URL and immediately confirm which pipeline actually put it there.

Turning On Pages for This Repo

Before the workflow can deploy anything, GitHub Pages needs to be told it’s expecting deployments from Actions, not from a branch:

  1. Go to the repo’s Settings → Pages.
  2. Under Build and deployment → Source, choose GitHub Actions (not “Deploy from a branch” — that’s the older, simpler mode this course is skipping, since it can’t participate in the Environment/Deployments machinery from the last chapter).

That’s the only manual setup step. No bucket, no DNS, nothing to provision.

The Workflow

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Stamp deployment target
        run: |
          sed -i 's/deployment target: unknown/deployment target: GitHub Pages/' index.html

      - name: Configure Pages
        uses: actions/configure-pages@v5

      - name: Upload site as Pages artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: .

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

A few things worth calling out, since this is a slightly different shape than workflows you’ve written before:

  • Two jobs, not one. build packages the site into a Pages-formatted artifact; deploy publishes that artifact. They’re split because the deploy job is the one that carries the environment: block — separating them means the Deployments tab tracks exactly the step that matters, not your whole build process.
  • permissions is not optional here. pages: write lets the job publish, and id-token: write lets it prove to GitHub Pages that the deployment genuinely came from this workflow run. Leave either out and deploy-pages fails outright.
  • needs: build is what makes deploy wait for the artifact to actually exist before trying to publish it — plain job ordering, nothing new.
  • The url on the environment: block reads page_url, an output the deploy-pages action itself produces once it knows the live address — you don’t hardcode your Pages URL anywhere.

Note

actions/configure-pages doesn’t touch any files. Its whole job is to inspect your repo’s Pages settings and set a few outputs (like the base URL) that other steps in the same job can use — here, nothing downstream needs those outputs directly, but the action also validates that Pages-via-Actions is actually enabled before you waste time uploading an artifact that can’t be deployed.

Watching It Deploy

Push this to main and open the Actions tab — you’ll see build then deploy run in sequence. Once deploy finishes, the repo’s Settings → Pages page shows the live URL directly, and the Environments sidebar (from the last chapter) now has a github-pages entry with a “View deployment” link. Open it — the page should read deployment target: GitHub Pages, confirming the sed step actually ran before upload rather than you eyeballing stale content.

Cleanup

GitHub Pages costs nothing to leave running, so there’s no billing reason to tear it down. If you’re using a disposable practice repo and want to stop hosting the page:

  1. Settings → Pages → Build and deployment → Source, switch back to “Deploy from a branch” (or leave it — either way, deleting the repository itself removes the Pages site).
  2. If you want to remove just the deployment history without touching the repo, the Environments page lets you delete the github-pages environment entirely, which also removes its deployment log.
Last updated on