Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 176 of X: Deploying a JavaScript Web App with Webpack & GitHub Actions (GitHub Pages)

01 Feb 2025

The previous instalment was a very high-level overview of GitHub Pages, and an attempt to whet your appetite for learning more. In this instalment we get into the meat of our exploration of GitHub Pages. And we’re going to start by looking back slightly and closing a circle we’ve left open for some time.

Most of the broader Programming by Stealth series has focused on learning client-side web technologies, culminating in building client-side web apps with HTML, CSS & Javascript that utilise popular third-party libraries like jQuery, Bootstrap, and Mustache. Then we broadened our focus a little to look at some tools used to facilitate the development of more complex apps like Git for source control, NodeJS + NPM for package management, ES Lint for flagging poor coding practices, JS Doc and Mermaid for documenting our work, Jest for testing our code, and finally Webpack for bundling our code for distribution.

Along the way, we also took some detours into more coding-adjacent areas like Chezmoi for managing configuration files for our command line tools in Git, the ubiquitous Bash for shell scripting, jq for processing JSON, YAML for simple data markup, and we even dipped our toes in some software engineering with a look at the MVC design pattern.

With all those diversions, the last web app we built as a worked example was actually in instalment 138, just as we’d learned to bundle our code ready for publishing to the web with Webpack. But that’s where we stopped. Figuring out how to actually publish our apps to the internet was left as an exercise for the reader! Well, not anymore!

In this instalment, we’ll learn how to use GitHub Pages to publish our bundled client-side web apps to the internet for free.

What’s more, we’re going to leverage GitHub’s powerful CI/CD (Continuous Integration/Continuous Deployment) tool, GitHub Actions, to automate the process.

We’ve mentioned CI/CD in passing in a few recent instalments, but only in very vague hand-waving terms. In this instalment, we’re finally going to dive in and implement a simple CI/CD pipeline with GitHub Actions to automatically publish the latest version of an example client-side web app to the internet each time updated code is pushed to the main branch using GitHub Pages.

Matching Podcast Episode

You can also Download the MP3

Read an unedited, auto-generated transcript with chapter marks: PBS_2025_02_15

Instalment Resources

What is CI/CD (Continuous Integration/Continuous Deployment)?

At a philosophical level, CI/CD is a model of software development where sporadic big bang releases are spurned in favour of a continuous stream of frequently deployed small iterative changes. The key to adopting this development mindset is automation, and that’s where CI/CD intersects with tools like GitHub. If you want to frequently deploy updated web apps, the process can’t be manual and laborious. It needs to be automated.

To facilitate the CI/CD mindset, code management platforms like GitHub have developed systems for automations that are triggered by actions performed on Git repositories, like commits and pull requests. These kinds of automations are generally referred to as pipelines, with code being fed into one end of the pipe and some kind of desired result appearing at the other end.

In the broader world of software development, CI/CD pipelines are often used to perform tasks like:

  1. Automatically running a test suite against every commit or every pull request
  2. Automatically applying a linter like ESLint to all source files on every commit
  3. Automatically deploying code to a staging server on every commit to a specific branch
  4. Automatically deploying production code on every commit to the default branch

For our purposes in this series we’re going to confine ourselves to that last use-case, automating the deployment of a website each time changes are pushed to a repository’s default branch.

If you’re interested in learning more about CI/CD, the Wikipedia article on the topic is a good starting point.

Introducing GitHub Actions

GitHub implements CI/CD pipelines with its GitHub Actions feature.

Believe it or not, each time a GitHub action is triggered, GitHub creates a new temporary virtual machine (VM) in the cloud, runs the action on it, and then destroys it! This is only possible because of the magic of containerization, which allows for extremely efficient creation and destruction of VMs.

GitHub Actions Terminology

The GitHub docs have a nice overview page for how GitHub Actions work, but the key concepts are quite simple:

  1. Individual CI/CD pipelines are referred to as workflows.
  2. Workflows are comprised of one or more jobs, each of which defines a sequence of steps.
  3. Workflows are triggered by what are referred to as events. These are actions performed on GitHub repositories.
  4. Workflows run on virtual machines referred to as runners, and as of February 2025, runners can be Ubuntu Linux, Windows, or macOS.

Defining Workflows

Workflows are defined in YAML files stored in the special folder .github/workflows. There is an entire section of the GitHub docs dedicated to writing workflows.

A GitHub workflow definition consists of a YAML file defining a single top-level dictionary with keys for the different aspects of a workflow.

The most important top-level workflow keys are:

