Once of the fantastic features of git is that it’s decentralized. This means I can keep my CI (Continuous Integration) workflow without requiring internet nor extra services - just git and Jekyll.

I would say this is the closest thing you can get to the KISS (Keep It Simple, Stupid) principle. Optimally I would be building the site in a Docker container as well, but as I am the only developer on the site, I won’t bother with that type of work.

As I mentioned in my previous post, I would be using the hooks functionality of git; more specifically, the pre-commit and post-commit hooks.

Tackling the issues

Certainly the largest hurdle to getting this on the road was deciding how to make sure nothing gets deleted, but at the same time it is not massively inconvenient.

Sometimes, the best solution is the simplest one. I hardcoded 2 lists of files: ones that would always be updated (like /feed.xml), and ones that should never be touched (like /code).

Any file not matching either of these would use rm -i, where -i stands for interactive - meaning that it would ask me for permission for every file imaginable.

Unfortunately the potential interactivity makes it unsuitable for use in scripting (unless you like to live dangerously), but it was not something I was worried about.

The hooks

For a short period of time, I was hoping to use a branch to push content rather than ‘hacking’ around a post-commit solution.

To my surprise, using git branches to deploy without pushing does not seem like a possible solution - or at least not how I see it.

The big showstopper for the branches was covering fast-forwarded branches. This is normally a desirable solution, because it keeps the history clean and devoid of merge commits.

It turns out that the hook associated with fast-forwarding is a checkout.

Since so many other things are also checkouts (basically anything that writes a file to disk), this was no longer a viable idea.

So, scrapping that idea, I ended up using an environment variable with the default being to not push. Worst case, I forget to set the variable and I have to do a git reset --soft HEAD^ and then recommit to deploy.

.git/hooks/post-commit

#!/bin/bash

ALWAYSDELETE=("404" "about" "assets" "blogs" "feed.xml" "index.html" "robots.txt" "speedruns")
NEVERDELETE=("code")
HOSTPATH=/srv/http/gonx.dk

if [ ! -z $PUSH_ON_COMMIT ]; then
    # build
    cd site
    jekyll clean -q || exit 1
    jekyll build --strict_front_matter -q || exit 1

    # clean destination
    for x in ${HOSTPATH}/*; do
        x="$(basename "$x")"

        unset SKIP NOWARN

        # iterate through "blacklist" array and skip this file if we have a match
        for y in ${NEVERDELETE[@]}; do
            if [ "a$x" == "a$y" ]; then
                SKIP=true
            fi
        done

        [ ! -z $SKIP ] && continue

        # iterate through "whitelist" array - files that get deleted without confirmation
        for y in ${ALWAYSDELETE[@]}; do
            if [ "a$x" == "a$y" ]; then
                NOWARN=true
            fi
        done

        if [ ! -z $NOWARN ]; then
            rm -r "${HOSTPATH}/${x}" || RMERROR=true
        else
            rm -ri "${HOSTPATH}/${x}" || RMERROR=true
        fi
    done

    # deploy
    cd _site
    cp -r * "${HOSTPATH}" || echo "copy of _site to '$HOSTPATH' failed"

    # exit sensibly
    if [ ! -z $RMERROR ]; then
        exit 1
    else
        echo "Changes pushed, PUSH_ON_COMMIT was set"
        exit 0
    fi
else
    echo "No changes pushed, PUSH_ON_COMMIT is unset"
fi

Nothing out of the ordinary here. I chose to use --strict_front_matter because I prefer having warnings as errors during deployments.

You could potentially drop the build files directly into the webroot from Jekyll, but I decided not to in case the build process throws an error.

.git/hooks/pre-commit

#!/bin/bash

if hash jekyll 2>&1 ; then
    tmpdir=$(mktemp -d)
    jekyll b -s site -d "$tmpdir" -q
    rv=$?
    if [ $rv -eq 0 ]; then
        echo "OK. Build passed without errors."
    else
        echo "ERROR. jekyll returned build errors, see above. Your commit was rejected."
    fi

    rm -rf "${tmpdir}"
    [ -d ".sass-cache" ] && rm -r .sass-cache

    exit $rv
else
    echo "'jekyll' not in PATH, skipping pre-commit check"
fi

Again, nothing too exciting here.

I used a temporary build dir since I wasn’t too sure what would happen in case jekyll serve was already running.

Conclusion

I feel like not going for high-end solutions such as buildbot and Jenkins was worth it in my case. I ended up with no resident services and essentially the smallest possible footprint using BASH.

I hope that satiated your thirst for some code and problem solving. This is the first of many styles of posts where I will be covering a technical problem with some real-world code examples.