PBS 177 of X: Publishing A Basic Jekyll Site (GitHub Pages)
In the previous instalment we learned how to use GitHub Actions to publish a web app using a custom build system, Webpack to be precise. We’re now moving on to solving a completely different problem — using GitHub Pages with Jekyll as a content management system. This is GitHub Pages’ default mode of operation, so we won’t need to build our own custom GitHub Actions this time, instead, we’ll be relying on the default GitHub Pages workflow provided by GitHub.
In the previous instalment, we made a point of using GitHub Pages in such a way that we could test our apps locally before pushing the tested code to GitHub and triggering an automated deployment to the internet. This kind of local testing removed a lot of friction from the development process, greatly speeding up debugging in particular. Hence, we’ll focus on using GitHub Pages and Jekyll in such a way as to facilitate the same kind of local testing. This approach complicates the initial setup of our projects a little, but that extra work up front more than pays for itself over the lifetime of any site developed in this way!
Finally, rather than building the theme for our sites completely from scratch, we’ll use Bootstrap as our scaffold, and build on top of the solid foundation it provides. Because Jekyll supports Sass (Syntactically Awesome Style Sheets) out of the box, and because Bootstrap uses Sass for customisation, this is the perfect opportunity to learn how to customise Bootstrap. Bootstrap was built to be both configurable and extendable, so its default behaviour is just a taste of its full capabilities.
In this instalment we’re going to start by laying a solid foundation for our work with Jekyll. We’ll learn how to start a Jekyll project on our local PCs, test and run it, and then publish it to the internet with GitHub Pages. We’ll start our Jekyll project with the simplest possible HTML 5 theme, and then add Bootstrap in such a way that it is ready to be customised later in the series.
Matching Podcast Episode
You can also Download the MP3
Read an unedited, auto-generated transcript with chapter marks: PBS_2025_03_01
Instalment Resources
- The instalment ZIP file — pbs177.zip
Some Important Context
It’s important to note that if you don’t want the ability to test your GitHub Pages site locally, you can skip much of this instalment and simply enable GitHub Pages to start serving your docs
folder just like we did in the first of the two worked examples in the previous instalment. At that point you can create a standard Jekyll directory structure and just start building your site.
However, there’s a very good reason not to take that easy route if you plan to use Jekyll as a full content management system — if you don’t follow these steps, your only means of testing your site will be to actually publish it to the internet by doing a full commit to main
for each and every change you want to test. This is inordinately tedious for debugging work!
The extra work described in this instalment results in a Jekyll site that is both compatible with GitHub Pages, and with a local install of Jekyll.
I mentioned in the introductory instalment that GitHub Pages uses a customised version of Jekyll, and that’s a vitally important point to understand. Because if it’s various customisations, you don’t install Jekyll directly in your docs
folder, instead you install a github-pages
package which lists many dependencies, including the specific version of Jekyll currently supported by GitHub Pages. This is always a somewhat older Jekyll version. As of March 2025, the main Jekyll project is at version 4.3
, but GitHub Pages is using Jekyll version 3.10
.
This lagging version number is important to keep in mind when reading the Jekyll documentation. Newer features will be annotated with the Jekyll version in which they were introduced, so at any time, some of the newer features described in the documentation will not yet be available for use with GitHub Pages.
Preparing to Run Jekyll Locally
Note: the instructions in this section need to be run on any machine where you have not used Jekyll before, even if you are cloning an existing Jekyll project from Git rather than starting a fresh one.
To test a Jekyll site locally you need to install Jekyll on your computer. Jekyll is written in Ruby, so the first thing you’ll need to do is install Ruby 3.
Warning: before installing Ruby 3 make sure you have the latest version of the XCode developer tools installed and that the license has been accepted, otherwise, strange errors will occour when installing various dependencies later in the process. If the developer tools are no fully in order at the point in time you install Ruby 3, simply fixing the developer tools later will not resolve the issues installing the dependencies, at the at point teh only solution will be to completely remove Ruby 3 and start over. The command
xcode-select --install
should trigger their installation or update.
Macs ship with Ruby version 2 installed, but it’s easy to upgrade to Ruby 3 using Homebrew.
To stop random things breaking on your Mac, it’s important not to replace the built-in Ruby, but to augment it with our desired newer version and a tool for flipping your terminal between versions as needed. We’ll be following Jekyll’s advice and using chruby
as our Ruby version manager.
If you don’t already have it installed, start by installing Homebrew using the instructions on their home page.
With Homebrew installed we can now install the universal Ruby version installer and the Chruby version switcher with the command:
brew install chruby ruby-install
Next, install the version of Ruby currently listed on the GitHub Pages dependencies page. As of March 2025 the command to run is:
ruby-install ruby 3.3.4
This will trigger a Unix/Linux style installation from source code, so you’ll see lots of output scrolling by, and it’ll take some time.
When Ruby finishes installing, the next step is to configure your shell to automatically use Chruby with the following commands (replacing the version number with the one you just installed):
echo "source $(brew --prefix)/opt/chruby/share/chruby/chruby.sh" >> ~/.zshrc
echo "source $(brew --prefix)/opt/chruby/share/chruby/auto.sh" >> ~/.zshrc
echo "chruby ruby-3.3.4" >> ~/.zshrc # run 'chruby' to see actual version
If you suspect you may have installed Ruby before, it is important to check that your
~/.zshrc
file does not container older duplicates of the three directives we just added. You can view the contents of the file with the commandcat ~/.zshrc
. If there are older versions, edit the file with your favourite text editor and keep the copies nearest the end of the file, those are the ones we just appended by the above commands.
These instructions assume a modern version of macOS using Zsh, if you’re still using Bash edit the commands to append to
~/.bash_profile
instead of~/.zshrc
!
With all that done, close and re-open your Terminal to pick up the new Ruby configuration. Verify your’re using Ruby 3 now with the command:
ruby --version
We’re going to be installing Ruby’s equivalent of JavaScript packages, but I just want to note that Ruby doesn’t call them ‘packages’, it calls them ‘gems’, which is some fun wordplay on the language’s name 🙂
To easily install Jekyll and its dependencies, we need to install the optional Ruby package manager Bundler. We can do that with the command:
gem install bundler
Now that we have bundler installed, we have everything we need to start running Jekyll locally.
Worked Example — A Basic Jekyll Site with Customisable Bootstrap
We’re now ready to build our first site, and we’re going to do it in such a way that we can test it locally.
Step 1 — Create a New Repository on GitHub
Log in to GitHub and create a new repository. Be sure to choose the following settings:
- Select public from the visibility radio group (GitHub Pages can be used on private repositories, but not for free).
- Not needed, but I strongly recommend ticking the box to Initialise the repo with a README file so you have a place to add instructions to yourself related to the site. This will also initialise the repository, making it easier to start using.
- Choose Jekyll in the Add .gitignore dropdown.
Step 2 — Clone Your Repo & Configure GitHub Pages Locally
Clone your newly created repo and open a command prompt in that folder.
Because Jekyll is a Ruby app, we are going to manage it using Ruby’s equivalent of an NPM configuration file, a file named Gemfile
. This file will declare the specific versions we need for each Ruby package that needs to be installed.
Start by creating a docs
folder, then change into it with the commands:
mkdir docs; cd docs
Rather than managing our Gemfile directly, we’re going to let the Bundler do that for us by using the bundle
command.
Before we can start adding our desired gems we need we need to initialise a blank Gemfile with the command:
bundle init
You’ll see that running this command in the docs
folder created the file docs/Gemfile
.
Older versions of Ruby contained the package webrick
as a standard package, but Ruby 3 doesn’t, so to avoid errors, let’s first add it to our Gemfile with the command:
bundle add webrick
This command both adds the gem to our Gemfile, and, if needed, installs it on our system.
Next, we want to add the GitHub Pages gem. This will install the needed version of Jekyll as a dependency. We can do this with the command:
bundle add github-pages --group jekyll_plugins
You’ll see that this command adds many dependencies. If you scroll up you can see the exact version of Jekyll that was added, in February 2025 the line is:
Fetching jekyll 3.10.0
Step 3 — Create an Initial Skeleton Site
Before we can test everything is working, we need to create a very basic Jekyll site in the docs
folder.
First, we’ll create a very basic settings file for the site by creating a file named docs/_config.yml
with the following contents (you’ll find a copy of this file in the Instalment Resources as _config-1.yml
which you’ll need to rename to _config.yml
):
# Site Settings
title: PBS 177 Test Site
Finally, create a very basic home page by creating a file named docs/index.md
with the following content (included in the resources as index-1.md
which you would rename to index.md
):
---
title: Home
---
# Hello World!
This is a really basic Jekyll site 🙂
Step 4 — Test Jekyll Locally
We now have Jekyll configured and a basic site in place so we’re ready to test it with the command:
bundle exec jekyll serve
This will render the contents of the docs
directory as a website and write the generated files to the folder docs/_site
and then start a locally running web server on port 4000
with that folder as the source. You’ll see the URL for this local server in the output, and by default it will be http://127.0.0.1:4000
.
Note that this command does not exit, but keeps running. You can kill the local server at any time with
ctrl
+c
or closing the common prompt, but you’ll only need to do that when you’re finished for the day, because the Jekyll server monitors the file system for changes, and automatically rebuilds your site each time you change a file in the thedocs
folder. This makes debugging very easy.
To see the generated site, open the URL in your browser.
Notice that the site title we defined in _config.yml
appears in the two places in the default theme:
- In the browser window/tab title
- In the automatically generated navigation at the top of the page as the link to always take you to the site’s home page
Notice that the page title defined in the front matter in index.md
also appears in the browser window/tab title.
The rest of the content of the home page is the rendered markdown from the body of index.md
.
Step 5 — Commit & Push to GitHub
Before we commit our changes, we need to add docs/Gemfile.lock
to the .gitignore
file. You can do that by editing the .gitignore
file manually, by using your Git GUI app of choice, or by running the following command:
echo '/docs/Gemfile.lock' >> ../.gitignore
Note that if you forgot to tell GitHub to use the standard ignore file for Jekyll you’ll also need to add the docs/_site
folder to your ignore file.
You can now commit all remaining new and changed files, and push your commit to GitHub.
Step 6 — Enable GitHub Pages & Test
Log in to the GitHub portal, open your repository, and then navigate to the GitHub Pages settings page (Settings → Pages).
Enable GitHub Pages with the following settings:
- Leave the Source as Deploy from a branch
- Set the Branch to
main
with the folder/docs
and save
Navigate to Actions from the top menu bar and watch the site build.
When the build completes, click on the workflow name to see the details, this will include the published URL of your site.
Open the site in your browser to verify it works!
Step 7 — Add Bootstrap
The version of Bootstrap we’ve used to date has been the compiled version, that is to say, a collection of finished CSS files. If you open those files you’ll find they’re massively repetitive, with very similar code repeated many times for similarly styled items. For example, the styles for all classes with -success
in their name share the same line for setting the colour. It would be nearly impossible to actually build a CSS library as large as Bootstrap by hand. That’s why it’s not written by hand, instead, it’s written in Sass. Sass is a language I like to think of as CSS with superpowers because it supports things like nesting, complex variables, and looping. Sass files get compiled down to CSS files, which is why Sass is referred to as a CSS pre-processor. Simple Sass files can be used to generate complex CSS files.
To effectively customise Bootstrap you need to use the source Sass files as your starting point. You then make your additions and changes in Sass, and render your custom version of Bootstrap to CSS.
Due to the fact that the default Jekyll theme uses CSS class names that clash with Bootstrap’s CSS class names, we can’t just add Bootstrap to the default theme. Instead, we’re going to need to create our own theme. Don’t worry, this is not difficult to do.
Enable Advanced Sass Processing on our Site
Jekyll has native Sass support, so it has almost everything we need to compile the Bootstrap Sass files to CSS for us. Almost, but not quite! It’s just missing one important feature — a so-called autoprefixer for automatically handling browser-specific quirks through vendor prefixes when compiling Sass to CSS. Thankfully there’s a Jekyll plugin available to provide this functionality, it’s just not installed by default.
Before we can configure the plugin we need to install it locally with the following command from /docs
:
bundle add jekyll-autoprefixer --group jekyll_plugins
As of March 2025, there’s a known issue with this plugin that requires a specific version of the execjs
gem to be used, so add that too with the command:
bundle add execjs -v 2.7
Then we can enable and configure the plugin in our site settings. Update docs/_config.yml
to the following (in the resources as _config-2.yml
):
# Site Settings
title: PBS 177 Test Site
# Build Settings
plugins:
- jekyll-autoprefixer
# Settings for the autoprefixer plugin
autoprefixer:
browsers:
- last 4 versions
- Safari > 2
Add the Needed Bootstrap Files
We’re now ready to download the source version of Bootstrap and add it to our site.
On the Bootstrap download page, click the button to Download source and extract the resulting ZIP file.
Before we add the Bootstrap code, let’s take a moment to learn about two important folder locations used by Jekyll that relate to JavaScript, CSS & Sass files.
- By convention, and as per the examples in the Jekyll documentation, the
assets
folder is used to store files referenced in your theme like Javascript files, cascading style sheets (CSS), and images like icons & logos. Another convention, though not quite as universal, is to organise theassets
folder into type-specific subfolders. We’ll be following this convention by using:assets/js
for our JavaScript files.assets/css
for our generated CSS. But, note that we’ll be writing our CSS as Sass, so the file extension on the files we create will be.scss
. Jekyll will convert them to plain CSS when it builds the site, so the files that appear in the generated website will have their file extensions changed from.scss
to.css
.
- The
_sass
folder is used to store additional Sass imports, this is to say Sass helper files referenced inassets/css/*.scss
files using the@import
Sass directive.
I want to draw attention to two very important things to understand about using Jekyll to convert SASS to CSS:
- Assets with the
.scss
file extension that contain front matter (usually blank) are compiled to CSS automatically. This means that a file namedassets/css/style.scss
with front matter will get converted toassets/css/style.css
, and your theme’s HTML needs to reference the file with that.css
file extension for it to be successfully used. But, if you forget to add front matter, the Sass conversion will not happen! - The entry points to your Sass code must be
assets
, so sticking to best practices, that means you add your primary Sass file(s) toassets/css
. Within those primary Sass files, each time you call@import
, Sass will try to find the requested Sass utility code in the_sass
folder.
Putting it all together, we need to create a primary Sass file in docs/assets/css
, save Bootstrap’s Sass files in docs/_sass
, and Bootstrap’s JavaScript files in docs/assets/js
.
In your local clone of your repository:
- Create a folder named
docs/_sass/bootstrap
and copy the entire contents of thescss
folder from the downloaded and extracted Bootstrap source ZIP here. - Create a folder named
docs/assets/js
and copy both thedist/js/bootstrap.bundle.min.js
anddist/js/bootstrap.bundle.min.js.map
files from the Bootstrap source ZIP here.
Because Bootstrap’s Sass code contains a folder named vendor
which often needs to be ignored in Jekyll sites, it is included in GitHub’s ignore template for Jekyll which we chose to use when creating our repo. This means we need to add an exception for just Bootstrap’s vendor
folder to our .gitignore
. As described in instalment PBS 120 in our long Git series, exceptions to being ignored also get defined in the .gitignore
file, but they need to come after the lines that would otherwise cause them to be ignored. Exceptions are pre-fixed with an (!
). So, all we need to do is add the following to the end of our ignore file:
!/docs/_sass/bootstrap/vendor/
You can do that by editing the .gitignore
file manually, by using your Git GUI app of choice, or by running the following command:
echo '!/docs/_sass/bootstrap/vendor/' >> ../.gitignore
We’re now ready to create a Sass entry point that imports Bootstrap, and, for now, does nothing more. Create a file named docs/assets/css/style.scss
with the following contents (assets-css-style-1.scss
in the resources):
---
---
@import "bootstrap/bootstrap";
At this point, if we asked Jekyll to rebuild our site, all of Bootstrap would appear in our generated site as assets/css/style.css
.
Create our Very Basic Custom Theme
Finally, we need to create the simplest possible custom theme and include the Bootstrap CSS in it.
Create a file named docs/_layouts/default.html
with the following content (in the resources as _layouts-default-1.html
):
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Publishing A Basic Jekyll Site | Programming by Stealth</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<main class="container pt-3">
<h1 class="mb-0">PBS 176 of X: Deploying a JavaScript Web App with Webpack & GitHub Actions (GitHub Pages)</h1>
<small class="mb-3 text-muted d-flex flex-column flex-md-row">
<div class="pr-3"><i class="fas fa-calendar-day pr-1" aria-label="Date"></i> 01 Feb 2025</div>
<div class="pr-3"><i class="fas fa-id-card pr-1" aria-label="Creators"></i>
<a href="/about.html#bart" class="badge badge-pill badge-accent text-white">Bart Busschots</a>
<a href="/about.html#allison" class="badge badge-pill badge-accent text-white">Allison Sheridan</a>
</div>
<div>
</div>
</small>
<p><a href="./pbs175">The previous instalment</a> was a very high-level overview of <a href="https://pages.github.com">GitHub Pages</a>, 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.</p>
<p>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 <a href="https://jquery.com">jQuery</a>, <a href="https://getbootstrap.com">Bootstrap</a>, and <a href="https://github.com/janl/mustache.js">Mustache</a>. Then we broadened our focus a little to look at some tools used to facilitate the development of more complex apps like <a href="https://git-scm.com">Git</a> for source control, <a href="https://nodejs.org/en">NodeJS</a> + <a href="https://www.npmjs.com">NPM</a> for package management, <a href="https://eslint.org">ES Lint</a> for flagging poor coding practices, <a href="https://jsdoc.app">JS Doc</a> and <a href="https://mermaid.js.org">Mermaid</a> for documenting our work, <a href="https://jestjs.io">Jest</a> for testing our code, and finally <a href="https://webpack.js.org">Webpack</a> for bundling our code for distribution.</p>
<p>Along the way, we also took some detours into more coding-adjacent areas like <a href="https://www.chezmoi.io">Chezmoi</a> for managing configuration files for our command line tools in Git, the ubiquitous <a href="https://www.gnu.org/software/bash/">Bash</a> for shell scripting, <a href="https://jqlang.org">jq</a> for processing <a href="https://www.json.org/json-en.html">JSON</a>, <a href="https://yaml.org">YAML</a> for simple data markup, and we even dipped our toes in some software engineering with a look at the <a href="https://en.wikipedia.org/wiki/Model–view–controller">MVC</a> design pattern.</p>
<p>With all those diversions, the last web app we built as a worked example was actually in <a href="./pbs138">instalment 138</a>, 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!</p>
<p>In this instalment, we’ll learn how to use GitHub Pages to publish our bundled client-side web apps to the internet for free.</p>
<p>What’s more, we’re going to leverage GitHub’s powerful <a href="https://en.wikipedia.org/wiki/CI/CD">CI/CD</a> (<em>Continuous Integration/Continuous Deployment</em>) tool, <a href="https://github.com/features/actions">GitHub Actions</a>, to automate the process.</p>
<p>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 <code class="language-plaintext highlighter-rouge">main</code> branch using GitHub Pages.</p>
<h2 id="matching-podcast-episode">Matching Podcast Episode</h2>
<audio controls="" src="https://media.blubrry.com/nosillacast/traffic.libsyn.com/nosillacast/PBS_2025_02_15.mp3?autoplay=0&loop=0&controls=1">Your browser does not support HTML 5 audio 🙁</audio>
<p>You can also <a href="https://media.blubrry.com/nosillacast/traffic.libsyn.com/nosillacast/PBS_2025_02_15.mp3">Download the MP3</a></p>
<p>Read an unedited, auto-generated transcript with chapter marks: <a href="https://podfeet.com/transcripts/PBS_2025_02_15.html">PBS_2025_02_15</a></p>
<h2 id="instalment-resources">Instalment Resources</h2>
<ul>
<li>The instalment ZIP file — <a href="./assets/pbs176.zip">pbs176.zip</a></li>
</ul>
<h2 id="what-is-cicd-continuous-integrationcontinuous-deployment">What is CI/CD (Continuous Integration/Continuous Deployment)?</h2>
<p>At a philosophical level, CI/CD is a model of software development where sporadic <em>big bang</em> 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.</p>
<p>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.</p>
<p>In the broader world of software development, CI/CD pipelines are often used to perform tasks like:</p>
<ol>
<li>Automatically running a test suite against every commit or every pull request</li>
<li>Automatically applying a linter like ESLint to all source files on every commit</li>
<li>Automatically deploying code to a staging server on every commit to a specific branch</li>
<li>Automatically deploying production code on every commit to the default branch</li>
</ol>
<p>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.</p>
<blockquote class="aside">
<p>If you’re interested in learning more about CI/CD, <a href="https://en.wikipedia.org/wiki/CI/CD">the Wikipedia article on the topic</a> is a good starting point.</p>
</blockquote>
<h2 id="introducing-github-actions">Introducing GitHub Actions</h2>
<p>GitHub implements CI/CD pipelines with its <a href="https://github.com/features/actions">GitHub Actions</a> feature.</p>
<p>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 <a href="https://en.m.wikipedia.org/wiki/Containerization_(computing)">containerization</a>, which allows for extremely efficient creation and destruction of VMs.</p>
<h3 id="github-actions-terminology">GitHub Actions Terminology</h3>
<p>The GitHub docs have <a href="https://docs.github.com/en/actions/about-github-actions/understanding-github-actions">a nice overview page for how GitHub Actions work</a>, but the key concepts are quite simple:</p>
<ol>
<li>Individual CI/CD pipelines are referred to as <strong>workflows</strong>.</li>
<li>Workflows are comprised of one or more <strong>jobs</strong>, each of which defines a sequence of <strong>steps</strong>.</li>
<li>Workflows are triggered by what are referred to as <strong>events</strong>. These are actions performed on GitHub repositories.</li>
<li>Workflows run on virtual machines referred to as <strong>runners</strong>, and as of February 2025, runners can be Ubuntu Linux, Windows, or macOS.</li>
</ol>
<h3 id="defining-workflows">Defining Workflows</h3>
<p>Workflows are defined in YAML files stored in the special folder <code class="language-plaintext highlighter-rouge">.github/workflows</code>. There is an entire section of the GitHub docs dedicated to <a href="https://docs.github.com/en/actions/writing-workflows">writing workflows</a>.</p>
<p>A GitHub workflow definition consists of a <strong>YAML file defining a single top-level dictionary</strong> with keys for the different aspects of a workflow.</p>
<p>The most important top-level workflow keys are:</p>
<table>
<thead>
<tr>
<th>Workflow Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>The human-friendly name for the Workflow in the GitHub user interface.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">on</code></td>
<td>Dictionary (<strong>Required</strong>)</td>
<td>The event or events that should trigger the workflow.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">jobs</code></td>
<td>Dictionary (<strong>Required</strong>)</td>
<td>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.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">permissions</code></td>
<td> </td>
<td>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 <code class="language-plaintext highlighter-rouge">none</code>, <code class="language-plaintext highlighter-rouge">read</code>, or <code class="language-plaintext highlighter-rouge">write</code> (<code class="language-plaintext highlighter-rouge">write</code> includes <code class="language-plaintext highlighter-rouge">read</code>). For more details, see <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions">the Permissions section of the documentation</a>.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within all jobs defined in the workflow.</td>
</tr>
</tbody>
</table>
<p>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:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
<span class="err"> </span><span class="na">push</span><span class="pi">:</span>
<span class="err"> </span><span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="s2">"</span><span class="s">main"</span> <span class="pi">]</span>
</code></pre></div></div>
<p>If you want to add additional events to your workflows, there is an entire section of the GitHub documentation dedicated to <a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs">triggering workflows</a>.</p>
<p>This takes us to the most complex of the top-level keys: <code class="language-plaintext highlighter-rouge">jobs</code>. The definition for a job is itself a dictionary, with the following being the most important keys:</p>
<table>
<thead>
<tr>
<th>Job Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>A human-friendly name for the job.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">needs</code></td>
<td>String or array of Strings (<strong>Optional</strong>)</td>
<td>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.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">runs-on</code></td>
<td>Can be a String, but more complex definitions are possible (<strong>Required</strong>)</td>
<td>The type of virtual machine on which the job should be run.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">steps</code></td>
<td>Array of Dictionaries (<strong>Required</strong>)</td>
<td>An array of dictionaries, each defining a step. The steps will be executed in order.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">permissions</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining permissions for the job. The syntax is the same as for the workflow-level <code class="language-plaintext highlighter-rouge">permissions</code> key described in the previous table.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within all steps defined in the job.</td>
</tr>
</tbody>
</table>
<p>Again, we’re going to keep things simple in this series, so we’re always going to use Linux runners. That means our <code class="language-plaintext highlighter-rouge">runs-on</code> definition will always be <code class="language-plaintext highlighter-rouge">runs-on: ubuntu-latest</code>.</p>
<p>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:</p>
<table>
<thead>
<tr>
<th>Step Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>A human-friendly name for the step.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">run</code></td>
<td>String</td>
<td>One or more shell commands to execute. To run multiple commands, use a multi-line string. <em>Use this key <strong>or</strong> <code class="language-plaintext highlighter-rouge">uses</code>, not both!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">uses</code></td>
<td>String</td>
<td>A valid specifier for a pre-defined action to execute with an optional but strongly recommended version number. See the documentation for <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses">descriptions of the supported specifiers</a>. You can find a <a href="https://github.com/marketplace?type=actions">listing of pre-defined actions in the GitHub market place</a>. <em>Use this key <strong>or</strong> <code class="language-plaintext highlighter-rouge">run</code>, not both!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">with</code></td>
<td>Dictionary</td>
<td>This key provides a mechanism for passing arguments to a predefined action included via the <code class="language-plaintext highlighter-rouge">uses</code> 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. <em>Only use this key with <code class="language-plaintext highlighter-rouge">uses</code>!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within just this step.</td>
</tr>
</tbody>
</table>
<h2 id="worked-example-1--manually-publishing-a-webpack-bundled-web-app-on-github-pages">Worked Example 1 — Manually Publishing a WebPack-Bundled Web App on GitHub Pages</h2>
<p>As we learned in instalments <a href="./pbs138">138</a> & <a href="./pbs139">139</a>, 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:</p>
<table>
<thead>
<tr>
<th>File/Folder</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">src/*</code></td>
<td>The source code for our web app.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">docs/*</code></td>
<td>The built web app.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">package.json</code></td>
<td>The NodeJS/NPM configuration for our web app.</td>
</tr>
</tbody>
</table>
<p>Rather than building a fresh example, we’ll reuse the final example web app from instalment <a href="./pbs139">139</a>.</p>
<h3 id="step-1--create-a-new-public-github-repository--clone-it-locally">Step 1 — Create a new Public GitHub Repository & Clone it Locally</h3>
<p>Log in to GitHub and <a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository">create a new repository in the usual way</a>. If you want to be able to experiment with GitHub Actions and GitHub Pages for <strong>free</strong> you’ll need to make it a <strong>public repository</strong>.</p>
<p>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.</p>
<h3 id="step-2--add-the-web-app-code--nodejsnpm-configuration">Step 2 — Add the Web App Code & NodeJS/NPM Configuration</h3>
<p>Extract the contents of the Instalment ZIP file and copy all the files and folders into your cloned repo. Rename the file <code class="language-plaintext highlighter-rouge">_gitignore</code> to <code class="language-plaintext highlighter-rouge">.gitignore</code> and then commit everything as the initial code. You’ll need to do that from the Terminal, as macOS prohibits this action.</p>
<h3 id="step-3--manually-build-the-app--commit-the-web-app">Step 3 — Manually Build the App & Commit the Web App</h3>
<p>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, <strong>make sure you have the latest NodeJS installed</strong> (or at least the latest LTS version)!</p>
<p>On the command prompt you have open in your cloned copy of the repo, start by initialising NodeJS with the command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm ci
</code></pre></div></div>
<p><em><strong>Reminder:</strong> this command does a ‘clean install’, downloading the exact versions of each package listed as required in <code class="language-plaintext highlighter-rouge">package.json</code> as recorded in <code class="language-plaintext highlighter-rouge">package-lock.json</code>.</em></p>
<p>Now, let’s build our web app with the command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build
</code></pre></div></div>
<p>This will build our web app using the source code in <code class="language-plaintext highlighter-rouge">/src</code> as specified by the Webpack configuration in <code class="language-plaintext highlighter-rouge">webpack.config.js</code> using the build command defined in <code class="language-plaintext highlighter-rouge">package.json</code> and publish it to <code class="language-plaintext highlighter-rouge">/docs/index.html</code>.</p>
<p>Open <code class="language-plaintext highlighter-rouge">/docs/index.html</code> 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.</p>
<p>Commit the changes and then push them up to GitHub.</p>
<h3 id="step-3--enable-github-pages">Step 3 — Enable GitHub Pages</h3>
<p>Log in to the GitHub web interface and open the repo’s GitHub Pages settings (<strong>Settings</strong> → <strong>Pages</strong>).</p>
<p>Change the <strong>Source</strong> dropdown to <strong>Deploy from a branch</strong>, choose <strong>main</strong> and the folder <strong>/docs</strong>, then <strong>Save</strong>.</p>
<p>After you save the settings, Choose <strong>Actions</strong> from the button bar to see all the actions on your repo. An action will have been created for you named <strong>pages build and deployment</strong>, and depending on how quickly you navigated, you will see its status as <em>queued</em> or <em>in progress</em>, 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.</p>
<p>Open the URL shown in the workflow output in your browser to verify that you have successfully published your web app to the internet!</p>
<h2 id="worked-example-2--automate-deployment-of-a-webpack-app-with-github-actions">Worked Example 2 — Automate Deployment of a Webpack App with GitHub Actions</h2>
<p>Now that we can publish our app manually, let’s automate it with GitHub Actions.</p>
<p>To do this, we’ll be making use of a collection of officially verified standard actions from the marketplace and using a <code class="language-plaintext highlighter-rouge">run</code> step to execute our two NPM commands (<code class="language-plaintext highlighter-rouge">npm ci</code> & <code class="language-plaintext highlighter-rouge">npm run build</code>).</p>
<blockquote class="notice">
<p>It’s important not to use an action from an untrusted author, so I always set the <strong>By</strong> filter for all my search results to <strong>Verified Creators</strong>.</p>
</blockquote>
<h3 id="step-1--update-github-pages-settings-to-use-a-github-actions-workflow">Step 1 — Update GitHub Pages Settings to Use a GitHub Actions Workflow</h3>
<p>Log back in to the GitHub web interface and open the GitHub Pages settings again. Change the <strong>Source</strong> dropdown to <strong>GitHub Actions</strong> and choose to create your own action. This will open a GitHub web editor with a default file as the content.</p>
<p>Enter a sensible filename with a <code class="language-plaintext highlighter-rouge">.yaml</code> extension, e.g. <code class="language-plaintext highlighter-rouge">deploy-app.yaml</code>.</p>
<p>For the file’s contents, enter the following</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build with Webpack and Publish to GitHub Pages</span>
<span class="na">on</span><span class="pi">:</span>
<span class="na">push</span><span class="pi">:</span>
<span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="s2">"</span><span class="s">main"</span> <span class="pi">]</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">permissions</span><span class="pi">:</span>
<span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout code</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup NodeJS</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v4</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">node-version</span><span class="pi">:</span> <span class="s">22.x</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build with Webpack</span>
<span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">npm ci</span>
<span class="s">npx webpack</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload generated files as artifact</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-pages-artifact@v3</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">docs/</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
<span class="na">permissions</span><span class="pi">:</span>
<span class="na">pages</span><span class="pi">:</span> <span class="s">write</span>
<span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">github-pages</span>
<span class="na">url</span><span class="pi">:</span> <span class="s">$</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to GitHub Pages</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/deploy-pages@v4</span>
</code></pre></div></div>
<p>This YAML code defines a <strong>workflow</strong> with two <strong>jobs</strong>, one with the id <code class="language-plaintext highlighter-rouge">build</code> and one with the id <code class="language-plaintext highlighter-rouge">deploy</code>. The workflow is triggered by pushing to <code class="language-plaintext highlighter-rouge">main</code> and both jobs are configured to run on Linux runners (<code class="language-plaintext highlighter-rouge">runs-on: unbuntu-latest</code>). Note that the <code class="language-plaintext highlighter-rouge">deploy</code> job forces itself to run after the <code class="language-plaintext highlighter-rouge">build</code> job with <code class="language-plaintext highlighter-rouge">needs: build</code>.</p>
<p>The <code class="language-plaintext highlighter-rouge">build</code> job executes <strong>steps</strong> to:</p>
<ol>
<li>Check out the code from the commit that triggered the workflow using the <a href="https://github.com/marketplace/actions/checkout">standard checkout action</a>.</li>
<li>Set up NodeJS version 22.x in the runner using the <a href="https://github.com/marketplace/actions/setup-node-js-environment">standard Node.JS Setup action</a>.</li>
<li>Build the web app by running the appropriate <code class="language-plaintext highlighter-rouge">npm</code> commands.</li>
<li>Upload the generated site to the GitHub Pages Servers as a so-called <em>artifact</em> using the <a href="https://github.com/actions/upload-pages-artifact">standard Upload Pages Artifact action</a>.</li>
</ol>
<p>With the generated files now uploaded as an artifact, the <code class="language-plaintext highlighter-rouge">deploy</code> job executes just one <strong>step</strong> to:</p>
<ol>
<li>Publish the uploaded artifact to the internet using the <a href="https://github.com/marketplace/actions/deploy-github-pages-site">standard Deploy Pages action</a>.</li>
</ol>
<h3 id="step-2--clean-up-and-test-the-automated-deployment">Step 2 — Clean up and Test the Automated Deployment</h3>
<p>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:</p>
<ol>
<li>
<p>Delete the <code class="language-plaintext highlighter-rouge">docs</code> folder.</p>
</li>
<li>
<p>Update line 27 of <code class="language-plaintext highlighter-rouge">src/index.js</code> so we’ll be able to see that the web app changed:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">#world-modifier</span><span class="dl">'</span><span class="p">).</span><span class="nx">text</span><span class="p">(</span><span class="dl">'</span><span class="s1">Automated Webpack</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div> </div>
</li>
</ol>
<p>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.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>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 <a href="https://beta.xkpasswd.net/">new JavaScript-based version of the XKPasswd secure memorable password generator</a> developed by members of the PBS community will be deployed when it comes out of beta in 2025!</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<div class="text-center mt-5">
<div class="mx-5 mb-1 text-center">
<div class="btn-group btn-group-sm w-100 px-5 text-center" role="group" aria-label="Miniseries Navigation">
<a href="/pbs175" class="btn btn-outline-secondary" role="button"><i class="far fa-arrow-alt-circle-left" aria-label="Previous"></i>
Static Site Generators
</a><a href="#" class="btn btn-outline-secondary disabled" role="button" aria-disabled="true">GitHub Pages Miniseries</a><a href="/pbs177" class="btn btn-outline-secondary" role="button">
Publishing A Basic Jekyll Site <i class="far fa-arrow-alt-circle-right" aria-label="Next"></i>
</a></div>
</div><div class="btn-group w-100 px-5 text-center" role="group" aria-label="Instalment Navigation">
<a href="/pbs175" class="btn btn-outline-primary" role="button"><i class="far fa-arrow-alt-circle-left" aria-label="Previous"></i> 175. Static Site Generators
</a><a href="/#pbs" class="btn btn-outline-primary" role="button">Programming by Stealth</a><a href="/pbs177" class="btn btn-outline-primary" role="button">177. Publishing A Basic Jekyll Site <i class="far fa-arrow-alt-circle-right" aria-label="Next"></i>
</a></div>
</div>
</main>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
</body>
</html>
As you can see, this file contains mostly just regular HTML, but it does contain a few Liquid template strings (the sections wrapped in doubled curly braces, i.e. ``). For now, I’m going to ask you to accept this syntax as magic sauce that makes the theme work, but we will learn all about Liquid templates as the series progresses.
Verify the Changes
With these changes made, we can test our site locally with the bundle exec jekyll serve
command, and you’ll see it now looks like a regular Bootstrap page.
Commit all changed files and push to GitHub. Watch the build action as normal, and then verify that the published site is now successfully using our basic Bootstrap theme.
Step 8 — Test Bootstrap
The fact that the regular Bootstrap CSS has been successfully generated and included in the template can be inferred from the fact that the page is rendering in Bootstrap’s default sans-serif font, but that doesn’t prove that we have successfully achieved the ability to customise Bootstrap, or, that Bootstrap’s JavaScript has been successfully incorporated into our theme. Let’s demonstrate both of those things in turn by making some temporary edits to our base theme.
Verifying Bootstrap Customisability
One of the mechanisms by which Bootstrap can be customised is by overriding the value of one of its Sass variables. As an example, let’s update the variables that control the body background and body text colours. Open docs/assets/css/style.scss
and edit it to add two variable definitions before the line that imports Bootstrap. The updated file should look like the following (assets-css-style-2.scss
in the resources):
---
---
/* Override Bootstrap Variables */
$body-bg: LightSkyBlue;
$body-color: MidnightBlue;
/* Import Bootstrap */
@import "bootstrap/bootstrap";
Saving this file and regenerating your site locally will now show dark blue text on a light blue background.
Verify Bootstrap JavaScript
To verify that Bootstrap’s JavaScript is being correctly loaded by the theme, we’ll add a dismissible Bootstrap alert to the top of our theme. Edit docs/_layouts/default.html
to add the following alert at the top of the <main>
tag:
<div class="alert alert-info alert-dismissible fade show" role="alert">
Click my close button to prove that Bootstrap JavaScript is working!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
The complete file should now contain (_layouts-default-2.html
in the resources):
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Publishing A Basic Jekyll Site | Programming by Stealth</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<main class="container pt-3">
<div class="alert alert-info alert-dismissible fade show" role="alert">
Click my close button to prove that Bootstrap JavaScript is working!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<h1 class="mb-0">PBS 176 of X: Deploying a JavaScript Web App with Webpack & GitHub Actions (GitHub Pages)</h1>
<small class="mb-3 text-muted d-flex flex-column flex-md-row">
<div class="pr-3"><i class="fas fa-calendar-day pr-1" aria-label="Date"></i> 01 Feb 2025</div>
<div class="pr-3"><i class="fas fa-id-card pr-1" aria-label="Creators"></i>
<a href="/about.html#bart" class="badge badge-pill badge-accent text-white">Bart Busschots</a>
<a href="/about.html#allison" class="badge badge-pill badge-accent text-white">Allison Sheridan</a>
</div>
<div>
</div>
</small>
<p><a href="./pbs175">The previous instalment</a> was a very high-level overview of <a href="https://pages.github.com">GitHub Pages</a>, 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.</p>
<p>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 <a href="https://jquery.com">jQuery</a>, <a href="https://getbootstrap.com">Bootstrap</a>, and <a href="https://github.com/janl/mustache.js">Mustache</a>. Then we broadened our focus a little to look at some tools used to facilitate the development of more complex apps like <a href="https://git-scm.com">Git</a> for source control, <a href="https://nodejs.org/en">NodeJS</a> + <a href="https://www.npmjs.com">NPM</a> for package management, <a href="https://eslint.org">ES Lint</a> for flagging poor coding practices, <a href="https://jsdoc.app">JS Doc</a> and <a href="https://mermaid.js.org">Mermaid</a> for documenting our work, <a href="https://jestjs.io">Jest</a> for testing our code, and finally <a href="https://webpack.js.org">Webpack</a> for bundling our code for distribution.</p>
<p>Along the way, we also took some detours into more coding-adjacent areas like <a href="https://www.chezmoi.io">Chezmoi</a> for managing configuration files for our command line tools in Git, the ubiquitous <a href="https://www.gnu.org/software/bash/">Bash</a> for shell scripting, <a href="https://jqlang.org">jq</a> for processing <a href="https://www.json.org/json-en.html">JSON</a>, <a href="https://yaml.org">YAML</a> for simple data markup, and we even dipped our toes in some software engineering with a look at the <a href="https://en.wikipedia.org/wiki/Model–view–controller">MVC</a> design pattern.</p>
<p>With all those diversions, the last web app we built as a worked example was actually in <a href="./pbs138">instalment 138</a>, 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!</p>
<p>In this instalment, we’ll learn how to use GitHub Pages to publish our bundled client-side web apps to the internet for free.</p>
<p>What’s more, we’re going to leverage GitHub’s powerful <a href="https://en.wikipedia.org/wiki/CI/CD">CI/CD</a> (<em>Continuous Integration/Continuous Deployment</em>) tool, <a href="https://github.com/features/actions">GitHub Actions</a>, to automate the process.</p>
<p>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 <code class="language-plaintext highlighter-rouge">main</code> branch using GitHub Pages.</p>
<h2 id="matching-podcast-episode">Matching Podcast Episode</h2>
<audio controls="" src="https://media.blubrry.com/nosillacast/traffic.libsyn.com/nosillacast/PBS_2025_02_15.mp3?autoplay=0&loop=0&controls=1">Your browser does not support HTML 5 audio 🙁</audio>
<p>You can also <a href="https://media.blubrry.com/nosillacast/traffic.libsyn.com/nosillacast/PBS_2025_02_15.mp3">Download the MP3</a></p>
<p>Read an unedited, auto-generated transcript with chapter marks: <a href="https://podfeet.com/transcripts/PBS_2025_02_15.html">PBS_2025_02_15</a></p>
<h2 id="instalment-resources">Instalment Resources</h2>
<ul>
<li>The instalment ZIP file — <a href="./assets/pbs176.zip">pbs176.zip</a></li>
</ul>
<h2 id="what-is-cicd-continuous-integrationcontinuous-deployment">What is CI/CD (Continuous Integration/Continuous Deployment)?</h2>
<p>At a philosophical level, CI/CD is a model of software development where sporadic <em>big bang</em> 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.</p>
<p>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.</p>
<p>In the broader world of software development, CI/CD pipelines are often used to perform tasks like:</p>
<ol>
<li>Automatically running a test suite against every commit or every pull request</li>
<li>Automatically applying a linter like ESLint to all source files on every commit</li>
<li>Automatically deploying code to a staging server on every commit to a specific branch</li>
<li>Automatically deploying production code on every commit to the default branch</li>
</ol>
<p>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.</p>
<blockquote class="aside">
<p>If you’re interested in learning more about CI/CD, <a href="https://en.wikipedia.org/wiki/CI/CD">the Wikipedia article on the topic</a> is a good starting point.</p>
</blockquote>
<h2 id="introducing-github-actions">Introducing GitHub Actions</h2>
<p>GitHub implements CI/CD pipelines with its <a href="https://github.com/features/actions">GitHub Actions</a> feature.</p>
<p>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 <a href="https://en.m.wikipedia.org/wiki/Containerization_(computing)">containerization</a>, which allows for extremely efficient creation and destruction of VMs.</p>
<h3 id="github-actions-terminology">GitHub Actions Terminology</h3>
<p>The GitHub docs have <a href="https://docs.github.com/en/actions/about-github-actions/understanding-github-actions">a nice overview page for how GitHub Actions work</a>, but the key concepts are quite simple:</p>
<ol>
<li>Individual CI/CD pipelines are referred to as <strong>workflows</strong>.</li>
<li>Workflows are comprised of one or more <strong>jobs</strong>, each of which defines a sequence of <strong>steps</strong>.</li>
<li>Workflows are triggered by what are referred to as <strong>events</strong>. These are actions performed on GitHub repositories.</li>
<li>Workflows run on virtual machines referred to as <strong>runners</strong>, and as of February 2025, runners can be Ubuntu Linux, Windows, or macOS.</li>
</ol>
<h3 id="defining-workflows">Defining Workflows</h3>
<p>Workflows are defined in YAML files stored in the special folder <code class="language-plaintext highlighter-rouge">.github/workflows</code>. There is an entire section of the GitHub docs dedicated to <a href="https://docs.github.com/en/actions/writing-workflows">writing workflows</a>.</p>
<p>A GitHub workflow definition consists of a <strong>YAML file defining a single top-level dictionary</strong> with keys for the different aspects of a workflow.</p>
<p>The most important top-level workflow keys are:</p>
<table>
<thead>
<tr>
<th>Workflow Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>The human-friendly name for the Workflow in the GitHub user interface.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">on</code></td>
<td>Dictionary (<strong>Required</strong>)</td>
<td>The event or events that should trigger the workflow.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">jobs</code></td>
<td>Dictionary (<strong>Required</strong>)</td>
<td>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.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">permissions</code></td>
<td> </td>
<td>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 <code class="language-plaintext highlighter-rouge">none</code>, <code class="language-plaintext highlighter-rouge">read</code>, or <code class="language-plaintext highlighter-rouge">write</code> (<code class="language-plaintext highlighter-rouge">write</code> includes <code class="language-plaintext highlighter-rouge">read</code>). For more details, see <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions">the Permissions section of the documentation</a>.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within all jobs defined in the workflow.</td>
</tr>
</tbody>
</table>
<p>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:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
<span class="err"> </span><span class="na">push</span><span class="pi">:</span>
<span class="err"> </span><span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="s2">"</span><span class="s">main"</span> <span class="pi">]</span>
</code></pre></div></div>
<p>If you want to add additional events to your workflows, there is an entire section of the GitHub documentation dedicated to <a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs">triggering workflows</a>.</p>
<p>This takes us to the most complex of the top-level keys: <code class="language-plaintext highlighter-rouge">jobs</code>. The definition for a job is itself a dictionary, with the following being the most important keys:</p>
<table>
<thead>
<tr>
<th>Job Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>A human-friendly name for the job.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">needs</code></td>
<td>String or array of Strings (<strong>Optional</strong>)</td>
<td>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.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">runs-on</code></td>
<td>Can be a String, but more complex definitions are possible (<strong>Required</strong>)</td>
<td>The type of virtual machine on which the job should be run.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">steps</code></td>
<td>Array of Dictionaries (<strong>Required</strong>)</td>
<td>An array of dictionaries, each defining a step. The steps will be executed in order.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">permissions</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining permissions for the job. The syntax is the same as for the workflow-level <code class="language-plaintext highlighter-rouge">permissions</code> key described in the previous table.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within all steps defined in the job.</td>
</tr>
</tbody>
</table>
<p>Again, we’re going to keep things simple in this series, so we’re always going to use Linux runners. That means our <code class="language-plaintext highlighter-rouge">runs-on</code> definition will always be <code class="language-plaintext highlighter-rouge">runs-on: ubuntu-latest</code>.</p>
<p>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:</p>
<table>
<thead>
<tr>
<th>Step Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">name</code></td>
<td>String (<strong>Recommended</strong>)</td>
<td>A human-friendly name for the step.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">run</code></td>
<td>String</td>
<td>One or more shell commands to execute. To run multiple commands, use a multi-line string. <em>Use this key <strong>or</strong> <code class="language-plaintext highlighter-rouge">uses</code>, not both!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">uses</code></td>
<td>String</td>
<td>A valid specifier for a pre-defined action to execute with an optional but strongly recommended version number. See the documentation for <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses">descriptions of the supported specifiers</a>. You can find a <a href="https://github.com/marketplace?type=actions">listing of pre-defined actions in the GitHub market place</a>. <em>Use this key <strong>or</strong> <code class="language-plaintext highlighter-rouge">run</code>, not both!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">with</code></td>
<td>Dictionary</td>
<td>This key provides a mechanism for passing arguments to a predefined action included via the <code class="language-plaintext highlighter-rouge">uses</code> 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. <em>Only use this key with <code class="language-plaintext highlighter-rouge">uses</code>!</em></td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">environment</code></td>
<td>Dictionary (<strong>Optional</strong>)</td>
<td>A dictionary defining environment variables that will be made available within just this step.</td>
</tr>
</tbody>
</table>
<h2 id="worked-example-1--manually-publishing-a-webpack-bundled-web-app-on-github-pages">Worked Example 1 — Manually Publishing a WebPack-Bundled Web App on GitHub Pages</h2>
<p>As we learned in instalments <a href="./pbs138">138</a> & <a href="./pbs139">139</a>, 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:</p>
<table>
<thead>
<tr>
<th>File/Folder</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">src/*</code></td>
<td>The source code for our web app.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">docs/*</code></td>
<td>The built web app.</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">package.json</code></td>
<td>The NodeJS/NPM configuration for our web app.</td>
</tr>
</tbody>
</table>
<p>Rather than building a fresh example, we’ll reuse the final example web app from instalment <a href="./pbs139">139</a>.</p>
<h3 id="step-1--create-a-new-public-github-repository--clone-it-locally">Step 1 — Create a new Public GitHub Repository & Clone it Locally</h3>
<p>Log in to GitHub and <a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository">create a new repository in the usual way</a>. If you want to be able to experiment with GitHub Actions and GitHub Pages for <strong>free</strong> you’ll need to make it a <strong>public repository</strong>.</p>
<p>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.</p>
<h3 id="step-2--add-the-web-app-code--nodejsnpm-configuration">Step 2 — Add the Web App Code & NodeJS/NPM Configuration</h3>
<p>Extract the contents of the Instalment ZIP file and copy all the files and folders into your cloned repo. Rename the file <code class="language-plaintext highlighter-rouge">_gitignore</code> to <code class="language-plaintext highlighter-rouge">.gitignore</code> and then commit everything as the initial code. You’ll need to do that from the Terminal, as macOS prohibits this action.</p>
<h3 id="step-3--manually-build-the-app--commit-the-web-app">Step 3 — Manually Build the App & Commit the Web App</h3>
<p>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, <strong>make sure you have the latest NodeJS installed</strong> (or at least the latest LTS version)!</p>
<p>On the command prompt you have open in your cloned copy of the repo, start by initialising NodeJS with the command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm ci
</code></pre></div></div>
<p><em><strong>Reminder:</strong> this command does a ‘clean install’, downloading the exact versions of each package listed as required in <code class="language-plaintext highlighter-rouge">package.json</code> as recorded in <code class="language-plaintext highlighter-rouge">package-lock.json</code>.</em></p>
<p>Now, let’s build our web app with the command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build
</code></pre></div></div>
<p>This will build our web app using the source code in <code class="language-plaintext highlighter-rouge">/src</code> as specified by the Webpack configuration in <code class="language-plaintext highlighter-rouge">webpack.config.js</code> using the build command defined in <code class="language-plaintext highlighter-rouge">package.json</code> and publish it to <code class="language-plaintext highlighter-rouge">/docs/index.html</code>.</p>
<p>Open <code class="language-plaintext highlighter-rouge">/docs/index.html</code> 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.</p>
<p>Commit the changes and then push them up to GitHub.</p>
<h3 id="step-3--enable-github-pages">Step 3 — Enable GitHub Pages</h3>
<p>Log in to the GitHub web interface and open the repo’s GitHub Pages settings (<strong>Settings</strong> → <strong>Pages</strong>).</p>
<p>Change the <strong>Source</strong> dropdown to <strong>Deploy from a branch</strong>, choose <strong>main</strong> and the folder <strong>/docs</strong>, then <strong>Save</strong>.</p>
<p>After you save the settings, Choose <strong>Actions</strong> from the button bar to see all the actions on your repo. An action will have been created for you named <strong>pages build and deployment</strong>, and depending on how quickly you navigated, you will see its status as <em>queued</em> or <em>in progress</em>, 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.</p>
<p>Open the URL shown in the workflow output in your browser to verify that you have successfully published your web app to the internet!</p>
<h2 id="worked-example-2--automate-deployment-of-a-webpack-app-with-github-actions">Worked Example 2 — Automate Deployment of a Webpack App with GitHub Actions</h2>
<p>Now that we can publish our app manually, let’s automate it with GitHub Actions.</p>
<p>To do this, we’ll be making use of a collection of officially verified standard actions from the marketplace and using a <code class="language-plaintext highlighter-rouge">run</code> step to execute our two NPM commands (<code class="language-plaintext highlighter-rouge">npm ci</code> & <code class="language-plaintext highlighter-rouge">npm run build</code>).</p>
<blockquote class="notice">
<p>It’s important not to use an action from an untrusted author, so I always set the <strong>By</strong> filter for all my search results to <strong>Verified Creators</strong>.</p>
</blockquote>
<h3 id="step-1--update-github-pages-settings-to-use-a-github-actions-workflow">Step 1 — Update GitHub Pages Settings to Use a GitHub Actions Workflow</h3>
<p>Log back in to the GitHub web interface and open the GitHub Pages settings again. Change the <strong>Source</strong> dropdown to <strong>GitHub Actions</strong> and choose to create your own action. This will open a GitHub web editor with a default file as the content.</p>
<p>Enter a sensible filename with a <code class="language-plaintext highlighter-rouge">.yaml</code> extension, e.g. <code class="language-plaintext highlighter-rouge">deploy-app.yaml</code>.</p>
<p>For the file’s contents, enter the following</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build with Webpack and Publish to GitHub Pages</span>
<span class="na">on</span><span class="pi">:</span>
<span class="na">push</span><span class="pi">:</span>
<span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="s2">"</span><span class="s">main"</span> <span class="pi">]</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">permissions</span><span class="pi">:</span>
<span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout code</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup NodeJS</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v4</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">node-version</span><span class="pi">:</span> <span class="s">22.x</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build with Webpack</span>
<span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">npm ci</span>
<span class="s">npx webpack</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload generated files as artifact</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-pages-artifact@v3</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">docs/</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
<span class="na">permissions</span><span class="pi">:</span>
<span class="na">pages</span><span class="pi">:</span> <span class="s">write</span>
<span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">github-pages</span>
<span class="na">url</span><span class="pi">:</span> <span class="s">$</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to GitHub Pages</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/deploy-pages@v4</span>
</code></pre></div></div>
<p>This YAML code defines a <strong>workflow</strong> with two <strong>jobs</strong>, one with the id <code class="language-plaintext highlighter-rouge">build</code> and one with the id <code class="language-plaintext highlighter-rouge">deploy</code>. The workflow is triggered by pushing to <code class="language-plaintext highlighter-rouge">main</code> and both jobs are configured to run on Linux runners (<code class="language-plaintext highlighter-rouge">runs-on: unbuntu-latest</code>). Note that the <code class="language-plaintext highlighter-rouge">deploy</code> job forces itself to run after the <code class="language-plaintext highlighter-rouge">build</code> job with <code class="language-plaintext highlighter-rouge">needs: build</code>.</p>
<p>The <code class="language-plaintext highlighter-rouge">build</code> job executes <strong>steps</strong> to:</p>
<ol>
<li>Check out the code from the commit that triggered the workflow using the <a href="https://github.com/marketplace/actions/checkout">standard checkout action</a>.</li>
<li>Set up NodeJS version 22.x in the runner using the <a href="https://github.com/marketplace/actions/setup-node-js-environment">standard Node.JS Setup action</a>.</li>
<li>Build the web app by running the appropriate <code class="language-plaintext highlighter-rouge">npm</code> commands.</li>
<li>Upload the generated site to the GitHub Pages Servers as a so-called <em>artifact</em> using the <a href="https://github.com/actions/upload-pages-artifact">standard Upload Pages Artifact action</a>.</li>
</ol>
<p>With the generated files now uploaded as an artifact, the <code class="language-plaintext highlighter-rouge">deploy</code> job executes just one <strong>step</strong> to:</p>
<ol>
<li>Publish the uploaded artifact to the internet using the <a href="https://github.com/marketplace/actions/deploy-github-pages-site">standard Deploy Pages action</a>.</li>
</ol>
<h3 id="step-2--clean-up-and-test-the-automated-deployment">Step 2 — Clean up and Test the Automated Deployment</h3>
<p>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:</p>
<ol>
<li>
<p>Delete the <code class="language-plaintext highlighter-rouge">docs</code> folder.</p>
</li>
<li>
<p>Update line 27 of <code class="language-plaintext highlighter-rouge">src/index.js</code> so we’ll be able to see that the web app changed:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">#world-modifier</span><span class="dl">'</span><span class="p">).</span><span class="nx">text</span><span class="p">(</span><span class="dl">'</span><span class="s1">Automated Webpack</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div> </div>
</li>
</ol>
<p>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.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>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 <a href="https://beta.xkpasswd.net/">new JavaScript-based version of the XKPasswd secure memorable password generator</a> developed by members of the PBS community will be deployed when it comes out of beta in 2025!</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<div class="text-center mt-5">
<div class="mx-5 mb-1 text-center">
<div class="btn-group btn-group-sm w-100 px-5 text-center" role="group" aria-label="Miniseries Navigation">
<a href="/pbs175" class="btn btn-outline-secondary" role="button"><i class="far fa-arrow-alt-circle-left" aria-label="Previous"></i>
Static Site Generators
</a><a href="#" class="btn btn-outline-secondary disabled" role="button" aria-disabled="true">GitHub Pages Miniseries</a><a href="/pbs177" class="btn btn-outline-secondary" role="button">
Publishing A Basic Jekyll Site <i class="far fa-arrow-alt-circle-right" aria-label="Next"></i>
</a></div>
</div><div class="btn-group w-100 px-5 text-center" role="group" aria-label="Instalment Navigation">
<a href="/pbs175" class="btn btn-outline-primary" role="button"><i class="far fa-arrow-alt-circle-left" aria-label="Previous"></i> 175. Static Site Generators
</a><a href="/#pbs" class="btn btn-outline-primary" role="button">Programming by Stealth</a><a href="/pbs177" class="btn btn-outline-primary" role="button">177. Publishing A Basic Jekyll Site <i class="far fa-arrow-alt-circle-right" aria-label="Next"></i>
</a></div>
</div>
</main>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Rebuild your site locally, then open it in your browser. You should see an alert with a close button. Clicking the close button should dismiss the alert.
A Final GitHub Pages Check
As a final sanity check, commit these changes and push them to GitHub. Wait for the site to rebuild, and then verify that the colour change and alert dismissal are behaving the same in GitHub Pages as they are locally.
Final Thoughts
This instalment was about building a scaffolding to get us started. We’ve left a lot of proverbial mystery meat, which always makes me a little uncomfortable. But, we can now get a basic a basic Jekyll site with a custom theme using customisable Bootstrap up and running both locally and on GitHub Pages. This gives us the ability to develop our site off-line, and to publish updates to the internet as and when we think our work is in an appropriate state to do so.
We’re now ready to start adding our content and evolving our theme. The next few instalments are going to be a little bit challenging because we’ll need to use a little bit of many new technologies all at once. Inevitably there’ll need to be a little hand-waving about a few details initially, but, rest assured, all that detail will be filled in before we finish the series!