# ghaction-github-pages deploys a build directory to a Pages branch with one step

> This GitHub Action pushes a build directory to a gh-pages branch and commits it, replacing the manual branch-and-force-push pattern that most Pages deployments still use. It is a TypeScript action bundled with esbuild, it can write a CNAME file, honour keep_history for incremental commits, and it offers a dry_run mode for testing a deployment without pushing anything.

**crazy-max/ghaction-github-pages** — GitHub Action to deploy to GitHub Pages

- Repository: https://github.com/crazy-max/ghaction-github-pages
- Website: https://github.com/marketplace/actions/github-pages
- Stars: 524 · Forks: 60
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/crazy-max-ghaction-github-pages

## The problem is the second branch and the force push

GitHub Pages has two deployment models and the second one needs a branch. When you build a site in CI and want Pages to serve it from a branch rather than through the built-in Actions pipeline, you end up with a git operation in the middle of your build: check out a target branch, copy your output into it, commit, and push.

Doing that by hand in a workflow step is a well known source of pain. The classic version is a set of shell commands with git config for the committer identity, a checkout of the target branch, a copy, an add, a commit and a force push. It breaks in predictable ways: a force push over a branch that other people are watching loses history, a shallow clone leaves you unable to commit, and the committer identity has to be set or the commit fails.

This action is that step, packaged. The README describes it as a GitHub Action to deploy to GitHub Pages, and the marketplace listing is what you would install from.

The workflow example in the README shows the shape of a deployment. A job named publish on ubuntu-latest generates a page into a public directory and then runs the action:

```yaml
      -
        name: Deploy to GitHub Pages
        uses: crazy-max/ghaction-github-pages@v5
        with:
          target_branch: gh-pages
          build_dir: public
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Two inputs carry the configuration. target_branch is the branch where assets will be deployed and defaults to gh-pages, and build_dir is the build directory to deploy, which the input table marks as required. The token comes through the environment as GITHUB_TOKEN from secrets.

The permissions block above the job is worth noting, since it is where the write access is granted. The workflow declares contents: write, which is what lets the action commit to the target branch at all. If your repository's default workflow permissions are read-only, that line is not optional.

The README also notes that a workflow is available for this repository itself and deploys every day to GitHub Pages, so you can see the action running against itself without setting anything up.

## The input table is where the real behaviour lives

The Customizing section documents the inputs as a table of name, type and description, and the table is more informative than the feature list would be.

There are fifteen of them. The structural ones are domain, the Git domain with a default of github.com, and repo, the GitHub repository where assets will be deployed with a default of $GITHUB_REPOSITORY. The domain input is what makes the action usable against a GitHub Enterprise or GitHub AE server, which is not obvious from the name alone.

Then target_branch, defaulting to gh-pages, and build_dir, which is the only required one. absolute_build_dir controls whether build_dir is treated as an absolute path, with a default of false that makes it relative to the working directory. That default is the sane one, since the working directory in an Actions job is the checkout.

follow_symlinks, defaulting to false, decides whether the content of symbolic links is copied. Leaving it off means a symlink is copied as a symlink, and on a Pages site served from a branch a dangling symlink is a broken asset rather than a copied file.

Then the git identity and history controls. keep_history, defaulting to false, creates an incremental commit instead of doing a push force. That single flag is the difference between a history that grows and a history that is rewritten on every deploy, and the default is the rewriting one. allow_empty_commit, defaulting to true, permits a commit even when nothing changed, which keeps a scheduled deploy producing a fresh commit timestamp. The committer and author inputs both take a Display Name <joe@foo.bar> format and default to the GitHub Actions bot user.

commit_message defaults to Deploy to GitHub pages. fqdn writes the given domain name to the CNAME file, which is how you attach a custom domain to a Pages site that is served from a branch. jekyll, defaulting to true, allows Jekyll to build your site, which matters because the Pages server runs Jekyll over branch content by default and it can mangle files that look like templates. dry_run, defaulting to false, means nothing will be pushed, and verbose, defaulting to false, enables verbose output.

The two modes worth noticing together are dry_run and verbose. A dry run with verbose output is the way to find out what the action would do to your branch before it does it.

## Two companion actions show the intended way to compose it

The README documents two other actions from the same author that are meant to be used alongside this one, and together they tell you how the author expects the tool to fit into a workflow.

The first is the Import GPG action, used to sign commits:

```yaml
      -
        name: Import GPG key
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: ${{ secrets.PASSPHRASE }}
          git_user_signingkey: true
