Skip to content
SRE & DevOpsDeep Dive Published Updated 6 min readViews unavailable

GitHub Pages with Custom Actions: Build Artifacts, Least Privilege, and Reliable Releases

Deploy a static site to GitHub Pages through an explicit build artifact and deployment job, with correct permissions, environments, and failure diagnostics.

GitHub Pages is often introduced as “publish a branch,” but a custom GitHub Actions workflow creates a more explicit release pipeline: source is checked out, a site generator produces a static directory, that output is packaged as the Pages artifact, and a deployment job publishes that artifact to the github-pages environment. This cleanly separates build permissions from deploy permissions and makes the deployed payload visible in the workflow run.

Pages has two publishing models. Branch-based publishing selects a branch and either its root or /docs; custom workflow publishing builds through GitHub Actions and is appropriate when the site requires a toolchain, tests, a non-default static generator, or a controlled artifact handoff. Do not configure both as competing authorities. Enable the Actions publishing source in repository settings when using a custom workflow.

Treat the Pages artifact as the release object

Build from a known commit, run validation before packaging, and upload only the generated site directory, not the repository checkout or temporary files. GitHub’s Pages artifact format is a gzip-compressed archive containing one tar archive; the tar must be smaller than 10 GB and contain no symbolic or hard links. Those limits can matter when a site generator accidentally copies a dependency tree or local cache into its output.

The deployment job must depend on the build job that created the artifact. If it is detached from that dependency, deployment may wait for an artifact that does not exist. The deployment action requires pages: write and id-token: write; those permissions belong only on the deployment job, not indiscriminately at workflow scope. The build job normally needs only read access to repository contents. Environment protection rules can then restrict who or what is allowed to publish.

Here is the structure; replace the illustrative build command and output directory with the generator’s actual contract. For high-assurance repositories, pin every third-party action to a reviewed full commit SHA and let a controlled update process advance those pins. The version tags below mirror GitHub’s documented usage examples and are not immutable references.

name: Build and publish Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: github-pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/configure-pages@v5
      - name: Build the site
        run: npm ci && npm run build
      - uses: actions/upload-pages-artifact@v4
        with:
          path: ./dist

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

The example’s npm ci && npm run build is only suitable for an npm project with a committed lockfile and those scripts. Other generators need their official build action or pinned toolchain. Before uploading, validate the final output rather than assuming a successful compiler command proves the site is publishable: check required files, links that must resolve, base-path behavior, and the configured custom domain.

Keep permissions and deployment authority narrow

Workflow-level contents: read is a reasonable baseline for a build that checks out source. Do not add pages: write or id-token: write to the build job unless it has a specific need. The deploy job does not need the source checkout when it publishes the artifact already passed through needs: build.

Use the github-pages environment as an auditable release target and apply branch restrictions or required reviewers where the plan and repository settings support them. A reviewer approval is only meaningful if untrusted code cannot alter the workflow or artifact after review. Avoid triggering deployment from untrusted pull-request code; use pull-request events for build/test and a trusted branch push or explicitly approved dispatch for deployment. If a reusable workflow or third-party build action runs with write permissions, it becomes part of the release boundary and must be reviewed accordingly.

For forks and pull requests, separate preview validation from publication. Forked workflow runs have constrained secrets and permissions, but do not treat those defaults as a reason to pass untrusted build outputs into a privileged deployment job. Build a fresh artifact from the trusted commit that is being released.

Understand Pages routing before diagnosing a “successful” deployment

The deployment output URL is the canonical location to verify. Project sites normally live beneath a repository path, while user/organization sites and custom domains have different path behavior. A static generator that assumes / can produce assets with incorrect URLs under a project-site base path; verify the built HTML and asset requests at the actual published URL. A successful deployment action only confirms that Pages accepted the artifact, not that every route, asset, redirect, or canonical URL behaves correctly.

Custom domains require both repository configuration and DNS to agree. Verify the domain ownership and HTTPS status in Pages settings, avoid conflicting apex/www records, and keep the CNAME or platform domain setting consistent with the generator’s canonical URL. When a deploy succeeds but the site shows an older build, compare the run’s source commit, artifact creation, environment deployment, and public cache behavior instead of rerunning an arbitrary older workflow.

Diagnose deployment failures by pipeline boundary

If configure-pages fails, confirm custom workflow publishing is enabled and that the repository and organization plan support the requested visibility. If artifact upload fails, inspect the generated path, symlinks, tar layout, and artifact size. If deployment cannot find an artifact, verify needs: build, the same workflow run, and the artifact upload step’s successful completion. If the deploy job is unauthorized, inspect its pages and id-token permissions and the environment’s rules. If the deployment action succeeds but the live site is wrong, inspect the generated output and Pages URL/path configuration before changing permissions.

Release acceptance checks

  • Pages is configured to use GitHub Actions rather than a conflicting branch source.
  • The artifact contains only the intended static output, with no secrets, temporary build data, symlinks, or hard links.
  • The artifact meets GitHub’s archive and size requirements.
  • Build and deploy are distinct jobs, and deploy has an explicit dependency on build.
  • Only the deploy job receives Pages write and OIDC token permissions.
  • The protected deployment environment targets the intended branch and approval policy.
  • Workflow actions, generator versions, lockfiles, and build commands are reviewed and updated through controlled changes.
  • A post-deployment smoke test follows the returned Pages URL and validates representative routes, assets, metadata, and custom-domain HTTPS.

This model does not replace tests of the rendered site or access controls on the repository. It does make the release artifact, permission grant, and deployment event explicit enough to audit and troubleshoot independently.

Related:

Sources:


Comments