A good[ish] website
Web development blog, loads of UI and JavaScript topics
An uncomplicated deploy script that doesn’t use Git, but still keeps your upstream Git repo up-to-date with the live server.
I’ve been using this simple script to deploy few side project for couple of years now. There are brilliant services like Vercel or Netlify that let you deploy static sites super easily. But this one is a 100% DIY solution, which assumes you have a VPS (like DigitalOcean etc.) with an ssh access.
Basically a dumb script that deploys to a simple server, in my case it only assumes that rsync
works to the target machine. If you deploy to an AWS bucket, you can change the upload step on the script.
No Git: the actual transporting of the files doesn’t happen via Git. This way Git is not needed to be installed onto the server, nor have any hooks listening. Which enables to roll with a really simple, perhaps rootless server setup, etc.
Build locally: if the build step happens locally, it also doesn’t expect anything from the server.
Keep Git and the server in sync: the problem with having the build step run locally, is that the deploy script can be run before the new code has been pushed upstream (Github, GitLab...), and then your server isn’t up-to-date with the the remote repo. But this can be solved with a bit of locally run Git smartness.
I’ll divide the explanation into 5 parts (or jump to the full script):
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace
readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"
First, it sets on the the unofficial Bash strict mode, in this case written in longhand syntax. It could also be written as set -euo pipefail
. It tells Bash to: 1) stop executing the script if an error happens, 2) Error and stop the script if an undefined variable is being called, 3) prevent errors in a pipeline from being masked.
Second: configure the deployment script, paths and names.
These totally optional helper functions are only for printing different colored messages to the terminal as you deploy. More on the colors in Bash.
green() {
echo "\\033[32m${1}\\033[m"
}
red() {
echo "\\033[31m${1}\\033[m"
}
yellow() {
echo "\\033[33m${1}\\033[m"
}
This is the most interesting bit of the code I think. It uses diffing and other Git wizardry to figure out if the local Git tree is clean and up-to-date with the upstream one.
die_if_wrong_project() {
local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")
if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
red "\\n😱 wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
exit 1
fi
}
die_if_dirty() {
local CHANGED_FILES=$(git diff-index --name-only HEAD --)
if [ "$CHANGED_FILES" != "" ]; then
red "\\n😱 the repository is not clean:\\n ${CHANGED_FILES}\\n"
exit 1
fi
}
die_if_untracked_files() {
local UNTRACKED=$(git ls-files --exclude-standard --others)
if [ "$UNTRACKED" != "" ]; then
red "\\n😱 there are untracked files:\\n ${UNTRACKED}\\n"
exit 1
fi
}
die_if_ahead_of_remote() {
local UPSTREAM="@{u}"
local LOCAL=$(git rev-parse @)
local REMOTE=$(git rev-parse "$UPSTREAM")
if [ "$LOCAL" != "$REMOTE" ]; then
red "\\n😱 the local branch is not up to date with the remote, please do a git push\\n"
exit 1
fi
}
Explanations:
die_if_wrong_project()
rm
stuff, better check if this is the right Git project so it doesn’t reek havoc if run in a wrong directory.die_if_dirty()
die_if_untracked_files()
die_if_ahead_of_remote()
The function that physically moves the files over to the server using rsync
:
move_files_to_server() {
yellow "File transfer started, might take a moment."
rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
green "Files moved to ${SERVER}:${TARGET_PATH}."
}
Here’s what the rsync
does:
-a
, --archive
-rlptgoD
. It is a quick way of saying you want recursion and want to preserve almost everything (with -H being a notable omission).-z
, --compress
rsync
compresses the file data as it is sent to the destination machine, which reduces the amount of data being transmitted.--info=progress2
--exclude=".*/"
.env
, which is super important!--chmod=+rwx
--delete
Then lastly, run all the functions and the build step. In this case I have the build script as an npm script. First run the sanity check, then build, then move the files:
yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"
Below is the wle script. You need to of course configure it to fit your project.
Run it like so:
$ sh deploy.sh bob@192.168.0.0
Or put it in your projects package.json
along with the other scripts your project needs:
{
"scripts": {
"deploy": "sh bin/deploy.sh bob@192.168.0.0",
"develop": "gatsby develop",
"clean": "gatsby clean",
"build": "gatsby build",
"serve": "gatsby serve"
}
}
The whole deploy.js
script:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace
readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"
green() {
echo "\\033[32m${1}\\033[m"
}
red() {
echo "\\033[31m${1}\\033[m"
}
yellow() {
echo "\\033[33m${1}\\033[m"
}
# Since we're doing some rm stuff, better check if this is the right Git project
# so it don't reek havoc if run in a wrong directory
die_if_wrong_project() {
local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")
if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
red "\\n😱 wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
exit 1
fi
}
die_if_dirty() {
local CHANGED_FILES=$(git diff-index --name-only HEAD --)
if [ "$CHANGED_FILES" != "" ]; then
red "\\n😱 the repository is not clean:\\n ${CHANGED_FILES}\\n"
exit 1
fi
}
die_if_untracked_files() {
local UNTRACKED=$(git ls-files --exclude-standard --others)
if [ "$UNTRACKED" != "" ]; then
red "\\n😱 there are untracked files:\\n ${UNTRACKED}\\n"
exit 1
fi
}
die_if_ahead_of_remote() {
local UPSTREAM="@{u}"
local LOCAL=$(git rev-parse @)
local REMOTE=$(git rev-parse "$UPSTREAM")
if [ "$LOCAL" != "$REMOTE" ]; then
red "\\n😱 the local branch is not up to date with the remote, please do a git push\\n"
exit 1
fi
}
move_files_to_server() {
yellow "File transfer started, might take a moment."
rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
green "Files moved to ${SERVER}:${TARGET_PATH}."
}
yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"
The script could take a version number as the second arg, and then create a tag based on that, so rollbacks would be possible by checking out the previous tag.
It could have a --force
option that ignores the Git stuff.
The script could also be configured with args or a config file, it could read in package.json
etc.
Comments would go here, but the commenting system isn’t ready yet, sorry.