Automating the Build Process with Travis-CI

Updated 9th December to illustrate latest iteration of Travis.yml

Before I ported this site over to Gatsby, it was powered by good old Jekyll. I’d make an edit, run jekyll build, then push the output to S3 with s3_website. Not exactly onerous, but I noticed that I’d be drafting posts, or updating the structure of the site and then not pushing them live in a timely manner. (And what do you know, it’s happened again! – Ed., December 2nd)

Cue a Continuous Integration (CI) solution – although it took me many attempts to get it right. And by right, I mean it at least:

  • builds the site
  • deploys to S3
  • invalidates the CloudFront distribution.

Attempt #1: Use the default S3 provider

Travis’ documentation is pretty, pretty, pretty good. I think you could just about get away with this, if you’re happy to serve straight from the S3 bucket:

language: node_js
node_js:
 - "8"
script:
 - yarn build
deploy:
  provider: s3
  access_key_id: $AWS_ACCESS_KEY_ID
  secret_access_key: $AWS_SECRET_ACCESS_KEY
  bucket: danmatthew.co.uk
  region: $AWS_DEFAULT_REGION
  # Prevent Travis from deleting your built site so it can be uploaded.
  skip_cleanup: true
  # Path to a directory containing your built site.
  local_dir: public
  on:
    branch: master

Because I have both package.json and yarn.lock in my repository, Travis is smart enough to figure I want to use Yarn to run during the install phase.

I like this implementation, but Travis is only concerned with S3 here. I have a Cloudfront distribution sat in front of this bucket in the hope of saving a few pence and making the site feel snappy. Sure, I could manually log in to AWS and invalidate the distribution or do it via the command line, but y’know: let’s automate it.

Attempt #2: Use AWS CLI to invalidate the Cloudfront distribution

This is where it feels messy, and there are a bunch of extra steps to mess up. AWS CLI is installed via Pip, so this implementation needs to be driven by Python.

language: python
python: 3.6
sudo: required
dist: trusty
env:
 - TRAVIS_NODE_VERSION="8"

I had to explicitly specify the version of Python, because the default appeared to be 2.4 and I ran into issues… Java-related, possibly?.

This is where it starts to be a blend of different tasks, and I’m piecing together instructions from various sources.

Install Yarn

before_install:
  - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
  - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
  - sudo apt-get -qq update
  - sudo apt-get -y install yarn

Install NVM

As it’s not provided by default, I need to install Node and NPM in order to install my site’s dependencies and then build it. I figured NVM would be the most sensible approach:

install:
 - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION
 - pip install awscli

I discover afterwards that the ugly $TRAVIS_NODE_VERSION environment variable could have been avoided by specifying the version in the project’s .nvmrc file.

As the Travis cycle goes install, script, deploy, next up is…

before_script:
  npm install
script:
  npm run build

(Travis experts – could those commands just have been listed sequentially in the script block?).

Configure S3 bucket

We use the same deploy block as before:

deploy:
  provider: s3
  access_key_id: $AWS_ACCESS_KEY_ID
  secret_access_key: $AWS_SECRET_ACCESS_KEY
  bucket: danmatthew.co.uk
  region: $AWS_DEFAULT_REGION
  # Prevent Travis from deleting your built site so it can be uploaded.
  skip_cleanup: true
  # Path to a directory containing your built site.
  local_dir: public
  on:
    branch: master

Blow away the old Cloudfront distribution

When this succeeds, we can now use the AWS CLI to ensure that Cloudfront serves the latest version of these files:

after_deploy:
 # Allow `awscli` to make requests to CloudFront.
 - aws configure set preview.cloudfront true
 # Invalidate every object in the targeted distribution.
 - test $TRAVIS_BRANCH = "master" && aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*"

Inspecting the bucket, I notice that Travis doesn’t doesn’t sync the files – i.e. only new files are added; old ones are deleted. It might not be an issue in practice, but I quite like keeping my buckets tidy… 😒

Next!

Attempt #3: Use s3_website

I mentioned in the intro that when publshing the website from my computer I used the s3_website gem. I know it, it does all the tasks I want it to, and it’s straightforward to configure.

(Although, I fib: since upgrading to Sierra, I’ve not been able inclined to deploy since it requires a Java installation which I really don’t want to do. I know, weaksauce.)

So, we go again! Now, we’re sacking off a Python-based deployment pipe in favour of a Ruby powered process:

language: ruby
rvm: 2.2
sudo: required
dist: trusty
# Again with the redundant env var
env:
 - TRAVIS_NODE_VERSION="8"
before_install:
  - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
  - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
  - sudo apt-get -qq update
  - sudo apt-get -y install yarn

Install s3_website

install:
  - . $HOME/.nvm/nvm.sh
  - nvm install stable
  - nvm use stable
  - gem install s3_website

Push to S3

before_script:
 - yarn
script:
 - yarn build
after_script:
 - test $TRAVIS_BRANCH = "master" && s3_website push

Boom. All my s3_website settings are configured in environment variables and my s3_website.yml config, so that one line in the after_script block pushes to the correct bucket and invalidates the Cloudfront distribution.

Looking back over it, I think I’d be quite happy to sack off the yarn installation and reply on NPM. Again I wonder whether the blocks can be consolidated into one and the commands listed sequentially? I’m not experienced enough with Travis to know what benefits I get from splitting the commands into the different stages, especially since this particular deployment process is not exactly complicated. Perhaps so the process can fail earlier if necessary?

Proposed attempt #4: Use AWS CLI for everything

I suspect I could probably do all of the above with the AWS CLI, though it will need switching back over to Python again. Woe is me 😩.

Attemp #5: Concise

…whereupon I decided to sack off Yarn, and after configuring Travis to build PR and branch merges, whitelist the master branch:

branches:
  only:
    - master
language: ruby
rvm: 2.2
sudo: required
dist: trusty
cache:
  - npm
install:
  - . $HOME/.nvm/nvm.sh
  - nvm install stable
  - nvm use stable
  - gem install s3_website
before_script:
  - npm install
script:
  - npm run build
after_script:
  - s3_website push