GitHub Actions as CI/CD for a site using a static generator and GitHub Pages

Habr Technical Original language: Russian

A historical walkthrough of using GitHub Actions to generate a Pelican website from a private repository and push the output to GitHub Pages, with Telegram notifications.

Translated by ChatGPT GPT-6.1 Sol

Original publication details

GitHub Actions как CI/CD для сайта на статическом генераторе и GitHub Pages

Archived publication. Roles, figures, and technical instructions reflect the original publication date.

After browsing Habr a little, I was surprised by how few articles had been published about GitHub’s (beta) feature, Actions.

You might explain that lack of coverage by saying the functionality is still being tested, even if it is in “beta.” But a useful feature of the beta is precisely that it lets you use this tool in private repositories. In this article, I will explain how to work with this technology.

Background

Starting from the beginning, I should probably mention that, while looking for a quick, convenient, lightweight, and free way to host a personal “About me” website, I spent several nights going through a great many articles.

Some choose hosting, others a cloud server. Those who do not want to figure out how all that works, interacts, and gets paid for tend to like uploading static websites to a repository. Fortunately, this can now be done on both GitHub and GitLab.

Of course, it is everyone’s personal choice.

My final choice was GitHub Pages.

About Pages

For those unfamiliar with it, gh-pages is a way to host documentation as a website, provided free of charge. Alongside documentation, it also offers hosting for personal websites. GitHub provides this functionality to all users, and it is available in repository settings.

For a project repository, the gh-pages branch is used; for a user website, a separate repository named username.github.io, with the website’s sources in the master branch.

You can read more in the documentation, but I will simply note that GitHub, with surprising generosity, lets everyone attach their own domain to such a site by adding a CNAME file containing the domain name and pointing their domain provider’s DNS to GitHub’s servers.

I am sure there are plenty of articles here on deploying such a site, so that is not what follows.

How the problem arose

The problem was that using a static generator requires writing additional kludges scripts and using libraries to simplify generating pages and uploading them to a repository. Put simply, if you keep the sources in a separate private repository, every change to the site requires setting up a local environment to generate static pages and publish them to the main website repository.

There are plenty of static generators, and they all have the same problem. These actions take too much time and effort and ultimately stall work on the site, especially after several migrations between operating systems or incidents involving lost hard-drive data—(as happened in my case).

Just recently, either in a pop-up notification on the website or in an email from GitHub, I noticed a newly integrated CI/CD feature that made it possible to carry out these actions with minimal effort.

About static page generators

I will not dwell on this subsection, but I will share a couple of points I arrived at while choosing and using them:

  1. Choose a generator in your own programming language, or one you understand as well as possible. I came to this idea when I had to add functionality for the site myself and put in workarounds to improve its stability and automation. It is also a good reason to write additional functionality yourself as plugins;

  2. Which generator to settle on is a personal choice, but bear in mind that, for an initial introduction to GitHub Pages functionality, you first need to install Jekyll. Fortunately, it lets you generate a site from sources directly in the repository—(which I will replicate with my choice).

My choice of generator is based on the first point. Pelican, written in Python, easily replaced Jekyll, which was unfamiliar territory for me—(I used it for almost a year). As a result, even creating and editing articles and working on the site provides additional experience in a language I find interesting.

__

Defining the task

The main task is to write a script—actually a configuration file—that would automatically generate static pages from a private repository. The solution will use a virtual environment’s capabilities. The script itself will add the completed pages to a public repository.

Tools for the solution

The tools we will use to solve the task:

  • GitHub Actions;

  • Python 3.7;

  • Pelican;

  • Git;

  • GitHub Pages.

Solving the problem

After getting somewhat acquainted with the documentation and figuring out how Actions scripts are written, it became clear that this mechanism would completely solve the problem. At the time of writing, using this functionality requires signing up for beta testing!

GitHub’s own description of the new functionality

Writing an Actions script starts with creating a named file in the .github folder and its workflows subfolder. You can do this manually or in the editor in the Actions tab on the repository page.

An example of a blank script template

Brief comments on the template

name: CI    # название скрипта: будет отображаться во вкладке Actions

on: [push]  # действие, по которому запускается данный скрипт

jobs:       # роботы, которые будут выполняться
  build:    # сборка, которая..

    runs-on: ubuntu-latest      # ..будет запущена на основе этого образа

    steps:              # шаги которые будут проделаны после запуска образа
    - uses: actions/checkout@v1     # переход в самую актуальную ветку
    - name: Run a one-line script   # имя работы номер 1
      run: echo Hello, world!       # суть работы номер 1 (bash-команда записана в одну строку)
    - name: Run a multi-line script   # имя работы номер 2
      run: |                    # суть работы номер 2 (многострочная)
        echo Add other actions to build,
        echo test, and deploy your project.

Let’s write our own based on the template:

  1. You can leave the name as “CI.” It is a matter of taste.

  2. Next, choose the action or trigger that will launch the script. In our case, this is simply pushing a new commit to the repository.

on:
  push
  1. We will also keep the image on which the script runs from the example, since Ubuntu fully satisfies our functional needs. Looking at the available tools, it becomes clear that this can be any image we need or simply find convenient, or a Docker container based on it.
  build:
    runs-on: ubuntu-latest
  1. In the steps, we first configure the environment in preparation for the main work.

3.1) Switch to the branch we need—the standard checkout step:

- uses: actions/checkout@v1

3.2) Install Python:

    - name: Set up Python
      uses: actions/setup-python@v1
      with:
        python-version: 3.7

3.3) Install our generator’s dependencies:

    - name: Install dependencies
      run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

3.4) Create the directory where the website pages will be generated:

   - name: Make output folder
      run: mkdir output
  1. To make work on the site continuous—without deleting previous changes and while allowing changes to be added to the website repository without conflicts—the next step will clone the website repository each time:
   - name: Clone master branch
      run: git clone "https://${{ secrets.ACCESS_TOKEN }}@github.com/${GITHUB_ACTOR}/${GITHUB_ACTOR}.github.io.git" --branch master --single-branch ./output

This step uses system variables:

  • GitHub sets GITHUB_ACTOR itself; it is the username of the person responsible for triggering the script;

  • secrets.ACCESS_TOKEN is a generated token for managing GitHub. We can pass it as an environment variable by setting it in the Secrets tab of our repository settings. Please note that when generated, the token is shown to us only once; it will not be accessible again. The same applies to the values in Secrets.

  1. Move on to generating our pages:
   - name: Generate static pages
      run: pelican content -o output -s publishconf.py

The parameters passed to the generator specify the directory where generated files will go (-o output) and the configuration file used for generation (-s publishconf.py; you can read about separating local configuration from publication configuration in the Pelican documentation).

Remember, we have already cloned the website repository into the output folder.

  1. Configure git and stage our modified files:
    - name: Set git config and add changes
      run: |
          git config --global user.email "${GITHUB_ACTOR}@https://users.noreply.github.com/"
          git config --global user.name "${GITHUB_ACTOR}"
          git add --all
      working-directory: ./output

This step uses the variable we already know and specifies the working directory in which its commands will run. Otherwise, the command for switching to that directory would look like cd output.

  1. Generate a commit message, commit the changes, and push them to the repository. To avoid an empty commit, and therefore a bash error—a result other than 0—we first check whether there is anything to commit and push at all. For this, we use git diff-index --quiet --cached HEAD --, which returns 0 in the terminal if there are no changes compared with the previous version of the website, and 1 if there are changes. We then handle that command’s result. This way, we record useful information about the site’s state at this stage in the script’s execution information, instead of automatically failing and sending us a failure report.

We also carry out these actions in our directory containing the completed pages.

   - name: Push and send notification
      run: |
          COMMIT_MESSAGE="Update pages on $(date +'%Y-%m-%d %H:%M:%S')"
          git diff-index --quiet --cached HEAD -- && echo "No changes!" && exit 0 || echo $COMMIT_MESSAGE
          # Only if repo have changes
          git commit -m "${COMMIT_MESSAGE}"
          git push https://${{ secrets.ACCESS_TOKEN }}@github.com/${GITHUB_ACTOR}/${GITHUB_ACTOR}.github.io.git master
      working-directory: ./output

The result

Ultimately, this script lets you stop thinking about generating static pages. Add changes directly to the private repository, whether by working with git on any system or creating a file through GitHub’s web interface, and Actions will do everything themselves. If the script fails unexpectedly, an email notification will arrive.

Full code

Here is my working version. Its final step adds a notification that the commit has been pushed to the main repository.

It uses the Secrets described above, with the bot token and the identifier of the user who should receive the message added.

name: Push content to the user's GitHub pages repository

on:
  push

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Set up Python
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Install dependencies
      run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
    - name: Make output folder
      run: mkdir output
    - name: Clone master branch
      run: git clone "https://${{ secrets.ACCESS_TOKEN }}@github.com/${GITHUB_ACTOR}/${GITHUB_ACTOR}.github.io.git" --branch master --single-branch ./output
    - name: Generate static pages
      run: pelican content -o output -s publishconf.py
    - name: Set git config and add changes
      run: |
          git config --global user.email "${GITHUB_ACTOR}@https://users.noreply.github.com/"
          git config --global user.name "${GITHUB_ACTOR}"
          git add --all
      working-directory: ./output
    - name: Push and send notification
      run: |
          COMMIT_MESSAGE="Update pages on $(date +'%Y-%m-%d %H:%M:%S')"
          git diff-index --quiet --cached HEAD -- && echo "No changes!" && exit 0 || echo $COMMIT_MESSAGE
          git commit -m "${COMMIT_MESSAGE}"
          git push https://${{ secrets.ACCESS_TOKEN }}@github.com/${GITHUB_ACTOR}/${GITHUB_ACTOR}.github.io.git master
          curl "https://api.telegram.org/bot${{ secrets.BOT_TOKEN }}/sendMessage?text=$COMMIT_MESSAGE %0ALook at ${GITHUB_ACTOR}.github.io %0ARepository%3A github.com/${GITHUB_ACTOR}/${GITHUB_ACTOR}.github.io&chat_id=${{ secrets.ADMIN_ID }}"
      working-directory: ./output

Screenshots

The result of one run, displayed in the Actions tab of the source repository

A message from the bot about the script’s completion

General information about Actions
Actions syntax
List of triggers
Virtual environment options
GitHub Pages
Static Generator list