```

The signing step sets git_user_signingkey and git_commit_gpgsign to true, and the key and its passphrase come from secrets. That is the mechanism for producing signed commits on the target branch, which matters if you want the deployed history to be verifiable rather than just present.

The second is the GitHub Status action, used to check availability of GitHub Pages before deploying:

```yaml
      -
        name: Check GitHub Pages status
        uses: crazy-max/ghaction-github-status@v3
        with:
          pages_threshold: major_outage
      -
        name: Deploy to GitHub Pages
        uses: crazy-max/ghaction-github-pages@v5
```

The threshold value is major_outage, so the deploy step only proceeds if GitHub Pages is not in a major outage state. That is a genuinely thoughtful addition to a deployment action, because a Pages outage that causes your pipeline to fail can turn a vendor problem into a red build on your side.

All three actions are pinned to major version tags in the examples, the ghaction-github-pages@v5, ghaction-import-gpg@v6 and ghaction-github-status@v3. Pinning to a major tag rather than a commit SHA means you get patch releases automatically and a major release could change behaviour underneath you. That is a normal trade for a marketplace action and worth a deliberate choice rather than an accident.

The overall composition the author is describing is: check the service is up, sign the commit, deploy the branch. Each step is a separate action from the same author, which means they are versioned and maintained in step with each other.

## How it is built and packaged

The repository layout shows a modern TypeScript action rather than a JavaScript one. There is src/ for the source, action.yml for the action manifest, dist/ for the built output that GitHub actually runs, and a dev.Dockerfile plus docker-bake.hcl for the local build environment.

The dist/ directory being committed is the important convention here. GitHub Actions runs the code in the repository at the tag you reference, so an action that is not built before use simply does not work. Committing dist/ means the reference in your workflow resolves to something that runs.

The build script in package.json makes that explicit. The build command runs esbuild over src/main.ts with the bundle and platform=node options and a node24 target, outputs a CommonJS file to dist/index.cjs with a sourcemap and minification, and then runs the license step. The license step uses generate-license-file with the package.json as input and writes dist/licenses.txt, which is the attribution file for the bundled dependencies.

The runtime dependencies are short and tell you what the action actually does. @actions/core and @actions/exec are the official Actions toolkits for logging, inputs and running commands. fs-extra is the file copy layer. addressparser is the one that surprises people, and it is there to parse the committer and author strings of the form Display Name <joe@foo.bar> into the separate name and email fields git needs.

Everything else is a dev dependency. esbuild does the bundling, eslint with the typescript-eslint plugin and prettier does the linting, and the lint script runs with --max-warnings=0, so a warning fails the lint. The package manager is pinned at yarn@4.17.0, and there is a .yarnrc.yml alongside a yarn.lock.

The TypeScript version is 5.9.3 and the Node types are 25.3.3, so the toolchain is current. The default branch is dev, with releases tagged from it, and the CI workflow badge points at ci.yml on the dev branch.

## Where this action is the wrong tool

The first limit is that if your site is served by the built-in Pages pipeline, none of this is necessary. GitHub can build and deploy a Pages site from a workflow directly, and a custom domain configured in the repository settings does not require a gh-pages branch to exist. This action is for the branch-based model, and choosing it when the built-in pipeline would do is more moving parts for the same result.

The second limit is the force push default. keep_history is false by default, so each deploy replaces the branch history. If anyone is watching that branch, or if you rely on its history to see what changed, the default discards it. The fix is a one word change to the input, but it is a change, and it is worth making explicitly rather than accepting the default.

The third is the token scope. The action needs contents write on the target repository, and the GITHUB_TOKEN it receives is scoped to the repository the workflow runs in by default. The repo input lets you point at a different repository, which is exactly the situation where the token scope has to be arranged deliberately, because the default token will not have write access elsewhere.

The fourth is that this only deploys static output. There is no build step inside the action, no Node version selection, no caching, no artifact handling. If your site needs to be compiled, that compilation happens in an earlier step and this action publishes the result. The build_dir input being required reflects that boundary.

The alternative to weigh is the built-in GitHub Pages deployment, which uses permissions and a deploy-pages action rather than a branch. The difference in approach is where the site lives: the built-in pipeline serves from an artifact under GitHub's control, while this action maintains a git branch that anyone with repository access can inspect, and which you can serve yourself or move elsewhere. If you want the site to be a branch, this action saves you the shell script. If you do not, it is extra machinery.

## Maintenance, licence and the release cadence

The maintenance record is current. The last push was on 2026-10-20 and the repository is not archived, so the action is being worked on. The release history shows v5.0.0 on 2026-03-02, v4.2.0 on 2025-03-29 and v4.1.0 on 2024-10-26, which is a major version every year or so with patch releases in between.

A major version bump in an action like this matters more than it would in a library, because the behaviour of the deployment changes. The v5 reference in the README examples is the current major, and pinning to it is what the examples tell you to do.

The licence is MIT, with a LICENSE file at the repository root. That is a permissive grant with no copyleft obligation on anything you build with it, and for an action whose output is a branch of your own repository the licensing question is a minor one. That is a licensing note rather than legal advice.

The repository also carries the project housekeeping that matters for an action. There is an eslint.config.mjs at the root with the flat config format, a .prettierrc.json and a .prettierignore, a .editorconfig, a .gitattributes, and a .dockerignore. The dev.Dockerfile and docker-bake.hcl are the reproducible local build environment, which is how the maintainer verifies that the committed dist/ matches what the source produces.

The README's own structure is a good guide to what this tool is. There is a Usage section with the workflow example, the sign commits example and the availability check example, then a Customizing section with the inputs table and the environment variables table, then Contributing and License. There is no architecture section and no discussion of internals, because the action's contract is its inputs and its behaviour on a branch, and everything else is an implementation detail of a bundled JavaScript file.

The environment variables table lists GITHUB_TOKEN as the one entry, with the same warning about it being a secret rather than a checked-in value, which is the standard advice for any action that pushes to a repository.

## Conclusion

Adopt ghaction-github-pages if you deploy a static site to GitHub Pages from a build step and want the branch commit, the CNAME handling and the git identity handled for you instead of a hand written force push. Do not adopt it if your site is served by the built-in Pages pipeline with a custom domain configured in repository settings, because that path needs no branch commit at all. Verify first that the token you pass has contents write on the target repository, and whether you want keep_history off, since the default force pushes and rewrites the branch history on every deploy.

## FAQ

### How do I deploy a static site to GitHub Pages with ghaction-github-pages?

Add a step that uses crazy-max/ghaction-github-pages@v5 with a target_branch input, defaulting to gh-pages, and a build_dir input pointing at the built output, then pass GITHUB_TOKEN from secrets in the step env. The job also needs permissions set to contents: write.

### What inputs does the ghaction-github-pages action accept?

There are fifteen. The structural ones are domain, repo, target_branch and build_dir, which is the only required one. The behavioural ones are keep_history for incremental commits instead of a force push, allow_empty_commit, committer and author for the git identity, commit_message, fqdn for writing a CNAME file, jekyll, dry_run so nothing is pushed, follow_symlinks, absolute_build_dir and verbose.

### Can I sign the commits that ghaction-github-pages makes?

Yes, by running the crazy-max/ghaction-import-gpg@v6 action before the deploy step with gpg_private_key and passphrase from secrets and git_user_signingkey and git_commit_gpgsign set to true. The README documents this as the way to sign commits on the target branch.

### What is the difference between dry_run and keep_history in ghaction-github-pages?

dry_run means nothing will be pushed, so it is a way to test what the action would do. keep_history creates an incremental commit instead of doing a push force, and it defaults to false, so by default each deploy rewrites the target branch history.

### What licence is ghaction-github-pages released under?

MIT, with a LICENSE file at the repository root. The last push was on 2026-10-20 and the repository is not archived, with releases v5.0.0 from 2026-03-02, v4.2.0 and v4.1.0 listed. The action is written in TypeScript and bundled with esbuild into dist/index.cjs.

## Sources

- [crazy-max/ghaction-github-pages on GitHub](https://github.com/crazy-max/ghaction-github-pages)
- [License: MIT](https://github.com/crazy-max/ghaction-github-pages/blob/dev/LICENSE)
- [Project website](https://github.com/marketplace/actions/github-pages)
- [README](https://github.com/crazy-max/ghaction-github-pages/blob/dev/README.md)
- [Releases](https://github.com/crazy-max/ghaction-github-pages/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/crazy-max-ghaction-github-pages
