Deploy to Github Pages like a pro with Github Actions

rolanddoda profile image Roland Doda Updated on ・5 min read

GitHub Pages is a static site hosting service designed to host your personal, organization, or project pages directly from a GitHub repository.

Note that GitHub Pages is a static site hosting service and doesn’t support server-side code such as, PHP, Ruby, or Python.

To learn more about the different types of GitHub Pages sites, take a look here.

🀨 That’s the theory! Let’s start: 🀨

Copy the name of your project.

The name of my repo is deploy-react-to-gh-pages.

For React specify the homepage in package.json (Docs).

  "homepage": "/name-of-your-project/",

For Vue create a new file in your project root named vue.config.js (Docs) and specify the publicPath (Docs).

module.exports = {
  publicPath: '/name-of-your-project/'

Now commit and push your changes.

Note: if you don't commit your changes, you will lose them in the next command, so make sure you don't skip this step

Open your terminal in your project and run:

Note: For the following commands, if you are using vue, just copy paste them, if you are using react replace dist with build.
npm run build creates a directory with a production build of your app. In vue, that directory in named dist, in react is named build.

  • git checkout --orphan gh-pages Docs
  • npm run build
  • git --work-tree dist add --all Docs (for react: replace dist with build)
  • git --work-tree dist commit -m 'Deploy' (for react: replace dist with build)
  • git push origin HEAD:gh-pages --force
  • rm -r dist (for react: replace dist with build)
  • git checkout -f master
  • git branch -D gh-pages
  • Navigate to your github project and click 'Settings'
  • Scroll to find the section 'Github Pages' , select the gh-pages branch and click 'Save'

πŸš€πŸš€ Congrats πŸš€πŸš€

Your site is ready to be published.
You might have to wait a bit. Generally it takes 2–10 minutes until this process is done.

πŸ€”πŸ€”But... wait. You probably want to re-deploy. There must be a simpler solution instead of re-doing over and over again all the commands above.

Create a node.js script

Now, we are gonna create a node.js script so whenever we want to deploy, we just run one command.

  • Create a scripts folder in your project root.

  • Inside scripts folder, create a gh-pages-deploy.js file.

  • Copy and paste the code below:

  const execa = require("execa");
  const fs = require("fs");

  (async () => {
    try {
      await execa("git", ["checkout", "--orphan", "gh-pages"]);
      await execa("npm", ["run", "build"]);
      // Understand if it's dist or build folder
      const folderName = fs.existsSync("dist") ? "dist" : "build";
      await execa("git", ["--work-tree", folderName, "add", "--all"]);
      await execa("git", ["--work-tree", folderName, "commit", "-m", "gh-pages"]);
      console.log("Pushing to gh-pages...");
      await execa("git", ["push", "origin", "HEAD:gh-pages", "--force"]);
      await execa("rm", ["-r", folderName]);
      await execa("git", ["checkout", "-f", "master"]);
      await execa("git", ["branch", "-D", "gh-pages"]);
      console.log("Successfully deployed");
    } catch (e) {
  • Open package.json and add execa (Docs) to your devDependencies.
    "devDependencies": {
      "execa": "latest"
  • Also add a new script in scripts section:
    "scripts": {
     "gh-pages-deploy": "node scripts/gh-pages-deploy.js"
  • Finally, run npm install.

βœ¨πŸŽ‰ And... that's it! πŸŽ‰βœ¨

You can now deploy as many times as you want by just running npm run gh-pages-deploy.

Me- But, hey... 🀫🀫! Wouldn't be even more exciting if we automate everything ?

Everyone- Do you mean that the app will be deployed itself ? 🀨🀨

Me- πŸ˜‰πŸ˜‰

Everyone- 😲😲

Github Pages- 🀭🀭

Create a github action to automate deployment

Wouldn't be great if on every push on master, the app gets deployed without us doing nothing ?? πŸ§™β€β™‚οΈπŸ§™β€β™‚οΈ

We can achieve that by using... πŸ™ŒπŸ™Œ Github Actions πŸ™ŒπŸ™Œ.

GitHub Actions enables you to create custom software development life cycle (SDLC) workflows directly in your GitHub repository. Docs

Let's start:

  • Create a .github (Don't forget the dot in front) folder in your project root

  • Inside create another folder named workflows

  • Inside workflows create a file named gh-pages-deploy.yml (The name is up to you).

  • Now copy & paste the code below inside that file.

  name: Deploy to github pages
        - master
      name: Deploying to gh-pages
      runs-on: ubuntu-latest
        - name: Setup Node.js for use with actions
          uses: actions/setup-node@v1.1.0
            version:  12.x
        - name: Checkout branch
          uses: actions/checkout@v2

        - name: Clean install dependencies
          run: npm ci

        - name: Run deploy script
          run: |
            git config user.name "Your username" && git config user.email "your email"
            npm run gh-pages-deploy

Important: Make sure to change your username and email:

  • Commit and push your changes

  • Now, navigate to your github project and first click Actions (1) then Deploy to github pages (2) and last click on the action (3).

  • If everything goes well, you will see this:

🌟🌟 Taadaaa!!! You successfully automated deployment!!! 🌟🌟

Now, whenever you merge a PR or push to master this action will run and will deploy your app automatically. πŸ‘πŸ‘

Things to know

πŸ™πŸ™ Thank you for reading. I would be glad to help you if you face any problem, so don't hesitate to email me at rolanddoda2014@gmail.com πŸ™πŸ™

Posted on Mar 15 by:

rolanddoda profile

Roland Doda


Modular and scalable Front End architecture πŸ—ƒοΈ Experienced πŸ’» Javascript Developer Master & Vue.js Lover πŸ’˜ Vue Consultant πŸ‘¨β€πŸ«


markdown guide

Small note for those who use yarn (like me) and not npm. You will get
npm ERR! cipm can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync.
because obviously package-lock.json doesn't exist. You either need to create package-lock by npm install or change a bit last lines of gh-pages-deploy.yml to

      - name: Clean install dependencies
        run: yarn install --frozen-lockfile

      - name: Run deploy script
        run: |
          git config user.name "Your username" && git config user.email "your email"
          yarn run gh-pages-deploy

Excellent post! Thank you! If I may make a recommendation, removing the first layer of indentation in the code block would help a lot I think. It took me a few minutes to figure out why my .yml was being rejected, but it was because everything except the name: line was indented too much.

Still, this got me up and running quickly, with a few minor tweaks to improve things for my workflow! Unfortunately, my user page has to deploy on master so I'm doing that. I might move my personal website to a different repo though, because being able to use gh-pages without polluting master would definitely be preferable.


Hello, Thank you for the great tutorial.
one problem I have is rm -r dist doesn't work! it says that rm is not recognized and I tried replacing it with del so it asked me to delete everything inside dist and I said yes.
and nothing is working :(
Can you please help?


I have to see what and where the issue is. Feel free to send me an email at rolanddoda2014@gmail.com so I can help you to fix the issue


Thank you for posting this great article. Just a few additional notes you might even want to put in the article.
1) I am fairly sure github pages has no way of implementing History mode, so it is important that if you are using the Vue router, it is set like so

export default new Router({
  mode: 'hash',

2) For the script to deploy to github pages from the local machine, I would advise checking if there are uncommitted changes first with something like

    try {
        await execa("git", ["update-index", "--refresh"]); 
        const { stdout } = await execa("git", ["diff-index", "HEAD"]);
        if (stdout) {
            console.log("Please stash or commit changes first!");

3) It might be worth putting the final git commands in a finally block so you always end up back on master. Also note below the rm command is replaced by rimraf - rm is platform specific (and certainly will not work on Windows) and rimraf deals with this for us.

   const rmrf = promisify(rimraf);
    let exitCode = 0;
    try {
        await execa("git", ["checkout", "--orphan", "gh-pages"]);
        await execa("yarn", ["run", "build-modern"]);
        // Understand if it's dist or build folder
        const folderName = existsSync("dist") ? "dist" : "build";
        await execa("git", ["--work-tree", folderName, "add", "--all"]);
        await execa("git", ["--work-tree", folderName, "commit", "-m", "gh-pages"]);
        console.log("Pushing to gh-pages...");
        await execa("git", ["push", "origin", "HEAD:gh-pages", "--force"]);
        await rmrf(folderName, { glob: false })
        console.log("Successfully deployed");
    } catch (e) {
        exitCode = 1;
    } finally {
        await promises.writeFile(configFilePath, originPublicPath, fileOpts);
        await execa("git", ["checkout", "-f", "master"]);
        await execa("git", ["branch", "-D", "gh-pages"]);

Hi Brent and thank you for this comment.

As I said in the end of the article, I want to keep the script very minimal so anyone understands it and can modify it. There is so many things to add to that script and to the whole article itself, but I wanted to go with simplicity.

Here are my answers to your notes:

1) There is a way to get the History mode work with github pages. See -> github.com/Rolanddoda/vue-router-g...
However, going with hash mode, is something that you can easily do, but it's out of the scope of this article on automating deployment to github pages by using github actions. (I would had to talk about vue router history mode and hash mode).

2) It's worth noting that the script will run in github actions environment after you have pushed on master. So no need to check for uncommitted changes.

3) For the same reason as on the second note, no need to add a finally block and also the rm command will run in ubuntu as I've defined in the github actions workflow.

Again, there are so many things to add on that article but I want to keep it simple so anyone can easily extend it. I hope I have helped people on how to take advantage of github actions and github pages.

Thanks for taking the time to comment Brent,
With Respect, Roland.


Thanks for the post, Roland, nice done!
Is exactly what i was looking for, and save me a lot of time for sure :)
Only with the intention of improvement, and for others who may be had the same issue,
there is a little mistake in this line, its miss a "-" in the word worktree:
git --worktree dist add --all Docs (for react: replace dist with build)

should be >>
git --work-tree dist add --all Docs (for react: replace dist with build)
Got me crazy until I realised it

My congrats for the site aswell. Shum Faleminderit!


Ahh yes, instead of --work-tree was --worktree without the dash. Thanks Javier. The typo is already fixed now.


Thanks for the post, Roland,

Especially arrows on the images are helpful as people don't have to scan the image to find out where.


Glad to read that you liked the article and the way it's written Sung. It took me some days (considering I am working full-time) to finish it but I always try to make good articles. So far I have got 3 emails from people thanking me and they are amazed about the automation. Wondering if you have tried it.


I've tried gh pages & actions but only separately.

Should an opportunity comes, then I will try it :)


Links are always nice.

I also use similar? method to deploy unrelated brach to Heroku.


Nice one, thanks for sharing.


Thanks for the comment Arthur


Thank you, just yesterday I searched for this information on Google, but found it here :)


Thanks for the comment Daniil


I got mine working after tweaks here and there. Thank you sooo much!


Thanks for the awesome post, it helps me a lot.


Very nice post mate thank you! Excellent work!!!


Glad to read that you liked it Max.