The Autodidacts

Exploring the universe from the inside out

How to Deploy a Ghost Theme using the Admin API + Bash + cURL

Figuring out how to upload a Ghost theme using the Admin API from Bash shouldn't have required heroic effort. Embarrassingly, it did.

↓ Jump to the script ↓

I cobbled a script together from the Ghost docs and the inevitable forum posts. However, the Bash JSON Web Token (JWT) example in the Admin API documentation produced a malformed JWT.

All that was needed, in the end, was one more trim command added to the pipeline in the base64_url_encode function: | tr -d '\n'. However, because of the width of my terminal window, general ignorance of the technology involved, and human stupidity, it took me two days of intermittent tinkering to discover that there was a linebreak in the token that I’d validated (successfully!) with an online service.

Anytime it takes me longer than it should to figure something out, I write a blog post about it, because I know someone else is following the same dead-end forum posts I did!

In addition to the bugfix to the example JWT, my script duct-tapes all the parts together into a working whole (fingers crossed!), and adds a slick interface for choosing which theme to deploy to which site.

My list of themes is hard-coded in the script (there are quite a few), along with an array of sites and their current API keys. When I pick a theme and a site, the API key is automatically pulled in.

$THEME, $SITE_URL, and $KEY can also be provided as environment variables (allowing the script to be run non-interactively).

After generating the token and getting the necessary user input, the script enters the directory where the themes live (you might have Ghost installed somewhere else, in which case change that path), zips the selected theme, ignoring .git and other stuff that isn’t needed, names it DATE-theme.zip, sends an authenticated cURL Admin API request to upload the theme, and then a second to activate it.

Once the script is set up, the whole deployment process literally takes seconds.

Why didn’t I do this earlier?! Wait, now I remember...

Enjoy!

As always, my bash scripts are provided for use at your own risk — which might be considerable!

Support me! Yay!Support me and Ghost!Support meSupport this blog ♥ get $90 worth of paid themes for $5 • Why not donate to Wikipedia while you’re at it!

#!/bin/bash

set -x # print each command before it's run
set -e # exit on error

# THEME=""
# SITE_URL=""
# KEY=""
API_VERSION="v3.0"
THEME_DIR="/var/www/ghost/content/themes"

declare -A sites

sites["http://localhost:2368"]="PASTE_API_KEY_HERE"
sites["https://example.com"]="PASTE_API_KEY_HERE"

if [ -z ${THEME+x} ]; then 
SELECTED_THEME=$(fzf << EOF 
golden-pro
weblog
laminim
undefined
casper
source
webcomic
standalone
EOF
)
export THEME=$SELECTED_THEME
fi

if [ -z ${SITE_URL+x} ]; then
SELECTED_SITE=$(fzf << EOF
http://localhost:2368 
https://example.com
EOF
)
export SITE_URL=$SELECTED_SITE
export KEY=${sites["$SELECTED_SITE"]}
fi

echo "Deploying $THEME to $SITE_URL"


# Split the key into ID and SECRET
TMPIFS=$IFS
IFS=':' read ID SECRET <<< "$KEY"
IFS=$TMPIFS

# Prepare header and payload
NOW=$(date +'%s')
FIVE_MINS=$(($NOW + 300))
HEADER="{\"alg\": \"HS256\",\"typ\": \"JWT\", \"kid\": \"$ID\"}"
PAYLOAD="{\"iat\":$NOW,\"exp\":$FIVE_MINS,\"aud\": \"/admin/\"}"

# Helper function for performing base64 URL encoding
base64_url_encode() {
    declare input=${1:-$(</dev/stdin)}
    # Use `tr` to URL encode the output from base64.
    printf '%s' "${input}" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_' | tr -d '\n' 
}

# Prepare the token body
header_base64=$(base64_url_encode "${HEADER}")
payload_base64=$(base64_url_encode "${PAYLOAD}")

header_payload="${header_base64}.${payload_base64}"

# Create the signature
signature=$(printf '%s' "${header_payload}" | openssl dgst -binary -sha256 -mac HMAC -macopt hexkey:$SECRET | base64_url_encode)

# Concat payload and signature into a valid JWT token
TOKEN="${header_payload}.${signature}"

echo "$header_payload"
echo "$signature"

TOKEN="${header_payload}.${signature}"

FILENAME="$(date -I)-${THEME}"

cd "$THEME_DIR" || exit
zip -r "$FILENAME.zip" "$THEME" -x '*git*' '*node_modules*' '*bower_components*'

# Upload theme
curl -H "Authorization: Ghost $TOKEN" \
-H "Content-Type: multipart/form-data" \
-H "Accept-Version: $API_VERSION" \
-F "file=@/var/www/ghost/content/themes/$FILENAME.zip" \
$SITE_URL/ghost/api/admin/themes/upload/

# Activate theme
curl -H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: $API_VERSION" \
-X PUT "$SITE_URL/ghost/api/admin/themes/$FILENAME/activate/"