skip to content

In GitHub Pages, what are the two publishing sources for a site, and how does each one get files onto the live site?

level: juniorimportance: should knowfreq 62%

answer

  1. one setting, two answers
  2. a branch and a folder, or an artifact
  3. Jekyll runs on GitHub's side only for one of them
  4. upload-pages-artifact then deploy-pages
  5. pages: write plus id-token: write

basics

~20 s

GitHub Pages publishes either from a branch — it serves a chosen branch and folder, running Jekyll over it first — or from GitHub Actions, where a workflow uploads a built directory as a Pages artifact and a deploy step publishes that artifact.

solid answer

~40 s

A Pages site has exactly one publishing source, chosen in the repository's Pages settings. **Deploy from a branch** is the classic mode: you pick a branch (often `main` or `gh-pages`) and a folder (`/` or `/docs`), and whatever you commit there becomes the site — GitHub runs it through Jekyll on its side unless a `.nojekyll` file is present. **GitHub Actions** is the other mode: your own workflow builds the site however you like, `actions/upload-pages-artifact` packages the output directory, and `actions/deploy-pages` publishes it. The Actions route is what you use for any non-Jekyll generator — Astro, Next.js export, Vite, MkDocs, Hugo — because the build runs in your workflow with your toolchain and versions, and nothing is built on GitHub's side. It needs `pages: write` and `id-token: write` permissions and the `github-pages` environment.

code

yaml · 37 lines
yaml
name: Deploy site to GitHub Pages
on:
  push:
    branches: [main]

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

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./dist

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

go deeper

for a junior

Be able to say where the site's files come from: a branch and folder you commit to, or an artifact a workflow uploads. Know that the choice is made in Settings, Pages, and that only one source is active.

for a middle

Explain the mechanics of the artifact route — upload-pages-artifact packages a directory, deploy-pages publishes it, and the job needs pages: write plus id-token: write on the github-pages environment. Contrast it with GitHub building Jekyll for you.

for a senior

Argue the choice on operational grounds: pinned toolchain, tests gating the deploy, and a concurrency group so overlapping pushes cannot publish out of order. Note that with a branch source, whatever lands on the branch is instantly live with no gate.

for a principal

Own the standard across many repositories: one reusable deploy workflow, environment protection on github-pages, and a policy on when a docs site is worth a build pipeline at all versus plain committed HTML.

## What "publishing source" means A GitHub Pages site is a directory of static files that GitHub serves at `https://<owner>.github.io/<repo>/` (or at a custom domain). The **publishing source** is the answer to one question: where does GitHub get that directory? A repository has exactly one publishing source at a time, set under **Settings → Pages → Build and deployment → Source**. Switching it is a settings change, not a code change. ## Source 1: deploy from a branch This is the original model and still the simplest. You choose a branch and one of two folders — the repository root `/` or `/docs`. Everything committed there is the site. Push to that branch and, within a minute or so, the live site changes. The important subtlety is that this path is not a plain file copy. GitHub runs the content through **Jekyll**, its built-in static site generator, on its own infrastructure. If the folder contains Jekyll sources (`_config.yml`, `_posts/`, Markdown with front matter), Jekyll renders them into HTML. If it contains an already-built site, Jekyll still processes it — which is where the classic breakage comes from, because Jekyll drops paths beginning with an underscore. Committing an empty `.nojekyll` file at the root of the publishing source turns that processing off and makes GitHub serve the files verbatim. A common variant is a dedicated `gh-pages` branch holding only build output, produced by a CI job or a tool that force-pushes the built directory. That keeps generated files out of `main`, at the cost of a branch whose history is machine-written. ## Source 2: GitHub Actions Here no branch is "the site". A workflow builds the site with whatever toolchain you want and hands the result to Pages through two first-party actions: ```yaml permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm run build - uses: actions/upload-pages-artifact@v3 with: path: ./dist deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages steps: - uses: actions/deploy-pages@v4 ``` `actions/upload-pages-artifact` packages the given directory into the specially-named artifact Pages expects; `actions/deploy-pages` creates the deployment from it. The permissions matter: `pages: write` lets the job create a Pages deployment, and `id-token: write` is required because the deploy action authenticates to the Pages service with an OIDC token rather than a stored credential. The deployment is attached to the `github-pages` environment, which is why environment protection rules and the deployment history in the repository UI apply to Pages too. ## Which one to pick Use **deploy from a branch** when the site genuinely is Jekyll, or when it is a handful of hand-written HTML files and you want zero moving parts. Use **GitHub Actions** for everything else. The Actions route wins on three practical grounds: - **Toolchain control.** Your Node, Ruby, Python or Go version is pinned in the workflow, not decided by GitHub's Jekyll environment, which supports only a fixed set of plugins. - **Any generator.** Hugo, Astro, MkDocs, Docusaurus, a Next.js static export — all produce a directory, and a directory is all `upload-pages-artifact` wants. - **Real CI before publish.** Link-checking, tests and linting run as ordinary steps, and a failing build simply never reaches the deploy job. With a branch source, whatever landed on the branch is live. The cost is that you now own a workflow: broken build, no new deploy, and the failure shows up in Actions rather than in the Pages panel. ## Things people get wrong The two sources are exclusive — you cannot have a branch source *and* an Actions deploy racing each other; setting one replaces the other. Pushing to `gh-pages` does nothing at all if the source is set to Actions. Conversely, a Pages deploy workflow that runs while the source is still "deploy from a branch" will fail or be ignored. And in both modes the result is the same kind of thing: a static directory. Choosing the Actions source does not give you server-side execution; it only changes who builds the files.

  • Why does the deploy job need id-token: write when it is not talking to a cloud provider?
    `actions/deploy-pages` authenticates to the Pages deployment service with a short-lived OIDC token minted for the job, rather than with a stored credential. Requesting an ID token requires `id-token: write` on the job, so omitting it makes the deploy step fail with a permissions error even though `pages: write` is present.
  • What happens if two pushes to main trigger the deploy workflow at the same time?
    Pages deployments are serialized per site, so overlapping runs can leave the newer commit published before the older one finishes and then get overwritten. That is why the generated workflow adds a `concurrency` group for pages with `cancel-in-progress: false`: queued runs wait rather than interleave, and the last completed build wins deterministically.
  • Can you keep site sources on main but publish build output without an Actions workflow?
    Yes — set the source to a branch such as `gh-pages` and have some external process force-push the built directory there. It works, but the branch history becomes machine-written noise and nothing verifies the output before it goes live. The Actions source gets you the same separation with a build you can gate on tests.

saying these in an interview costs you the question

  • Thinking a repo can use a branch source and an Actions deploy at once
  • Assuming Pages always builds your site for you
  • Believing gh-pages is a magic branch name Pages always watches
  • Forgetting id-token: write and blaming the deploy action
  • Thinking the Actions source enables server-side rendering at request time

context