Improving UX with Makefiles

Recently I updated the code for this website to use Makefiles and rely less on one-off shell scripts. The reason for this change was to improve my write-test-release workflow. I figured make is available on all of my computers and it serves as a perfect minimal orchestration tool. I now type make deploy instead of ./deploy.sh to push a new version of my website up to production. While this may not seem like a lot, it reduced the number of files in my directory.

typicalrunt.me$ ls *.sh
build.sh
deploy.sh
notify.sh
s3-sync.sh
server.sh

The files are named after their purpose which is good, but I want to remember less files in my project, since most of these files are simple one-liners. For instance, this is build.sh:

#!/bin/bash

bundle exec middleman build

I originally wrote this because I didn't want to type out the entire bundle exec sequence everytime I rendered my website to static files.

When I decided to move to Makefiles, I have a simple interface that takes each of those one-liners and moves it into a Makefile target. Thus, the above build.sh file becomes in make:

build: deps
    @bundle exec middleman build

What is that deps target? It ensures that the system requirements are met for running the bundle command. Since I run Bundler in multiple scripts, this means I can DRY up the dependency checks inside the Makefile, instead of duplicating it among the shell scripts.

Here is my completed Makefile:

.DEFAULT_GOAL := help
.PHONY: build test deploy deps help

AWS_BIN := aws
BUNDLE_BIN := bundle
CURL_BIN := curl

BUCKET := REDACTED

SERVER := api.hipchat.com
ROOM_ID := REDACTED
URL := https://$(SERVER)/v2/room/$(ROOM_ID)/notification?auth_token=$(HIPCHAT_TOKEN)

deps:
  @hash $(AWS_BIN) > /dev/null 2>&1 || \
    (echo "Install aws to continue."; exit 1)
  @hash $(BUNDLE_BIN) > /dev/null 2>&1 || \
    (echo "Install bundler to continue."; exit 1)
  @hash $(CURL_BIN) > /dev/null 2>&1 || \
    (echo "Install curl to continue."; exit 1)
  @test -n "$(HIPCHAT_TOKEN)" || \
    (echo "HIPCHAT_TOKEN env must be set"; exit 1)

help:
  @echo "Builds, tests, and deploys the static website files"
  @echo ""
  @echo "Targets:"
  @echo "  build     Renders the static website"
  @echo "  help      This message"
  @echo "  deploy    Uploads static website to S3"
  @echo "  deps      Ensures the system requirements are met"
  @echo "  test      Starts a local server to view static website"

build: deps
  @$(BUNDLE_BIN) exec middleman build

test: deps
  @$(BUNDLE_BIN) exec middleman server

deploy: deps
  @$(AWS_BIN) s3 sync build/ s3://$(BUCKET)
  @$(CURL_BIN) -X POST \
       -d @notification.json \
       --header "Content-Type:application/json" \
       $(URL)

With this I now have a simple UI to building, testing, and releasing new website articles:

typicalrunt.me$ make help
Builds, tests, and deploys the static website files

Targets:
  build     Renders the static website
  help      This message
  deploy    Uploads static website to S3
  deps      Ensures the system requirements are met
  test      Starts a local server to view static website

This also allowed me to remove all of the shell scripts and replace it with one Makefile, making an awesome commit:

typicalrunt.me$ git rm *.sh
typicalrunt.me$ git add Makefile
typicalrunt.me$ git commit -m 'Makefiles FTW!'

Makefiles aren't for everyone or every situation, but when you consider the cost of maintaining separate one-liner shell scripts, you might find that using a simple build tool provides better UX and less cognitive load.