Building a Continuous Integration Pipeline for Ghost Themes Using Git and Codeship

One of the best things about the Ghost blogging platform is the impressive flexibility in development and deployment options. Of course, with that flexibility comes a commitment to doing a lot of things yourself. I recently configured a Codeship Continuous Integration pipeline for deploying changes to a Ghost theme I maintain, and while it was fairly straightforward, I'm documenting the process for posterity. Feel free to skip over all the CI steps if you're only interested in using Git push to update a theme on a remote server.

Even though I'm the only developer working on this project—I know, I won't enjoy all the benefits of CI—I like using Codeship to run the build and test processes for my theme. This way, I get to keep a nice clean local working repo, and avoid any build scripts on my live server.

The full stack for my Ghost theme looks like this:

  • Local development Ghost 1.20.3 server
  • Ghost theme running Webpack and various JS / CSS processors
  • Origin repo in Github
  • Free tier Codeship account using node 6.9
  • Digital Ocean Ubuntu 16.04 droplet also running Ghost 1.20.3

Broadly speaking, the CI process looks like this:

  • Make local changes, commit, push to Github
  • Github triggers Codeship build
  • Codeship compiles the theme for production, swaps .gitignore files
  • Codeship server pushes to repo on production server
  • Production server receives updated theme files, restarts to display changes

Push your theme to a remote repo

You can't push a project directly to Codeship. When you create a Codeship project you'll have a choice to integrate to either Gitlab, Bitbucket or Github. I chose to use Github, but they're all roughly equivalent. So, from your local machine, prepare a repo from your working files. In the primary .gitignore file, you should ignore any production or build files. My .gitignore looks like this:

# ignore development / production files 

/node_modules
/assets

Next, create a .gitignore file to be used after Codeship builds your theme. This ignore list should specify working files, and allow production files to be committed. I call my build ignore file .codeshipignore and include these files:

# allow assets to be pushed from codeship

/node_modules
/src
build.gitignore

Once you have all the appropriate local files, go ahead and push to your remote repo, then create a Codeship project and give it access to your origin repo.

Configure Codeship to build your theme

Codeship will prompt you for some "Setup Commands." This is where you can specify the environment required for your theme's tests. If you don't have any test commands to run, don't worry about these fields. After you save this setup, you'll want to click the "project settings" button, and navigate over to the "Deploy" tab. This is where Codeship runs build processes.

Because I'm pushing to a theme that's on a production server, I used the master branch as my deployment trigger. If you push to multiple servers, i.e., a staging and a production server, you might want to configure multiple pipelines.

Once you've established a pipeline, it's time to add your build script (pick the 'custom script' option). These are commands you feed the Codeship server to be executed just like you're in a terminal session.

Here's what my build script looks like:

git config --global user.email "xxxxx"
git config --global user.name "xxxxxx"
git checkout master
mv .gitignore build.gitignore
mv .codeshipignore .gitignore
nvm install 6.9.0
nvm use 6.9.0
npm install
npm run build:prod
ls -la
git add .
git commit -m "deploy"
git remote add live ssh://git@xxx.xx.xx.xxx/home/git/xxxx-ghost/
git push live master -f

It's a fairly straightforward process: ensure you're on the right version of Node, install Node modules, build the theme from source, swap the .gitinore files, add your server's repo, and push. Codeship's environments reset after each build so it's necessary to add the remote every time.

Configuring your live server to receive Codeship pushes

Next, you need to be sure your live server can receive the Codeship repo. Log into your remote server through ssh, and create a bare repo for Codeship to push to, inside the home dir for the git user (this user should be created already, assuming you have git installed on this machine). You will also need to ensure the Ghost CLI is installed on your server.

cd /home/git
mkdir yourblog.git
cd yourblog.git
git --bare init

Jump into the 'hooks' directory and create a file called post-receive.

cd /home/git/yourblog.git/hooks
touch post-receive

Open the new file with the editor of your choice, and paste the following lines in, adjusting for the path to your ghost install / theme:

#!/bin/sh
git --work-tree=/var/www/yourblog/content/themes/xxxx-xxxx --git-dir=/home/git/yourblog.git/ checkout -f
cd /var/www/yourblog
sudo /usr/lib/node_modules/ghost-cli/bin/ghost restart

This script will direct git to place the repo files in your existing ghost theme directory, and to use the CLI to restart your blog instance.

Next, we need to ensure this script can actually execute. Change the permissions for post-receive:

chmod +x post-receive

...and it would be nice if Git owned the proper directories, too:

cd /home/git
chown -R git:git yourblog.git
cd /var/www/yourblog/content/themes
chown -R git:git yourtheme

At this point, we need to ensure Codeship can push to the remote server. Under the General Project Settings tab in Codeship, copy the "SSH public key" and paste it into the git .ssh dir:

cd /home/git/.ssh
touch authorized_keys
nano authorized_keys
[paste the key and close/save]

There's one last task to complete! We need to ensure the git user can run the Ghost CLI's restart command without being prompted for a password, since the Codeship server is unable to respond interactively. Open up the sudoers file with this command:

visudo

On newer flavors of Ubuntu this opens up a special session in nano, ensuring that the syntax of your sudoers is correct before saving, minimizing the chance that you blow up your system permissions. Go ahead and insert these lines at the end of the file, where ghost_instance is the name of the specific Ghost blog you have hooked the Git repo to:

# Allow git to restart ghost blogs
git ALL = NOPASSWD: /usr/lib/node_modules/ghost-cli/bin/ghost restart ghost_instance

The line added translates like this:
git ALL == allow connections from all hosts
NOPASSWD: == don't require the pw for sudo actions for...
/usr/lib/node_modules/ghost-cli/bin/ghost == the absolute path to the Ghost CLI

Source your bash profile to reload the sudoers list:

. ~/.bashrc

That ought to do it.