Workflow Key Type Description
name String (Recommended) The human-friendly name for the Workflow in the GitHub user interface.
on Dictionary (Required) The event or events that should trigger the workflow.
jobs Dictionary (Required) A dictionary defining the jobs the workflow contains. The keys for this dictionary are the alphanumeric IDs of your own choosing, and the values are dictionaries defining the jobs themselves.
permissions   A dictionary defining default permissions for all the jobs in the workflow. The keys are permission names, and the values are permission levels, one of none, read, or write (write includes read). For more details, see the Permissions section of the documentation.
environment Dictionary (Optional) A dictionary defining environment variables that will be made available within all jobs defined in the workflow.

In terms of events, we’re going to keep things very simple in this series and trigger our jobs when commits are pushed to the default branch. So, we’ll always use the following configuration:

on:
	push:
		branches: [ "main" ]

If you want to add additional events to your workflows, there is an entire section of the GitHub documentation dedicated to triggering workflows.

This takes us to the most complex of the top-level keys: jobs. The definition for a job is itself a dictionary, with the following being the most important keys:

Job Key Type Description
name String (Recommended) A human-friendly name for the job.
needs String or array of Strings (Optional) If a job needs one or more jobs to successfully complete before it can start, set this key to a single job ID or an array with multiple job IDs.
runs-on Can be a String, but more complex definitions are possible (Required) The type of virtual machine on which the job should be run.
steps Array of Dictionaries (Required) An array of dictionaries, each defining a step. The steps will be executed in order.
permissions Dictionary (Optional) A dictionary defining permissions for the job. The syntax is the same as for the workflow-level permissions key described in the previous table.
environment Dictionary (Optional) A dictionary defining environment variables that will be made available within all steps defined in the job.

Again, we’re going to keep things simple in this series, so we’re always going to use Linux runners. That means our runs-on definition will always be runs-on: ubuntu-latest.

Finally, we get to the real meat of workflows, the steps that make up a job. Each step is yet another dictionary, with the most important keys being:

Step Key Type Description
name String (Recommended) A human-friendly name for the step.
run String One or more shell commands to execute. To run multiple commands, use a multi-line string. Use this key or uses, not both!
uses String A valid specifier for a pre-defined action to execute with an optional but strongly recommended version number. See the documentation for descriptions of the supported specifiers. You can find a listing of pre-defined actions in the GitHub market place. Use this key or run, not both!
with Dictionary This key provides a mechanism for passing arguments to a predefined action included via the uses key. The details are entirely determined by the included action, so you’ll need to read that action’s documentation to know what keys the action supports. Only use this key with uses!
environment Dictionary (Optional) A dictionary defining environment variables that will be made available within just this step.

Worked Example 1 — Manually Publishing a WebPack-Bundled Web App on GitHub Pages

As we learned in instalments 138 & 139, to publish a web app bundled with Webpack, we need to use NodeJS to import our JavaScript dependencies and then to run the WebPack command. For consistency, we’ll be sticking with the directory structure we used for our worked examples in those instalments:

File/Folder Description
src/* The source code for our web app.
docs/* The built web app.
package.json The NodeJS/NPM configuration for our web app.

Rather than building a fresh example, we’ll reuse the final example web app from instalment 139.

Step 1 — Create a new Public GitHub Repository & Clone it Locally

Log in to GitHub and create a new repository in the usual way. If you want to be able to experiment with GitHub Actions and GitHub Pages for free you’ll need to make it a public repository.

Once you’ve created your example repository, clone it to your personal computer as you usually would, then open a command prompt in that folder.

Step 2 — Add the Web App Code & NodeJS/NPM Configuration

Extract the contents of the Instalment ZIP file and copy all the files and folders into your cloned repo. Rename the file _gitignore to .gitignore and then commit everything as the initial code. You’ll need to do that from the Terminal, as macOS prohibits this action.

Step 3 — Manually Build the App & Commit the Web App

With the initial code committed, let’s manually build the web app so we can see what it is we are trying to automate. Before continuing, make sure you have the latest NodeJS installed (or at least the latest LTS version)!

On the command prompt you have open in your cloned copy of the repo, start by initialising NodeJS with the command:

npm ci

Reminder: this command does a ‘clean install’, downloading the exact versions of each package listed as required in package.json as recorded in package-lock.json.

Now, let’s build our web app with the command:

npm run build

This will build our web app using the source code in /src as specified by the Webpack configuration in webpack.config.js using the build command defined in package.json and publish it to /docs/index.html.

Open /docs/index.html in your browser, and you should see the placeholder has been replaced with the same dummy web app saying “Hello jQuery World!” we built at the end of instalment 139.

Commit the changes and then push them up to GitHub.

Step 3 — Enable GitHub Pages

Log in to the GitHub web interface and open the repo’s GitHub Pages settings (SettingsPages).

Change the Source dropdown to Deploy from a branch, choose main and the folder /docs, then Save.

After you save the settings, Choose Actions from the button bar to see all the actions on your repo. An action will have been created for you named pages build and deployment, and depending on how quickly you navigated, you will see its status as queued or in progress, or it will have a green tick mark indicating it has completed. Click on the workflow’s name to see the details of the last run. The deploy step will show the URL to which your app was published.

Open the URL shown in the workflow output in your browser to verify that you have successfully published your web app to the internet!

Worked Example 2 — Automate Deployment of a Webpack App with GitHub Actions

Now that we can publish our app manually, let’s automate it with GitHub Actions.

To do this, we’ll be making use of a collection of officially verified standard actions from the marketplace and using a run step to execute our two NPM commands (npm ci & npm run build).

It’s important not to use an action from an untrusted author, so I always set the By filter for all my search results to Verified Creators.

Step 1 — Update GitHub Pages Settings to Use a GitHub Actions Workflow

Log back in to the GitHub web interface and open the GitHub Pages settings again. Change the Source dropdown to GitHub Actions and choose to create your own action. This will open a GitHub web editor with a default file as the content.

Enter a sensible filename with a .yaml extension, e.g. deploy-app.yaml.

For the file’s contents, enter the following

name: Build with Webpack and Publish to GitHub Pages

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    permissions:
      contents: read

    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup NodeJS
      uses: actions/setup-node@v4
      with:
        node-version: 22.x

    - name: Build with Webpack
      run: |
        npm ci
        npx webpack

    - name: Upload generated files as artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: docs/

  deploy:
    needs: build

    permissions:
      pages: write
      id-token: write

    environment:
      name: github-pages
      url: $

    runs-on: ubuntu-latest
    steps:
    - name: Deploy to GitHub Pages
      uses: actions/deploy-pages@v4

This YAML code defines a workflow with two jobs, one with the id build and one with the id deploy. The workflow is triggered by pushing to main and both jobs are configured to run on Linux runners (runs-on: unbuntu-latest). Note that the deploy job forces itself to run after the build job with needs: build.

The build job executes steps to:

  1. Check out the code from the commit that triggered the workflow using the standard checkout action.
  2. Set up NodeJS version 22.x in the runner using the standard Node.JS Setup action.
  3. Build the web app by running the appropriate npm commands.
  4. Upload the generated site to the GitHub Pages Servers as a so-called artifact using the standard Upload Pages Artifact action.

With the generated files now uploaded as an artifact, the deploy job executes just one step to:

  1. Publish the uploaded artifact to the internet using the standard Deploy Pages action.

Step 2 — Clean up and Test the Automated Deployment

Now that we are building our GitHub Pages output in a temporary virtual machine, instead of committing it to the repository in the docs folder, we can remove the docs folder. Committing that change now would trigger a re-build, but it would not change the look of the site, so for a more obvious test, let’s make two changes on your local repo:

  1. Delete the docs folder.

  2. Update line 27 of src/index.js so we’ll be able to see that the web app changed:

    $('#world-modifier').text('Automated Webpack');
    

With that done, commit and push all changes. Then watch the action run on the GitHub web interface before refreshing your web browser to verify the website has been updated.

Final Thoughts

We’ve now seen a real CI/CD pipeline in action, and we’ve learned how we can start to publish our client-side web apps to the world. This could prove useful for some coding projects you may end up working on, but there is another reason for covering this concept in this series. This is the mechanism by which the new JavaScript-based version of the XKPasswd secure memorable password generator developed by members of the PBS community will be deployed when it comes out of beta in 2025!

In this instalment, we’ve seen how GitHub Pages can be used to publish a web app rather than a website. For the remainder of our journey into GitHub Pages, we’re going to be looking at how to use GitHub Pages as a content management system for publishing websites.

In this instalment, we replaced GitHub Pages’ default build process with a custom CI/CD pipeline that used NodeJS and Webpack to transform source files into the published website. But, from now on, we’ll be using GitHub Pages’ default publishing pipeline, which uses a different tool — the Jekyll Static Site Generator.

We’ll still be using the Actions page on the GitHub site to monitor our deployments, but the workflows we’ll be watching will be standard ones managed by GitHub’s own developers. It’s important to know, though, that those workflows work in just the same way our custom one did. The standard workflow is also defined in YAML and uses the same combination of raw terminal commands and published pre-created workflow actions we did.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack