PBS 176 of X: Deploying a JavaScript Web App with Webpack & GitHub Actions (GitHub Pages)
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
- The instalment ZIP file — pbs176.zip
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:
- Automatically running a test suite against every commit or every pull request
- Automatically applying a linter like ESLint to all source files on every commit
- Automatically deploying code to a staging server on every commit to a specific branch
- 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:
- Individual CI/CD pipelines are referred to as workflows.
- Workflows are comprised of one or more jobs, each of which defines a sequence of steps.
- Workflows are triggered by what are referred to as events. These are actions performed on GitHub repositories.
- 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 (Settings → Pages).
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:
- Check out the code from the commit that triggered the workflow using the standard checkout action.
- Set up NodeJS version 22.x in the runner using the standard Node.JS Setup action.
- Build the web app by running the appropriate
npm
commands. - 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:
- 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:
-
Delete the
docs
folder. -
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.