A redone blog, again

I'm just about due for another recreation of my blog, so here we are. Last time, I switched from a CMS to a statically generated website, using my own static site generator. This time, I'm taking it a step further and, in addition to recreating my static site generator in .NET Core, harnessing the power of GitHub Actions for CI/CD purposes, and I'm using GitHub Pages for hosting.

My new static site generator isn't ready for a proper first release yet, so I won't talk much about it at this point. Instead, I'll focus on how I'm using GitHub Actions to deploy blog changes to my staging blog and my production blog. First things first: GitHub Actions is currently in beta. I wouldn't be surprised if it goes through a lot of changes before it's released fully. While the beta is mostly functional, it has a number of limitations and bugs. That's beta software for you. What I'm trying to get at is, the things I'm going to describe below might not be applicable by the time this feature is out of beta.

The idea behind GitHub Actions is relatively straightforward: you configure "actions", which are simply Docker containers, to do stuff for you. During the public beta, the only available trigger for these actions is a push. That's good enough for my purposes with this blog. Having never really worked with Docker before, I'm probably doing a lot of stuff in a suboptimal manner, but it works, so there's that. I've got two sections in my GitHub Actions workflow file: the staging section, and the production section. They are very similar, with just a few changes to point to different repos and such.

Each section has two actions: a filter and a "do all the things" action. The filter simply ensures that the main action only executes when something is pushed to the correct branch - it's a branch filter. The setup for it looks like this:

    action "Filter to master branch" {
        uses = "actions/bin/filter@b2bea07"
        args = "branch master"

The uses directive says that this version of the Filter action should be used, with args telling the Filter action that I want the branch name to match "master".

The "do all the things" action is where it gets weird. I didn't want to publish my own GitHub Action for building a static site using my generator for a few reasons. First, as I mentioned earlier, my generator isn't ready yet. Second, I was having trouble finding the right documentation for precisely how to publish a GitHub Action, what the necessary components are, and how everything works together. And third, I fully expect the publishing process and requirements to change during the beta period, and I'm not overly interested in keeping up with all of these changes at the moment. So, I chose to do this in a bit of a backwards way. I decided to use the Docker CLI GitHub Action to build a Docker image that just happens to perform all the steps I want during the build. It's mildly horrifying, but here's what it currently looks like:

    action "Docker-Staging" {
        uses = "actions/docker/cli@76ff57a"
        needs = ["Filter to master branch"]
        args = "build --build-arg GITHUB_TOKEN --build-arg GH_SOURCE_REPO=\"arktronic/arktronic.com--source\" --build-arg GH_SOURCE_BRANCH=\"master\" --build-arg GH_DEST_REPO=\"arktronic/staging.arktronic.com\" --build-arg GH_DEST_DEPLOY_KEY --build-arg GH_CNAME=\"staging.arktronic.com\" --build-arg ACCEPT_RISK=\"1\" .github/main_support"
        secrets = ["GITHUB_TOKEN", "GH_DEST_DEPLOY_KEY"]

Let's dissect that mess. First, we have the familiar uses directive for this action. The needs directive helps to set up the chain of actions in my workflow. The secrets directive tells GitHub which secret environment variables to make available to this action (there is a new tab in GitHub repo settings that lets you manage these secrets). Finally, args is the messy part here. It's mostly build arguments, so if you ignore those, you're left with build .github/main_support. That's not so bad. It's just telling Docker to use the Dockerfile located in the specified relative path. The Dockerfile located in .github/main_support looks like this:

    FROM microsoft/dotnet:2.1-sdk


    COPY "entrypoint.sh" "/entrypoint.sh"
    RUN /entrypoint.sh

As you can see, I'm using Microsoft's .NET Core 2.1 SDK Docker image to build and run my static site generator. After that, there are a whole bunch of ARG and ENV directives, which take all those --build-arg arguments from the GitHub Actions workflow and transform them into environment variables that are available to the container. Finally, a single shell script is copied into the container and then executed. That shell script is where most of the magic happens:


    set -e

    cd /srv
    mkdir ~/.ssh
    echo -e "$GH_DEST_DEPLOY_KEY" >> ~/.ssh/id_rsa 2>/dev/null
    echo -e "$GH_PROD_DEPLOY_KEY" >> ~/.ssh/id_rsa 2>/dev/null
    chmod 600 ~/.ssh/id_rsa
    ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts 2>/dev/null

    if [ "$ACCEPT_RISK" == "1" ]; then
    echo "Risk accepted - will force push!" >&2
    export PUSH_PARAMS="-f"
    echo "Risk not accepted - will perform dry run." >&2
    export PUSH_PARAMS="-n -f"

    git config --global user.name "BuildBot"
    git config --global user.email "noreply@example.com"

    git clone https://github.com/arktronic/genmaicha.git /srv/genmaicha
    dotnet publish -c Release -o /srv/genmaicha/publish /srv/genmaicha/Genmaicha/Genmaicha.csproj

    echo "Shallow cloning $GH_SOURCE_REPO, branch $GH_SOURCE_BRANCH"
    git clone --depth 1 --branch $GH_SOURCE_BRANCH https://$GITHUB_TOKEN@github.com/$GH_SOURCE_REPO.git input &>/dev/null
    dotnet genmaicha/publish/genmaicha.dll -o input/

    cd input/_build
    echo $GH_CNAME >CNAME
    git init
    git checkout -b master
    git add -A
    git commit -m "Recreate GitHub Pages"
    git remote add origin git@github.com:$GH_DEST_REPO.git
    echo "Pushing to $GH_DEST_REPO with params '$PUSH_PARAMS'"
    git push $PUSH_PARAMS origin master

    echo Done

The set -e line tells Bash to stop executing the script after the first time it encounters a non-zero exit code, i.e., after the first error. This is important because otherwise we could potentially end up deploying something invalid.

The next few lines set up SSH. There are two lines that try to throw a private key into ~/.ssh/id_rsa, but realistically, only one of them should succeed, since the workflow is set up to provide either the staging one or the production one - not both. That file then needs its permissions adjusted because SSH doesn't like it when private keys are world-readable. Go figure. And finally, in order to have SSH trust GitHub, its public key is retrieved and pushed to the known_hosts file.

After setting up SSH, I've got some risk avoidance code, which will either let Git force push, or merely perform a dry run (-n). Better safe than sorry. And then the local Git user info is set up for the eventual commit that will be created and force pushed.

Building my static site generator just takes a couple of lines: cloning its repo, and executing dotnet publish on it. After that, the blog source code is shallow cloned and then processed by the newly-built generator, with the output going into the _build directory.

Finally, a new Git repo is created in the _build directory, everything is committed, and the contents are then force pushed to the target repo, which should already be set up to use GitHub Pages for hosting.

All of this code (and, incidentally, this blog post) is currently located here. Feel free to take a look at it. And I would definitely recommend for people to check out GitHub Actions because it's an extremely powerful tool, which can be utilized for many useful purposes.

comments powered by Disqus