Deploying Course Websites with git hooks.

Now that I’ve moved my course websites to Hugo via ox-hugo, life is definitely easier than when I used WordPress, and my teaching is unquestionably crawling a little bit closer to actual reproducibility. It’s been non-trivial to figure out a good deployment process, though. My course materials live in a slightly broader context than my websites: I have my readings, my private notes, my grades, and a few other documents like teaching contracts, etc., in the main course directory. Some but not all of these materials can be committed to Git, so it makes intuitive sense to me to have a course repository which exists independently of the website. If I didn’t use ox-hugo to manage the org-to-hugo process – if, say, I used the somewhat more limited googrgeous support which is built into Hugo – I might try to build the website directly from the course materials. Instead, I’ve come up with a somewhat more convoluted process. It’s by no means perfect, and there are probably better solutions , but for now this process is working pretty well for me. Documenting here in case it’s useful to anyone else, and also in case I forget what I’ve done!

For this post I’ll take my Digital History course as an example, but I’m hoping to move all my courses to the same system, semester by semester.

Repository setup

Each course has two repositories:

  • a main repo, mostly for my own use, but potentially of use to colleagues. These sites are small, with one file per content type – usually one each for syllabus, assignments, lectures, and some kind of “other” category.
  • a website repo, containing one file per webpage – so instead of a single Assignments.org, an entry for each individual assignment. These are markdown files created by org-hugo-export-subtree.

The website repo has two relevant branches: a master branch that contains the uncompiled source files, and a gh-pages branch that holds the Hugo-generated files that are actually served on the server. The gh-pages branch is checked out as a git worktree as described in the Hugo docs. It took me a little while to figure this out, but once you understand the concept of git worktrees it’s kind of fun to do things this way.

For Digital History, I’m using the excellent docdock theme, and as they suggest, I’ve installed the theme as a git submodule.

Workflow

I do all my editing inside Emacs, in the original org-mode files that live in the “main” repo. Usually changes belong to one of two categories:

  • “features” – planned updates to assignments, course materials, due dates, etc.
  • “hotfixes” – corrections to errors, omissions, duplications in the course materials. I have to do these more often than I’d like to admit!

In either case, the workflow should look like this:

  • make the change in source (.org) text
  • export change to Hugo
  • inspect changes on local server
  • if changes look good, commit them to master in the main site
  • commit those changes to the web master
  • run hugo -s in the website master dir; commit changes to gh-pages branch; push gh-pages to upstream.

There are a lot of steps there! Fortunately we can automate or semi-automate them.

I’ve left the first two as manual stages, because it’s smart to check changes before committing. However, I now make it a little easier by following Kaushal’s advice to run hugo server with the -p and --navigateToChanged switches. I now keep a pinned tab up in Firefox at localhost:xxxx (choose your own port with -p), and as soon as I export a page, that tab will update to the appropriate page. Fantastic! Makes checking my work much quicker. As for the rest, those steps are all done via 2 git post-commit hooks, one for each repo:

  • in the main repo, I have a post-commit hook that checks if the commit was made on the master branch, and if so, then grabs the commit message & commit hash, then cd’s to the website directory & creates a new commit on master with references to the original commit.
  • in the web repo, I have a second post-commit hook, which again checks if we’re on master, and if so, generates the website in the public directory (where we have the gh-pages site checked out as a git subtree), cd’s there, and commits to the gh-pages brnach. Then both branches are pushed to gh-pages
  • I’m in a transitional period where I’m moving from self-hosted sites to gh-pages where possible. So I actually regenerate the site in another directory using hugo server -s -d myserver; I also have to update config.toml before doing this to fix a couple of paths. This is all pretty easy to do.

Here’s the first, somewhat simpler script from the main repo:

push_branch=master
cur_branch=$(git rev-parse --abbrev-ref HEAD)
cur_hash=$(git log -1 --pretty=%h)
cur_msg=$(git log -1 --pretty=%B)
web_dir=dh-website
cur_dir=$PWD

# only run if commit is to master

if [ "$cur_branch" ==  "$push_branch" ]; then
    # if [ "$(git status -uno --porcelain)" ]; then
    #   echo "uncommitted  changes, can't push";
  # else
  # cd to website
  cd $web_dir
  # commit hcanges
  echo "committing changes to  website master branch\n\n"
  git commit -a -m "push changes to website from titaniumbones/Digital-History#$cur_hash

$cur_msg" 
  cd $cur_dir
  echo "pushing master to origin"
  git push -f origin master
fi

And the slightly more baroque version from the website repo, which pushes to both gh-pages and to hackinghistory (shimano is just an alias for hackinghistory.ca).

Note that I had to set up the hackinghistory repo as a non-bare repo with receive.denyCurrentBranch set to updateInstead. Thank you, stackoverflow!.

push_branch=master
cur_branch=$(git rev-parse --abbrev-ref HEAD)
cur_hash=$(git log -1 --pretty=%h)
cur_msg=$(git log -1 --pretty=%B)



# only run if commit is to master

if [ "$cur_branch" ==  "$push_branch" ]; then
    if [ "$(git status -uno --porcelain)" ]; then
      echo "uncommitted  changes, can't push";
    else
      echo "pushing master to origin"
      git push -f origin master
      echo "Rebuilding Site"
      hugo -s ./
      cd public &&
        echo "changing env variables" &&
        GIT_WORK_TREE="../.git/worktrees/public" &&
        GIT_INDEX_FILE="../.git/worktrees/public/index"&&
        echo "adding all files\n\n"  &&
        git add --all
      echo "commitchanges to  gh-pages\n\n"
        git commit -a -m "push changes to gh-pages from $cur_hash

$cur_msg" 
        echo "pushing gh-pages to origin"
        git push -f origin gh-pages && echo "successfully pushed gh-pages"
      cd ..

      sed -i.gh 's/baseURL = \"https:\/\/digitalhistory.github.io\/dh-website\/\"/baseURL = \"https:\/\/digital.hackinghistory.ca\/\"/' config.toml
      hugo -s ~/DH/dh-website/ -d ~/DH/dh-website/shimano/

      # now shimano dir, shimano-deploy branch, shimano remote
      cd shimano &&
        echo "changing env variables" &&
        GIT_WORK_TREE="../.git/worktrees/shimano" &&
        GIT_INDEX_FILE="../.git/worktrees/shimano/index"&&
        echo "adding all files\n\n"  &&
        git add --all
      echo "commit changes to shimano\n\n"
      git commit -a -m "push changes to shimano from $cur_hash

$cur_msg" 
      echo "pushing shimano-deploy to shimano"
      git push -f shimano shimano-deploy && echo "successfully pushed shimano"
      cd ..

      # rsync -azvbP ~/DH/dh-website/shimano/ shimano:/var/www/digital.hackinghistory.ca/
      mv config.toml.gh config.toml
    fi    
fi