Last active
November 6, 2021 21:20
-
-
Save tonious/27a04460ab29e3d7e7f57a0c0aaf3431 to your computer and use it in GitHub Desktop.
Quick and dirty local deploy/rollback script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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="[email protected]: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