Skip to content

Instantly share code, notes, and snippets.

@tonious
Last active November 6, 2021 21:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tonious/27a04460ab29e3d7e7f57a0c0aaf3431 to your computer and use it in GitHub Desktop.
Save tonious/27a04460ab29e3d7e7f57a0c0aaf3431 to your computer and use it in GitHub Desktop.
Quick and dirty local deploy/rollback script.
#!/bin/bash
set -euf -o pipefail
# Usage
usage() {
cat <<EOF
usage: $0 [ -d ] [ -c RCFILE ] [ -t GITREF ] [ -h | -k | -r | -s | -x ]
-c Use the parameters specified in RCFILE.
-d Dryrun. Do not actually modify anything.
-t Check out the version tagged GITREF.
-h Print this help message and exit.
-k Clean out old deploys, keeping the 'original', current, and previous as well as a specified number of recent deploys.
-r Rollback; return symlink to the previous version.
-s Show current paths and exit.
-x Print an example RCFILE and exit.
This utility will check out a git repo, maintaining multiple versions. It will
create symlinks for both the current and immediately previous deploys allowing
for zero downtime deploys and rollbacks.
If RCFILE is not given, $0 will try to load ./deployrc and then ./.deployrc in
the current working directory. Failing that, it will try ~/.deployrc and
finally /etc/deployrc. If no file is specified, and none of those options
exist, it will exit with an error.
EOF
}
# RCFile utilities.
lock() {
# http://wiki.bash-hackers.org/howto/mutex
if mkdir "$lockfile" &>/dev/null; then
echo $$ > "$lockfile/pid"
else
otherpid="$(cat "$lockfile/pid")"
if [ $? != 0 ]; then
echo "Lock failed, PID $otherpid is active."
exit 2
fi
if ! kill -0 "$otherpid" &>/dev/null; then
# lock is stale, remove it and restart
rm -rf "$lockfile"
exec bash "$0" "$@"
else
# lock is valid
echo "Lock failed, PID $otherpid is active."
exit 2
fi
fi
}
unlock() {
rm -rf "$lockfile"
}
ssh_cleanup() {
/bin/kill "$SSH_AGENT_PID"
}
# Check if we know the github host key. If not, add it.
get_github_host_key () {
if [ "$(whoami)" == 'root' ]; then
known_hosts=/etc/ssh/ssh_known_hosts
elif [ "$(whoami)" == 'www-data' ]; then
known_hosts=/var/www/.ssh/known_hosts
else
known_hosts=$HOME/.ssh/known_hosts
fi
if ! ssh-keygen -q -F github.com -f "$known_hosts" > /dev/null; then
key=$(ssh-keyscan -H github.com 2>/dev/null)
echo "$key" >> "$known_hosts"
fi
}
load_rcfile() {
if [[ -z ${rcfile+:} ]]; then
if [[ -e ./deployrc ]]; then
rcfile="./deployrc"
source ./deployrc
elif [[ -e ./.deployrc ]]; then
rcfile="./.deployrc"
source ./.deployrc
elif [[ -e ~/.deployrc ]]; then
rcfile="$HOME/.deployrc"
source ~/.deployrc
elif [[ -e /etc/deployrc ]]; then
rcfile="/etc/deployrc"
source /etc/deployrc
else
echo "Could not find deployrc."
exit 1
fi
else
if [[ -e "$rcfile" ]]; then
source "$rcfile"
else
echo "Could not read $rcfile."
exit 1
fi
fi
lockfile="/tmp/$(basename "$rcfile").lock"
# Do we have a git url to work from?
if [[ -z ${url+:} ]]; then
echo "No git url specified."
exit 1
fi
if [[ -z ${gitref+:} ]]; then gitref='master'; fi
# Check to see if variables are set.
if [[ -z ${rootpath+:} ]]; then
echo "No rootpath set in deployrc."
exit 1
fi
if [[ -z ${currentpath+:} ]]; then currentpath="$rootpath/current"; fi
if [[ -z ${previouspath+:} ]]; then previouspath="$rootpath/previous"; fi
if [[ -z ${sharedpath+:} ]]; then sharedpath="$rootpath/shared"; fi
if [[ -z ${deploypath+:} ]]; then deploypath="$rootpath/deploys"; fi
if [[ -z ${deployname+:} ]]; then deployname="$(date +%Y%m%dT%H%M%S)"; fi
if [[ -z ${targetpath+:} ]]; then targetpath="$deploypath/$deployname"; fi
if [[ -z ${chownneeded+:} ]]; then
chownneeded=false
else
if [[ -z ${chuser+:} ]]; then
echo "chownneeded is true, but no chuser is specified."
exit 1
fi
fi
if [[ -z ${keepcopies+:} ]]; then keepcopies=3; fi
if [[ -z ${runcomposer+:} ]]; then runcomposer=true; fi
if [[ -z ${composeropts+:} ]]; then composeropts=""; fi
if [[ -z ${runnpm+:} ]]; then runnpm=true; fi
if [[ -z ${npmopts+:} ]]; then npmopts=""; fi
if [[ -n ${sshkeypath+:} && -r ${sshkeypath} ]]; then
eval "$(ssh-agent -s)" >/dev/null 2>&1
set +f
ssh-add "$sshkeypath" >/dev/null 2>&1
set -f
trap ssh_cleanup EXIT
fi
}
print_rcfile() {
cat <<EOF
rootpath="/tmp/example"
url="git@github.com:getgrav/grav.git"
gitref="master" # This can be overridden on the command line using the -t flag.
# If running as root, set chownneeded to true, and specify target user and group.
chownneeded=false
chuser="admin"
# Defaults for other values. Uncomment to override.
# currentpath="\$rootpath/current"
# previouspath="\$rootpath/previous"
# sharedpath="\$rootpath/shared"
# deployname="\$(date +%Y%m%dT%H%M%S)"
# deploypath="\$rootpath/deploys"
# targetpath="\$deploypath/\$deployname"
# keepcopies=3
# runcomposer=true
# runnpm=true
post_checkout_hook() {
# This function will be executed after a version has been checked out.
# Symlink in any shared resources here.
# Prefixing commands with '\$runner' will ensure they are _not_ executed during a dry run.
\$runner echo "post-checkout hook"
}
post_symlink_hook() {
# This function will be executed after the currentpath symlink has been updated.
# Restart your webserver or whatever.
# Prefixing commands with '\$runner' will ensure they are _not_ executed during a dry run.
\$runner echo "post-symlink hook"
}
EOF
}
print_variables() {
cat <<EOF
rootpath="$rootpath"
url="$url"
gitref="$gitref"
currentpath="$currentpath"
previouspath="$previouspath"
sharedpath="$sharedpath"
deployname="$deployname"
deploypath="$deploypath"
targetpath="$targetpath"
keepcopies=$keepcopies
EOF
if $chownneeded; then cat <<EOF
chownneeded=$chownneeded
chuser="$chuser"
EOF
fi
}
# Tasks
clone() {
if [[ ! -d "$deploypath" ]]; then
$runner mkdir -p "$deploypath"
fi
if [[ -d $targetpath ]]; then
echo "$targetpath already exists. I'm cowardly refusing to check it out again."
exit 1
fi
$runner get_github_host_key
$runner git clone -q "$url" "$targetpath"
if [[ "$gitref" != 'master' ]]; then
$runner git checkout -q "$gitref"
fi
if $chownneeded; then
$runner chown -R "$chuser" "$targetpath"
fi
}
clean() {
currenttarget="$(readlink "$currentpath")"
previoustarget="$(readlink "$previouspath")"
while read -r release; do
$runner rm -fr "$deploypath/$release"
done < <( (cd "$deploypath" && ls; basename "$currenttarget"; basename "$previoustarget") | sort | uniq -u | grep -v "original" | tail -n "+$keepcopies" )
}
check_buildtools() {
if ! [[ -e "$targetpath/composer.json" ]]; then
echo "Can't find $targetpath/composer.json. Not running composer."
runcomposer=false
fi
if ! [[ -e "$targetpath/package.json" ]]; then
echo "Can't find $targetpath/package.json. Not running npm."
runnpm=false
fi
}
run_buildtools() {
if $runcomposer; then
$runner composer install --quiet $composeropts
fi
if $runnpm; then
$runner npm install $npmopts >/dev/null 2>&1
fi
}
# Update master symlink.
link_current() {
if [[ -h "$currentpath" ]]; then
previoustarget="$(readlink "$currentpath")"
$runner ln -snf "$previoustarget" "$previouspath"
if $chownneeded; then
$runner chown -h "$chuser" "$currentpath"
fi
else
if [[ -e "$currentpath" ]]; then
echo "Cowardly refusing to overwrite non-symlink $currentpath with a symlink to $targetpath"
exit 1
fi
fi
if [[ ! -d "$targetpath" ]]; then
echo "Cowardly refusing to symlink to a missing directory: $targetpath"
exit 1
fi
$runner ln -snf "$targetpath" "$currentpath"
if $chownneeded; then
$runner chown -h "$chuser" "$currentpath"
fi
}
link_rollback() {
if [[ -h "$previouspath" ]]; then
previoustarget="$(readlink "$previouspath")"
else
echo "Could not determine rollback target.";
exit 1
fi
if [[ ! -d "$previoustarget" ]]; then
echo "Cowardly refusing to symlink to a missing directory: $previoustarget"
exit 1
fi
if [[ -h "$currentpath" ]]; then
currenttarget="$(readlink "$currentpath")"
if [[ "$previoustarget" == "$currenttarget" ]]; then
echo "No older revision; can't roll back."
exit 1
fi
else
if [[ -e "$currentpath" ]]; then
echo "Cowardly refusing to overwrite non-symlink $currentpath with a symlink to $previoustarget"
exit 1
fi
fi
$runner ln -snf "$previoustarget" "$currentpath"
if $chownneeded; then
$runner chown -h "$chuser" "$currentpath"
fi
}
run_if_function() {
if [[ $(type -t "$1") == "function" ]]; then
$1
fi
}
# Strategies for execution
dryrun() {
echo "$@"
}
runhere() {
dryrun "$@"
if [[ -d "$targetpath" ]]; then
cd "$targetpath" && ("$@")
else
("$@")
fi
}
# Process options.
action='deploy'
runner='runhere'
wantedref='';
while getopts "c:dt:hkrsxl" opt; do
case "$opt" in
c)
rcfile="$OPTARG"
;;
d)
runner='dryrun'
;;
t)
wantedref="$OPTARG"
;;
h)
action='help'
;;
k)
action='clean'
;;
r)
action='rollback'
;;
s)
action='showpaths'
;;
x)
action='printrcfile'
;;
l)
action='shellcheck'
;;
*)
usage
exit 1
;;
\?)
usage
exit 1
;;
esac
done
# Choose an action.
case "$action" in
'deploy')
load_rcfile
if [[ -n "${wantedref}" ]]; then gitref="$wantedref"; fi
if lock "$@"; then
clone
run_if_function post_checkout_hook
check_buildtools
run_buildtools
link_current
run_if_function post_symlink_hook
unlock
else
echo "Could not get lockfile $lockfile"
exit 1
fi
;;
'help')
usage
;;
'clean')
load_rcfile
if lock "$@"; then
clean
unlock
else
echo "Could not get lockfile $lockfile"
exit 1
fi
;;
'rollback')
load_rcfile
if lock "$@"; then
link_rollback
unlock
else
echo "Could not get lockfile $lockfile"
exit 1
fi
;;
'showpaths')
load_rcfile
print_variables
;;
'printrcfile')
print_rcfile
;;
'shellcheck')
shellcheck -e 1090,1091,2154 "$0"
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment