#!/usr/bin/env bash
# **_A (magic) shell script to deploy Git repositories_**
#
# [](https://travis-ci.org/AlphaHydrae/deploy)
# [](https://badge.fury.io/js/bash-deploy)
# [](https://opensource.org/licenses/MIT)
#
# Read the [annotated source](https://alphahydrae.github.io/deploy/)
#
# Repository: [AlphaHydrae/deploy](https://github.com/AlphaHydrae/deploy)
#
# Shamelessly inspired by: [visionmedia/deploy](https://github.com/visionmedia/deploy)
#
#
VERSION=3.1.0
COLORS=
COLOR_BOLD=1
COLOR_RED=31
COLOR_GREEN=32
COLOR_YELLOW=33
COLOR_MAGENTA=35
COLOR_CYAN=36
DEPLOY_ARGS=("$@")
DEPLOY_COMMAND=
DEPLOY_ENV=
DEPLOY_OPTIONS_VALUE_MAP=''
test -z "$DEPLOY_COLOR" && DEPLOY_COLOR=auto
test -z "$DEPLOY_CONFIG" && DEPLOY_CONFIG=./deploy.conf
test -z "$DEPLOY_UPDATE_PREFIX" && DEPLOY_UPDATE_PREFIX=/usr/local
test -z "$DEPLOY_UPDATE_REPO" && DEPLOY_UPDATE_REPO=https://github.com/AlphaHydrae/deploy.git
test -z "$DEPLOY_UPDATE_REV" && DEPLOY_UPDATE_REV=master
# ## Usage
#
# **deploy** is a shell script to deploy your Git projects through SSH.
# Add a `deploy.conf` file in your project's directory.
# Here's an example for a Node.js project:
#
# # deploy.conf
# [production]
# repo https://github.com/me/my-app
# host my.server.com
# user deploy
# path /var/www/app
#
# # describe how to deploy your app
# env NODE_ENV=production
# deploy npm install --production
# post-deploy npm start
#
# **deploy** is language-agnostic.
# For a Rails project, you could replace the last 3 lines with:
#
# env RAILS_ENV=production
# deploy bundle install --without development test
# deploy rake assets:precompile
# post-deploy rails server
#
# Now run **deploy**!
#
# ```sh
# deploy production setup
# deploy production rev master
# ```
#
# It will:
# * Connect to `my.server.com` as the `deploy` user through SSH
# * The `setup` command will set up a deployment structure and clone your repository
# * The second `rev` command will checkout the latest version of your `master` branch and run the deployment hooks you defined (`deploy` and `post-deploy` in the configuration file)
#
# Read on to learn what to write in the [configuration file](#configuration-file) or how to use each [sub-command](#sub-commands).
usage() {
cat <<-EOF
Usage: deploy [options] [env] [command]
Default command:
[options] [rev] deploy a release (commit, branch or tag)
(same as the "rev" command)
Deployment commands:
setup run host setup commands
rev [options] [rev] deploy a release (commit, branch or tag)
console [path] open an ssh session to the host
exec execute the given command on the host
list list deployed releases
cleanup [options] clean up old releases
Local commands:
[env] config [key] output whole config file or the value of one key
config-all output all values of a config key in an environment
[env] config-section output a config section
update [options] [rev] update this script to the latest version
General options:
-C, --chdir change the working directory
--color set color mode to "always", "never" or "auto" (defaults to "auto")
-c, --config set config file path (defaults to ./deploy.conf)
-y, --yes accept all confirmation prompts (use with caution)
-v, -V, --version output current version
-h, --help output usage information
Deployment options:
-P, --path set the remote path to deploy the project to on the host
-r, --repo set the Git URL to fetch source code from when deploying
SSH options:
-A, --forward-agent enable SSH agent forwarding
-H, --host set the host to connect to
-i, --identity select a file from which the identity (private key)
for public key authentication is read
-p, --port set the port to connect to
-t, --tty force pseudo-terminal allocation
-u, --user set the host user to connect as
Self-update options:
--update-prefix set the base directory to update the deploy script
(full path will be /bin/deploy)
--update-path set the full path to the deploy script
(overrides --update-prefix)
Examples:
deploy prod setup set up the 'prod' host
deploy prod master deploy the 'master' branch in the 'prod' env (shortcut)
deploy prod rev master (same)
deploy dev foo deploy the 'foo' branch in the 'dev' env
deploy prod exec pm2 status run 'pm2 status' in the 'prod' env
deploy dev config user get the value of the 'user' key in the 'dev' env
deploy update update the deploy script
EOF
}
# ## Installation
#
# With npm:
#
# ```sh
# npm install -g bash-deploy
# ```
#
# With curl:
#
# ```sh
# PREFIX=/usr/local/bin \
# FROM=https://raw.githubusercontent.com \
# && curl -sSLo $PREFIX/deploy \
# $FROM/AlphaHydrae/deploy/master/bin/deploy \
# && chmod +x $PREFIX/deploy
# ```
#
# With wget:
#
# ```sh
# PREFIX=/usr/local/bin \
# FROM=https://raw.githubusercontent.com \
# && wget -qO $PREFIX/deploy \
# $FROM/AlphaHydrae/deploy/master/bin/deploy \
# && chmod +x $PREFIX/deploy
# ```
#
# Or [download it](https://raw.githubusercontent.com/AlphaHydrae/deploy/master/bin/deploy) yourself.
# ## Requirements
#
# **deploy** is a one-file bash script which requires the following commands: `cat`, `cut`, `date`, `git`, `grep`, `ls`, `mkdir`, `readlink`, `sed`, `ssh`, `tail`, `tar` and `wc`.
# Most bash shells have all of those out of the box except perhaps [Git](https://git-scm.com).
#
# It also optionally requires the `chmod`, `cp` and `mktemp` commands to update itself.
check_requirements() {
for com in cat cut date git grep ls mkdir sed ssh tail tar wc; do
require_command $com
done
}
require_command() {
command_exists "$1" || abort "$1 command not found"
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# ## Configuration file
#
# **deploy** reads its main configuration from a `deploy.conf` file in the current directory.
# It can also be customized with [environment variables and command line options](#command-line-options-environment-variables).
config_file_exists() {
test -f "$DEPLOY_CONFIG" && test -r "$DEPLOY_CONFIG"
}
# The configuration file is basically a series of sections containing key/value pairs:
#
# # deploy.conf
# [staging]
# host staging.example.com
# user test
# # how to deploy
# post-deploy ./run-test.sh
#
# [production]
# host 192.168.1.42
# user root
# # how to deploy
# env NODE_ENV=production
# deploy npm install --production
# deploy npm run build
# post-deploy pm2 start pm2.json
#
# Each named section, delimited by `[name]`, represents an **environment** (i.e. a host machine) to deploy to,
# in this example the *staging* and *production* environments.
#
# Lines beginning with `#` are comments and are ignored.
#
# Other lines are key/value pairs.
# A key is a sequence of characters containing no whitespace, followed by at least one tab or space.
#
# For example, in the line `deploy npm run build`, the key is `deploy` and its value is `npm run build`.
config_section_exists() {
grep "^\[$1\]$" "$DEPLOY_CONFIG" &> /dev/null
}
get_config_section() {
local env="$1"
grep "^\[$env\]$" -A 999 "$DEPLOY_CONFIG" \
| tail -n +2 \
| grep -v "^ *#" \
| grep -v "^ *$" \
| sed "/^\[/q" \
| sed "/^\[/d"
}
get_config_key_values_in_env() {
local key="$1"
local env="$2"
get_config_section $env \
| grep "^$key[ \t][ \t]*[^ \t]" \
| cut -d ' ' -f 2-999 \
| sed "s/^ *//" \
| sed "s/ *$//"
}
# ### Single-value and multiple-value keys
#
# Some keys like `repo`, `host`, `port` or `user` are simple configuration properties that have one value per environment.
# If multiple values are found, the last one is used.
get_last_config_key_value() {
local key=$1
local env_var=$(get_config_key_env_var $key)
# These keys can be overriden through an environment variable of the same name in uppercase and prefixed by `DEPLOY_`.
# For example, the environment variable to override the `repo` key is `DEPLOY_REPO`.
local env_var_value=${!env_var}
if test -n "$env_var_value"; then
echo "$env_var_value"
else
test -n "$key" \
&& get_all_config_key_values "$key" \
| tail -n 1
fi
}
get_config_key_env_var() {
local key=$1
echo "DEPLOY_$key" | tr '-' '_' | tr '[a-z]' '[A-Z]'
}
# Other keys like `setup`, `deploy` or `post-deploy` are multiple-value keys used for [hooks](#hooks).
# They can be present multiple time in the same environment and all their values will be used in order.
get_all_config_key_values() {
local env=$DEPLOY_ENV
local key=$1
test -n "$env" \
&& test -n "$key" \
&& get_config_key_values_inherited $key $env \
| tail -n +2
}
# ### Environment inheritance
#
# When deploying your project to multiple environments, there will probably be some configuration properties
# that are identical for multiple environments.
# To avoid repetition, an environment can **inherit** from one or multiple other environments:
#
# # deploy.conf
# [common]
# user dev
# deploy do-stuff
# deploy do-more-stuff
#
# [secure]
# user sekret
# deploy do-sensitive-stuff
#
# [production]
# inherits common
# inherits secure
# user root
# deploy just-do-it
#
# In this example, the `production` environment inherits from `common` and `secure` (in that order).
#
# The value of a *single-value* key like `user` will be the last value found in the environment inheritance tree.
# In this case, it will be `root` for the `production` environment (the values inherited from the `common` and `secure` environments are overwritten).
#
# A *multiple-value* key like `deploy` will include all values found in the entire inheritance tree.
# In this case, it will have 4 values for the `production` environments, taken in order from the `common`, `secure` and `production` sections:
#
# do-stuff
# do-more-stuff
# do-sensitive-stuff
# just-do-it
get_config_key_values_inherited() {
local key="$1"
local env="$2"
shift 2
local without=("$@")
test -n "$env" && without+=("[$env]")
local values=
if ! config_section_exists $env; then
abort "[$env] config section not defined"
fi
local new_line=$'\n'
local old_ifs=$IFS
IFS=$'\n'
local inherits="$(get_config_key_values_in_env inherits $env)"
for inherits_key in $inherits; do
IFS="$old_ifs"
if ! value_is_in "[$inherits_key]" "${without[@]}"; then
local inherited_result="$(get_config_key_values_inherited $key $inherits_key "${without[@]}")"
local inherited_without=("$(echo "$inherited_result"|head -n 1)")
local inherited_values="$(echo "$inherited_result"|tail -n +2)"
without=("${without[@]}" "${inherited_without[@]}")
if test -n "$inherited_values"; then
test -n "$values" && values="$values$new_line"
values="$values$inherited_values"
fi
fi
IFS=$'\n'
done
IFS="$old_ifs"
# For convenience in simple use cases, you can also add a **default environment** by including a nameless section in your configuration file:
#
# # deploy.conf
# []
# user dev
# deploy do-stuff
# deploy do-more-stuff
#
# [development]
# deploy do-it-somehow
#
# [production]
# user root
# deploy do-it-seriously
#
# All environments that have no `inherits` key automatically inherit from the default environment.
if test -z "$inherits" && ! value_is_in "[]" "${without[@]}"; then
without+=("[]")
local default_environment_values="$(get_config_key_values_in_env $key)"
if test -n "$default_environment_values"; then
[ -n "$values" ] && values="$new_line$values"
values="$default_environment_values$values"
fi
fi
local current_environment_values="$(get_config_key_values_in_env $key $env)"
if test -n "$current_environment_values"; then
[ -n "$values" ] && values="$values$new_line"
values="$values$current_environment_values"
fi
echo "${without[@]}$new_line$values"
}
# ## Hooks
#
# Hooks are user-defined commands that can be run during a *deployment phase*.
# There are currently two phases defined: **setup** and **deploy**.
# The *setup* phase happens when you run the `setup` command,
# while the *deploy* phase happens when you run the `rev` command.
#
# There are various hooks for each phase: some that run before, some during and some after **deploy** does its thing.
# These are the currently available hooks:
#
# pre-setup
# post-setup
#
# pre-deploy
# deploy
# post-deploy
#
# Hooks are multiple-value keys that are optional and can be used as many times as you want.
# They will all be run in order.
run_hooks() {
local name=$1
test -n "$name" || abort hook name required
local dir=$2
test -n "$dir" || abort hook working directory required
local path=`get_last_config_key_value path`
local commands=`get_all_config_key_values $name`
log hook $name
if test -z "$commands"; then
echo " nothing to do"
return 0
fi
local old_ifs=$IFS
IFS=$'\n'
for cmd in $(echo "$commands"); do
IFS="$old_ifs"
# Each hook is run in a specific working directory and has access to various environment variables.
# `$DEPLOY_PATH` is always exported and indicates the deployment directory
# (which is not necessarily the same as the hook's working directory).
# Additional user-defined variables may also be made available (see [environment variables](#host-environment-variables)).
#
# Here's an example of how you could use hooks to cache your `node_modules` directory after every deployment
# to shorten future installation times:
#
# # restore cache (if present)
# deploy tar -xzf $DEPLOY_PATH/cache.gz -C . || exit 0
# # install new dependencies
# deploy npm install --production
# # update cache
# post-deploy tar -czf $DEPLOY_PATH/cache.gz node_modules
run "cd $dir \
&& $(build_export_env_command) \
&& $cmd 2>&1" \
|| abort $name hook failed
IFS=$'\n'
done
IFS="$old_ifs"
}
# See the [`setup`](#setup) and [`rev`](#deploy) commands to learn exactly when and where hooks are executed.
# ## Configuration file
# ### Project
#
# * **`repo `** (or the `$DEPLOY_REPO` variable) defines the Git URL from which your repository will be cloned at setup time.
# * **`path `** (or the `$DEPLOY_PATH` variable) defines the directory into which your project will be deployed on the host.
# *
# **`keep `** (or the `$DEPLOY_KEEP` variable) defines how many releases to keep after deploying.
# Older releases will be deleted. It must be either `all` (the default), or an integer greater than zero.
#
# If it's a number, it indicates the number of releases that should be kept **after successful deployment**.
# For example, with `keep` set to 3, if there are 5 old releases deployed before running the script, 3 will be deleted
# (so that 2 old releases and the new one being deployed, 3 in total, remain).
#
# It's also possible to manually trigger cleanup with the [cleanup](#cleanup) command.
# ### SSH connection
#
# Various SSH options can be specified through the configuration file or environment variables:
build_ssh_command() {
# * **`host `** (or the `$DEPLOY_HOST` variable) is mandatory for all environments.
# It indicates which host or IP address to connect to.
local host=$(get_last_config_key_value host)
require_options host
local url=$host
# * **`user `** (or the `$DEPLOY_USER` variable) specifies the user to connect as.
# By default, you connect with the same name as your local user.
local user=$(get_last_config_key_value user)
test -n "$user" && url="$user@$url"
# * **`identity `** (or the `$DEPLOY_IDENTITY` variable) specifies a file from which the private key for public key authentication is read.
local identity="`get_last_config_key_value identity`"
test -n "$identity" && identity="-i $identity"
# * **`forward-agent yes`** (or the `$DEPLOY_FORWARD_AGENT` variable) enables agent forwarding.
local forward_agent="`get_last_config_key_value forward-agent`"
test -n "$forward_agent" && forward_agent="-A"
# * **`port `** (or the `$DEPLOY_PORT` variable) specifies the host port to connect to.
local port="`get_last_config_key_value port`"
test -n "$port" && port="-p $port"
# * **`tty yes`** (or the `$DEPLOY_TTY` variable) forces pseudo-terminal allocation.
local tty="`get_last_config_key_value tty`"
test -n "$tty" && tty="-t"
echo "ssh $tty $forward_agent $port $identity $url"
}
# Commands executed on the host through SSH will be logged to the console.
run() {
log_command "$@"
run_no_log "$@"
}
# For some commands, the output may be retrieved and displayed in a more user-friendly manner.
run_no_log() {
local shell
shell="$(build_ssh_command 2>&1)"
test $? -eq 0 || abort "$shell"
$shell $@
}
# ### Host environment variables
#
# You can define environment variables that will be exported on the host when executing hooks.
build_export_env_command() {
# The `$DEPLOY_PATH` variable is always exported and indicates the deployment directory
# configured by the user with the `path` config key or the local `$DEPLOY_PATH` variable.
local path="$(get_last_config_key_value path)";
local env_command="export DEPLOY_PATH=$path"
# * **`env =...`** defines one or multiple environment variables to export on the host when running hooks.
#
# # deploy.conf
# env FOO=BAR
# env BAZ=QUX CORGE=GRAULT
env_command="$env_command $(echo `get_all_config_key_values env`|sed 's/\n/ /g')"
# * **`forward-env ...`** defines the name(s) of one or multiple local environment variables
# to export on the host when running hooks.
#
# # deploy.conf
# forward-env FOO
# forward-env BAR BAZ QUX
for forward_env in $(echo `get_all_config_key_values forward-env`|sed 's/\n/ /g'); do
env_command="$env_command $forward_env=$(echo "${!forward_env}"|sed 's/ /\\ /g')"
done
echo "$env_command"
}
# If you have a **`.env`** file in your local project directory, it will automatically be sourced.
# This can be handy to create local variables that you can forward to the host.
#
# # .env
# export FOO=BAR
# export YEAR=$(date "+%Y")
#
# (Note that if you use the `-C, --chdir` command line option or the `$DEPLOY_CHDIR` variable,
# the `.env` file is read *after* the working directory is changed, so setting `$DEPLOY_CHDIR`
# in the file has no effect.)
load_env_files() {
test -f .env && { test -r .env || abort "environment file .env is not readable"; }
test -f .env && { . .env || abort "an error occurred while sourcing .env"; }
# If you have an environment file named after the environment, it will also be sourced.
# For example, when deploying in the "production" environment, a **`.env.production`** file
# would be sourced if present in the local project directory.
#
# If both `.env` and `.env.production` are present, they are sourced in that order.
if test -n "$DEPLOY_ENV"; then
local file=".env.$DEPLOY_ENV"
test -f "$file" && { test -r "$file" || abort "environment file $file is not readable"; }
test -f "$file" && { . "$file" || abort "an error occurred while sourcing $environment_specific_file"; }
fi
}
# ## Command line options & environment variables
#
# The properties in the configuration file can also be specified through *command line options* or *environment variables*.
#
# For example, the `repo` property in the configuration file can also be specified:
#
# * With the `-r, --repo ` command line option.
# * With the `$DEPLOY_REPO` environment variable.
#
# Their precedence is as follows:
#
# * The command line option (e.g. `-r, --repo `) always takes precedence if specified.
# * Next, the environment variable (e.g. `$DEPLOY_REPO`) takes precedence over the configuration file.
# * Otherwise, the value in the configuration file (e.g. `repo`) is used.
DEPLOY_OPTIONS_MAP=(
# ### General options
#
# These options are valid for most sub-commands:
# * **`-C, --chdir `** (or the `$DEPLOY_CHDIR` variable) changes **deploy**'s *local* working directory before loading the configuration file.
C:chdir:dir
# * **`--color always|never|auto`** (or the `$DEPLOY_COLOR` variable) enables/disables colors in the output of the script.
#
# This defaults to `auto`, which only enables colors if the current terminal is interactive.
:color:mode
# * **`-c, --config `** (or the `$DEPLOY_CONFIG` variable) allows you to set a custom path for the configuration file (defaults to `./deploy.conf`).
c:config:path
# * **`--help`** prints usage information and exits.
h:help
# * **`-v, -V, --version`** prints the current version and exits.
V:version
v:version
# * **`-y, --yes`** (or the `$DEPLOY_YES` variable) will automatically accept all confirmation prompts
# (only valid for the `rev`, `cleanup` and `update` sub-commands).
#
# **Use with caution:** old releases may be deleted with the `rev` or `cleanup` sub-commands if a [`keep` option](#keep) is configured.
y:yes
# ### Project options
#
# These options configure where and how the project is deployed on the remote host:
# * **`-r, --repo `** (or the `$DEPLOY_REPO` variable) sets the Git URL to fetch the project's source code from when deploying.
r:repo:url
# * **`-P, --path `** sets the remote path to deploy the project to on the host
# (this is the path to the directory managed by **deploy**, not to the current release).
P:path:dir
# * **`-k, --keep `** (or the `$DEPLOY_KEEP` variable) changes the number of old releases that are kept
# after successful deployment (see the [`keep` option](#keep)).
k:keep:n
# * **`-f, --force`** (or the `$DEPLOY_FORCE` variable) forces deployment to proceed even when you have uncommitted changes.
f:force
# ### SSH options
#
# These options are valid for all commands which connect to the remote host through SSH
# (`setup`, `rev`, `console`, `exec`, `list` and `cleanup`):
# * **`-A, --forward-agent`** (or the `$DEPLOY_FORWARD_AGENT` variable) enables forwarding of the authentication agent connection.
#
# **Use with caution.**
# Users with the ability to bypass file permissions on the remote host (for the agent's UNIX-domain socket)
# can access the local agent through the forwarded connection.
# An attacker cannot obtain key material from the agent,
# however they can perform operations on the keys that enable them to authenticate
# using the identities loaded into the agent.
A:forward-agent
# * **`-H, --host `** (or the `$DEPLOY_HOST` variable) sets the host to connect to.
H:host:address
# * **`-i, --identity `** (or the `$DEPLOY_IDENTITY` variable) selects a file from which the identity (private key)
# for public key authentication is read.
#
# The default is `~/.ssh/id_dsa`, `~/.ssh/id_ecdsa`, `~/.ssh/id_ed25519` and `~/.ssh/id_rsa`.
i:identity:file
# * **`-p, --port `** (or the `$DEPLOY_PORT` variable) sets the port to connect to.
p:port:n
# * **`-t, --tty`** (or the `$DEPLOY_TTY` variable) forces pseudo-terminal allocation.
#
# This can be used to execute arbitrary screen-based programs on a remote machine,
# which can be very useful, e.g. when implementing menu services.
t:tty
# * **`-u, --user `** (or the `$DEPLOY_USER` variable) sets the remote user to connect as.
u:user:name
# ### Self-update options
#
# These options can be used by **deploy** to update itself with the `update` sub-command:
# * **`--update-path `** (or the `$DEPLOY_UPDATE_PATH` variable) sets the path to save the updated script when using the **update** command.
#
# This overrides the `--update-prefix` or `$DEPLOY_UPDATE_PREFIX` options.
:update-path:file
# * **`--update-prefix `** (or the `$DEPLOY_UPDATE_PREFIX` variable) sets the base directory when using the **update** command.
#
# The updated script will be saved in `bin/deploy` relative to this path.
# (Note that setting `--update-path` or `$DEPLOY_UPDATE_PATH` overrides this option.)
:update-prefix:dir
)
# ## Command line options format
#
# Note that all of **deploy**'s command line options can be used anywhere in a command.
# The following 3 commands are equivalent:
#
# * `deploy -u deploy production --keep 3 --yes master`
# * `deploy -u deploy --keep 3 --yes production master`
# * `deploy production master -u deploy --keep 3 --yes`
parse_script_options() {
local i=0
while test $i -lt "$(script_arg_count)"; do
local option="$(script_arg $i)"
if [[ "$option" != -* ]]; then
((i++))
continue
fi
local option_name=
local option_value=
local option_args_count=1
# ### Short option format
#
# Some options have a short form which may be specified with **one hyphen**,
# e.g. `-h` to display usage or `-f` to force deployment with uncommitted local changes.
if [[ "$option" =~ ^-[a-zA-Z](=..*)?$ ]]; then
option_name="$(get_option_name "${option:0:2}")"
# If a short option takes a value, it may be provided either as the next argument
# (e.g. `-k 5`) or with an equal sign (e.g. `-k=5`).
if [[ "$option" =~ ^..= ]]; then
option_value="${option:3}"
fi
# ### Long option format
#
# All options have a long form which may be specified with **two hyphens**,
# e.g. `--help` or `--force`.
elif [[ "$option" =~ ^--[a-zA-Z][a-zA-Z]*(-[a-zA-Z][a-zA-Z]*)*(=..*)?$ ]]; then
local long_option="$(echo "$option"|sed 's/=.*$//')"
option_name="$(get_option_name "$long_option")"
# If a long option takes a value, it may be provided either as the next argument
# (e.g. `--keep 5`) or with an equal sign (e.g. `--keep=5`).
if [[ "$option" =~ ^[^=]*= ]]; then
option_value="$(echo "$option"|sed 's/^[^=]*=//')"
fi
else
# ### Invalid options
#
# The following errors will cause the script to be interrupted and print an error:
#
# * Badly formatted or unknown options (e.g. `---foo`, `-bar`, `--baz`).
abort "unsupported option format '$option'"
fi
local option_arity="$(get_option_arity "$option_name")"
if test $option_arity -lt 0 || test $option_arity -gt 1; then
abort "option arity $option_arity is not supported"
# * Options that take no value specified with a value (e.g. `--help=3`).
elif test -n "$option_value" && test $option_arity -eq 0; then
abort "option $(describe_option "$option_name") does not take a value"
elif test -z "$option_value" && test $option_arity -eq 1; then
option_value="$(script_arg $((i+1)))"
option_args_count=2
elif test -z "$option_value" && test $option_arity -eq 0; then
option_value=1
fi
# * Options that take a value specified without their value.
test -z "$option_value" && test $option_arity -ge 1 \
&& abort "option $(describe_option "$option_name") requires a value"
if [[ "$option_name" == "help" ]]; then
usage; exit 0
elif [[ "$option_name" == "version" ]]; then
echo $VERSION; exit 0
fi
# * Duplicate options (e.g. both `-k 5` and `--keep 3`).
script_has_option "$option_name" && abort "option $(describe_option "$option_name") can only be given once"
DEPLOY_OPTIONS_VALUE_MAP="$(printf "$DEPLOY_OPTIONS_VALUE_MAP\n$option_name $option_value")"
script_remove_args $i $option_args_count
done
}
apply_script_options() {
local options="$@"
test -z "$options" && options="$(echo "$DEPLOY_OPTIONS_VALUE_MAP"|cut -d ' ' -f 1|tr '\n' ' ')"
for option in $options; do
local variable_name="$(get_config_key_env_var "$option")"
local variable_value="$(echo "$DEPLOY_OPTIONS_VALUE_MAP"|grep "^${option} "|cut -d ' ' -f 2-999)"
export $variable_name="$variable_value"
done
}
main() {
parse_script_options
# ### Changing the working directory
#
# By default, **deploy** looks in the current directory to find its configuration file
# and optional environment files. This can be customized by setting the `-c, --chdir `
# command line option or the `$DEPLOY_CHDIR` variable.
#
# If this option is set, **deploy** will first move to that directory before sourcing
# the environment files and reading the configuration file.
apply_script_options chdir
validate_working_directory
test -n "$DEPLOY_CHDIR" && cd "$DEPLOY_CHDIR"
apply_script_options
local subcommand=
while script_has_args && test -z "$subcommand"; do
DEPLOY_COMMAND="$(script_arg)"
case "$DEPLOY_COMMAND" in
exec) subcommand=exec_remote_command ;;
cleanup) subcommand=clean_up_old_releases ;;
console) subcommand=open_remote_console ;;
setup) subcommand=set_up_host ;;
list) subcommand=list_releases ;;
rev) subcommand=deploy_rev ;;
update) subcommand=update_self ;;
config) subcommand=print_config_or_value ;;
config-all) subcommand=print_config_values ;;
config-section) subcommand=print_config_section ;;
*)
if test -z "$DEPLOY_ENV"; then
DEPLOY_ENV="$DEPLOY_COMMAND"
else
break
fi
;;
esac
script_shift
done
load_env_files
# ## Default command
#
# **deploy** is usually used this way:
#
# * `deploy prod setup`
# * `deploy prod list`
# * `deploy prod rev master`
#
# However, since the most often used sub-command is `rev`,
# it is also the **default command**. The following 2 commands are equivalent:
#
# * `deploy prod master`
# * `deploy prod rev master`
if test -z "$subcommand"; then
subcommand=deploy_rev
DEPLOY_COMMAND=rev
fi
$subcommand
}
# ## Sub-commands
#
# These are the commands you will use to set up and deploy your projects.
#
# Note that most (but not all) of **deploy**'s sub-commands require an environment to be specified before the actual command name, for example:
#
# ```sh
# deploy setup # error! no environment
# deploy production setup # all good
# ```
require_env() {
test -n "$DEPLOY_ENV" || abort " required"
config_section_exists $DEPLOY_ENV || abort "[$DEPLOY_ENV] config section not defined"
}
require_config() {
validate_config_file
}
require_no_args() {
if script_has_args; then
local message="$(printf "command '$DEPLOY_COMMAND' takes no more arguments\n$(script_arg_count) extra argument")"
test $(script_arg_count) -ne 1 && message="${message}s"
abort "$message found: ${DEPLOY_ARGS[@]}"
fi
}
require_options() {
local option_names="$@"
for option_name in $option_names; do
local option_value=$(get_last_config_key_value "$option_name")
test -n "$option_value" && continue
log_error
log_error "The '$option_name' option is required by the '$DEPLOY_COMMAND' command."
log_error "Configure it one of the following ways:"
log_error
log_error "* Add the '$option_name' key to the [$DEPLOY_ENV] section of the $DEPLOY_CONFIG configuration file."
log_error "* Set the \$$(get_config_key_env_var "$option_name") environment variable."
log_error "* Add the $(describe_option "$option_name") command line option to the command."
abort "$DEPLOY_COMMAND interrupted"
done
}
#
# ### ` setup`
#
# Perform **first-time setup** tasks on the host before deployment.
# You should only have to run this once before deploying the first time.
#
# ```sh
# deploy production setup
# ```
#
# **deploy** will create the following structure for you in the deployment directory defined by the `path` config key (or the `$DEPLOY_PATH` variable):
# * `$DEPLOY_PATH/releases` will contain each deployment's files
# * `$DEPLOY_PATH/repo` will be a bare clone of your Git repository
# * `$DEPLOY_PATH/tmp` will be used to store temporary files during deployment
#
# This phase has 3 hooks that are all executed in the deployment directory.
# Note that this directory **must already exist** on the host and be writable by the user you connect as.
#
# This is what **deploy** will do during setup:
set_up_host() {
require_env
require_no_args
validate_deploy_options repo
require_options path repo
local path="$(get_last_config_key_value path)"
local repo="$(get_last_config_key_value repo)"
# * Run user-defined `pre-setup` hooks (if any).
run_hooks pre-setup "$path"
# * Create the `releases` and a `tmp` directories if they don't exist already.
log running setup
run "{ test -d $path || exit 1; } && mkdir -p $path/releases $path/tmp" \
|| abort "failed to set up directories; make sure $path exists and is writable"
# * Clone the repository into the `repo` directory if it isn't already there.
log cloning repo
run "test -d $path/repo || { git clone --bare $repo $path/repo || exit 1; }" \
|| abort "failed to clone repo"
# * Run user-defined `post-setup` hooks (if any).
run_hooks post-setup "$path"
echo
log_success "setup complete"
echo
}
#
# ### ` rev [rev]`
#
# **Deploy a new release** from the latest changes in the Git repository.
deploy_rev() {
require_env
# You must provide a Git revision, i.e. a **commit, branch or tag** to deploy,
# either as the `[rev]` argument, with the `rev` config key or the `$DEPLOY_REV` variable.
#
# If your Git repository is **private**, make sure that the deployment user has access to it.
local rev="$(get_last_config_key_value rev)"
if script_has_args; then
rev="$(script_arg)"
script_shift
fi
test -n "$rev" || abort "no commit, branch or tag specified"
require_no_args
validate_deploy_options force keep yes
require_options path
# For each deployment, a new release directory will be created in `releases` in the deployment directory.
# The name of a release directory is the current date and time in the `YYYY-MM-DD-HH-MM-SS` format.
# (You can list deployed releases with the [list command](#list).)
local path=$(get_last_config_key_value path)
local release=$(date -u '+%Y-%m-%d-%H-%M-%S')
local current_dir="$path/current"
local release_dir="$path/releases/$release"
local tmp_dir="$path/tmp"
local keep=$(get_last_config_key_value keep)
# Note that **deploy** will refuse to deploy unless all your local changes are committed and pushed.
# You can override this behavior with the `-f|--force` option (or the `$DEPLOY_FORCE` variable).
require_no_local_changes
# This is what **deploy** will do during deployment:
log deploying
echo " revision: $rev"
echo " release: $release"
echo " keep: ${keep:-all}"
log checking current release
local get_current_release_dir_command="readlink $path/current"
log_command "$get_current_release_dir_command"
local current_release_dir
current_release_dir="$(run_no_log "$get_current_release_dir_command" 2>&1)"
test $? -ne 0 && test -n "$current_release_dir" && abort "$current_release_dir"
if test -n "$current_release_dir"; then
log_color $COLOR_CYAN "$current_release_dir"
else
log_color $COLOR_CYAN "none (pre-deploy hooks will not be run)"
fi
# * If a [`keep` option](#keep) is configured, all previously deployed releases are listed,
# with those to be deleted in red, and those to be kept and the current release in green, and
# confirmation is asked that they may be deleted if the deployment is successful.
prepare_to_delete_old_releases deploy
# * User-defined pre-deploy hooks (if any) are run **in the directory of the previous release**.
# You may define **pre-deploy hooks** to perform any task you might want to do before the actual deployment
# (e.g. you might want to stop the currently running version or put it into maintenance mode).
if test -n "$current_release_dir"; then
run_hooks pre-deploy "$current_release_dir"
else
log hook pre-deploy
log_color $COLOR_CYAN "skipping because there is no current release"
fi
# * The latest changes are fetched from the repository.
log fetching updates
run "cd $path/repo \
&& git fetch origin '+refs/heads/*:refs/heads/*' \
&& git rev-list -n 1 $rev" \
|| abort "could not fetch changes"
# * The new release directory is created and the source is extracted at the specified revision into it.
log "extracting source at $rev"
run "cd $path/repo \
&& mkdir -p $release_dir \
&& git archive --output $tmp_dir/release-$release.tar $rev \
&& tar -C $release_dir -x --file $tmp_dir/release-$release.tar \
&& rm -f $tmp_dir/release-*.tar" \
|| abort "could not extract source from repository"
# * User-defined deploy hooks (if any) are run in the new release directory.
# You should define **deploy hooks** to build your application or install its dependencies at this stage.
run_hooks deploy $release_dir
# * A symlink of the new release directory is created as `current` in the deployment directory.
log linking current
run "ln -fns $release_dir $current_dir" \
|| abort "could not create current symlink"
# * User-defined post-deploy hooks (if any) are run in the current release directory
# (which is now the same as the new release directory that was just created).
#
# You should define **post-deploy hooks** to execute or start your application at this stage.
run_hooks post-deploy $current_dir
# * Old releases are deleted according to the `keep` option (if configured and any were found).
test -n "$releases_to_delete" && delete_old_releases "$releases_to_delete"
echo
log_success successfully deployed
echo
}
require_no_local_changes() {
if test -z "$DEPLOY_FORCE"; then
git --no-pager diff --exit-code --quiet || abort "commit or stash your changes before deploying"
git --no-pager diff --exit-code --quiet --cached || abort "commit your staged changes before deploying"
[ -z "`git rev-list @{upstream}.. -n 1`" ] || abort "push your changes before deploying"
fi
}
# ### `[env] config [key]`
#
# Print values from your `deploy.conf` configuration file.
print_config_or_value() {
# * If called with **no environment and no argument**, it prints the whole configuration file:
#
# ```sh
# $> deploy config
# []
# user dev
#
# [production]
# user root
# port 222
# ```
if ! script_has_args; then
validate_default_options
test -z "$DEPLOY_ENV" || abort "this commands only supports both [env] and [key] arguments or neither"
require_config
cat "$DEPLOY_CONFIG"
# * If called with **the environment and a key**, it prints the last value of that key for that environment
# (only the last value is printed even for a multiple-value key):
#
# ```sh
# $> deploy production config user
# root
# ```
#
# Exits with status 1 if no value is found for the key.
else
local key="$(script_arg)"
script_shift
require_env
require_no_args
validate_default_options
require_config
local value=$(get_last_config_key_value "$key")
test -n "$value" && echo "$value"
fi
}
# ### ` config-all `
#
# Print all values of a config key in the current environment and its inherited environments
# (all values are printed even for a single-value key):
#
# ```sh
# $> deploy production config-all user
# dev
# root
# ```
#
# Exits with status 1 if no value is found for the key.
print_config_values() {
require_env
local key="$(script_arg)"
script_shift
require_no_args
validate_default_options
require_config
test -n "$key" || abort " missing"
local values=$(get_all_config_key_values "$key")
test -n "$values" && echo "$values"
}
# ### `[env] config-section`
#
# Print all values of a config section "as is" (with no inheritance).
# If no environment is specified, the default config section is printed.
#
# Exits with status 1 if the config section is not found (including the default one).
print_config_section() {
require_no_args
validate_default_options
require_config
config_section_exists "$DEPLOY_ENV" && get_config_section "$DEPLOY_ENV"
}
# ### ` console [path]`
#
# Launch an interactive **ssh session** on the host.
open_remote_console() {
require_env
# * If an **absolute path** is specified, the session starts there.
#
# ```sh
# deploy production console /var/www
# ```
local path="$(script_arg)"
script_shift
require_no_args
validate_deploy_options
# * If **no path** is specified, the session starts in the deployment directory.
#
# ```sh
# deploy production console
# ```
if test -z "$path"; then
require_options path
path="$(get_last_config_key_value path)"
# * If a **relative path** is specified, it starts there relative to the deployment directory.
#
# ```sh
# deploy production console current
# ```
elif [[ ! "$path" =~ ^\/ ]]; then
require_options path
path="$(get_last_config_key_value path)/$path"
fi
test -n "$path" || abort "path is required"
local shell
shell="$(build_ssh_command 2>&1)"
test $? -eq 0 || abort "$shell"
log_command "$shell -t \"cd $path; \$SHELL -il\""
$shell -t "cd $path; \$SHELL -il"
}
#
# ### ` cleanup`
#
# Deletes old releases based on the [`keep` option](#keep).
#
# The number of releases to keep is determined from the `keep` configuration file option,
# the `$DEPLOY_KEEP` variable or the `-k|--keep` command line option.
#
# The releases to delete will be listed and confirmation will be asked before they are actually deleted.
clean_up_old_releases() {
require_env
require_no_args
validate_deploy_options keep yes
require_options path
local keep=$(get_last_config_key_value keep)
log cleaning up
echo " keep: ${keep:-all}"
prepare_to_delete_old_releases cleanup
if test -z "$releases_to_delete"; then
echo
log_success "nothing to clean up"
echo
else
delete_old_releases
echo
fi
}
# ### ` exec `
#
# **Execute** the specified command on the host.
#
# ```sh
# deploy production exec ps -ef
# ```
exec_remote_command() {
require_env
validate_deploy_options
require_options path
local path="$(get_last_config_key_value path)"
run "cd $path && $(build_export_env_command) && ${DEPLOY_ARGS[@]}"
}
#
# ### ` list`
#
# List the **releases** that have been deployed on the host.
#
# ```sh
# $> deploy production list
# 2016-12-24-17-45-23
# 2017-01-01-02-03-43
# 2017-04-01-00-00-00
# ```
list_releases() {
require_env
require_no_args
validate_deploy_options
require_options path
local path=`get_last_config_key_value path`
local releases_path="$path/releases"
log "listing releases"
local list_releases_command="ls -1 '$releases_path'"
log_command "$list_releases_command"
local releases_list
releases_list="$(run_no_log "$list_releases_command")"
test $? -eq 0 || abort "could not list releases directory $releases_path; make sure it exists and is readable"
if test -z "$releases_list"; then
echo
echo "No releases found"
echo
exit 0
fi
log "checking current"
local check_current_command="readlink $path/current"
log_command "$check_current_command"
local current_release_dir
current_release_dir="$(run_no_log "$check_current_command")"
echo
for release in $releases_list; do
if [[ "$releases_path/$release" == "$current_release_dir" ]]; then
log_color $COLOR_GREEN "$releases_path/$release (current)"
else
log_color $COLOR_CYAN "$releases_path/$release"
fi
done
echo
local number_of_releases=$(echo "$releases_list"|wc -l)
local message="$number_of_releases release"
test $number_of_releases -ne 1 && message="${message}s"
log_success "$message found"
echo
}
# ### `update [rev]`
#
# Updates **deploy** to the latest version by downloading it from the Git repository and installing it at `/usr/local/bin/deploy` (by default).
update_self() {
if script_has_args; then
DEPLOY_UPDATE_REV="$(script_arg)"
script_shift
fi
test -z "$DEPLOY_ENV" || abort "environment not supported for this command"
require_no_args
validate_update_options
# In addition to the basic requirements, this sub-command also requires `chmod`, `cp` and `mktemp` to be available in the shell.
for com in chmod cp mktemp; do
require_command $com
done
local rev="$DEPLOY_UPDATE_REV"
local path="$DEPLOY_UPDATE_PATH"
local repo="$DEPLOY_UPDATE_REPO"
test -z "$path" && path="$DEPLOY_UPDATE_PREFIX/bin/deploy"
# The installation path must be a writable file or not exist.
# If the installation path does not already exist, its parent must be a writable directory.
test -e "$path" && ! test -f "$path" && abort "$path already exists and is not a file"
test -f "$path" && ! test -w "$path" && abort "$path is not writable"
local dir="$(dirname "$path")"
test -f "$path" || test -e "$dir" || abort "$dir does not exist"
test -f "$path" || test -d "$dir" || abort "$dir is not a directory"
test -f "$path" || test -w "$dir" || abort "$dir is not writable"
log updating
test -z "$DEPLOY_UPDATE_PATH" && echo " prefix: $DEPLOY_UPDATE_PREFIX"
echo " path: $path"
echo " version: $rev"
echo " repository: $repo"
if test -z "$DEPLOY_YES"; then
echo
local update_confirmation=
while ! prompt_is_yes "$update_confirmation" && ! prompt_is_no "$update_confirmation"; do
log_prompt "will update $path; please confirm (yes/no) "
read update_confirmation
done
if prompt_is_no "$update_confirmation"; then
abort "update interrupted"
fi
fi
log "cloning deploy from $rev @ $repo"
# To perform the update, **deploy** will download its Git repository into a temporary directory that will be cleaned up when the update is done (or fails).
local tmp_dir=`mktemp -d -t deploy.XXX`
trap "local_cleanup $tmp_dir" EXIT
# The correct revision of the script is then copied to the installation path and made executable.
cd "$tmp_dir" \
&& git clone "$repo" "$tmp_dir" || abort could not clone "$repo" \
&& { \
git rev-parse --verify $rev &>/dev/null && git reset --hard $rev \
|| { git fetch origin $rev && git reset --hard origin/$rev; }; \
} || abort could not find $rev \
&& cp bin/deploy $path \
&& chmod +x $path \
|| abort could not update deploy
echo
log_success "updated $VERSION -> `./bin/deploy --version`"
echo
}
local_cleanup() {
local tmp_dir="$1"
test -n "$tmp_dir" && test -d "$tmp_dir" && rm -fr "$tmp_dir"
}
# ## Cleaning up old releases
#
# **deploy** creates a new release directory for each deployment.
# By default, it keeps these directories forever, leaving you responsible for cleaning them up.
#
# If given a [`keep` option](#keep), it can do it for you automatically after each deployment.
prepare_to_delete_old_releases() {
local command="$1"
local keep="$(get_last_config_key_value keep)"
local path=$(get_last_config_key_value path)
local release=$(date -u '+%Y-%m-%d-%H-%M-%S')
releases_to_delete=
if test -z "$keep" || [[ "$keep" == "all" ]]; then
return 0
fi
# **deploy** will list the contents of the `releases` directory on the host.
log listing previous releases
local list_releases_command="ls -1 $path/releases"
log_command "$list_releases_command"
local releases_list
releases_list="$(run_no_log "$list_releases_command 2>&1")"
test $? -eq 0 || abort "$releases_list"
local number_of_releases="$(echo "$releases_list"|wc -l)"
# If no old release is found, deployment will proceed normally.
if test -z "$releases_list"; then
log_color $COLOR_CYAN "no old releases to delete"
return 0
fi
# If the number of old releases is equal to or greater than the `keep` number,
# **deploy** will print the list of releases that will be deleted in red.
local releases_to_keep=
if { [[ "$command" == "deploy" ]] && test "$number_of_releases" -lt "$keep"; } || { [[ "$command" != "deploy" ]] && test "$number_of_releases" -le "$keep"; }; then
releases_to_keep="$releases_list"
else
local to_keep=$keep
[[ "$command" == "deploy" ]] && to_keep=$((to_keep-1))
local to_delete=$keep
[[ "$command" != "deploy" ]] && to_delete=$((to_delete+1))
releases_to_keep="$(echo "$releases_list"|tail -n "$to_keep")"
releases_to_delete="$(echo "$releases_list"|reverse|tail -n "+$to_delete"|reverse)"
fi
echo
for old_release in $releases_to_delete; do
log_color $COLOR_RED "$path/releases/$old_release (will be deleted)"
done
# Old releases that will be kept will be shown in green.
for old_release in $releases_to_keep; do
log_color $COLOR_GREEN "$path/releases/$old_release (will be kept)"
done
if [[ "$command" == "deploy" ]]; then
# During deployment, the current release being deployed is also indicated in green
# (as it counts towards the `keep` number).
log_color $COLOR_GREEN "$path/releases/$release (current)"
fi
# If there are any releases to delete, **deploy** will ask for confirmation
# (unless the `-y, --yes` command line option or the `$DEPLOY_YES` variable is specified).
if test -n "$releases_to_delete" && test -z "$DEPLOY_YES"; then
echo
local number_of_releases_to_delete="$(echo "$releases_to_delete"|wc -l)"
local message="$number_of_releases_to_delete release"
test "$number_of_releases_to_delete" -ne 1 && message="${message}s"
while test -z "$delete_releases"; do
log_prompt "$message will be deleted; please confirm (yes/no) "
read delete_releases
# Responding "no" interrupts the deployment and prints a help message explaining
# how to configure the `keep` number.
if prompt_is_no "$delete_releases"; then
echo
echo "Use the \"keep\" configuration file option, the -k|--keep command line option"
echo "or the \$DEPLOY_KEEP variable to change the number of old releases that are kept"
echo "(use the value \"all\" to keep all releases forever)."
abort "$command interrupted"
elif ! prompt_is_yes "$delete_releases"; then
delete_releases=
fi
done
fi
}
# The actual cleanup of old releases is always performed **after successful deployment**.
delete_old_releases() {
if test -z "$releases_to_delete"; then
return 0
fi
log deleting old releases
local path=$(get_last_config_key_value path)
local old_ifs=$IFS
IFS=$'\n'
local directories=
for release in $releases_to_delete; do
IFS="$old_ifs"
directories="$directories \"$path/releases/$release\""
IFS=$'\n'
done
IFS="$old_ifs"
# A single `rm` command is executed on the host through SSH,
# deleting all appropriate old release directories in one go.
run "rm -fr $directories"
local number_of_releases_to_delete="$(echo "$releases_to_delete"|wc -l)"
local message="$number_of_releases_to_delete old release"
test "$number_of_releases_to_delete" -ne 1 && message="${message}s"
echo
log_success "$message deleted"
}
#
# ## Output & exit codes
#
# **deploy** will log various messages indicating what it is doing.
# Those messages are printed in colors by default on interactive terminals
# (this can be disabled with the `--color never` option or `$DEPLOY_COLOR` variable).
echo_color() {
if test -z "$COLORS"; then
if [ "$DEPLOY_COLOR" == "always" ]; then
COLORS=yes
elif [ "$DEPLOY_COLOR" != "never" ] && is_interactive; then
COLORS=yes
fi
fi
local color="$1"
shift
local message="$@"
if [ "$COLORS" == "yes" ]; then
echo -e "\033[${color}m${message}\033[0m"
else
echo "$message"
fi
}
is_interactive() {
test "${-#*i}" != "$-" || test -t 0 || test -n "$PS1"
}
# * **Deployment progress updates** will be printed in bold.
log() {
echo
echo_color $COLOR_BOLD " ○ $@"
}
# * **Commands executed** on the host will be printed in yellow.
log_command() {
echo_color $COLOR_YELLOW "$@" | tr -s ' ' | sed 's/^/ /'
}
# * **Input prompts** will be printed in magenta.
log_prompt() {
echo -n "$(echo_color $COLOR_MAGENTA "$@")" | tr -s ' ' | sed 's/^/ /'
}
# * Some output will be printed in various colors to indicate status.
log_color() {
local color="$1"
shift
local message="$@"
echo_color "$color" "$message" | tr -s ' ' | sed 's/^/ /'
}
# * **Success** message will be printed in green.
log_success() {
echo_color $COLOR_GREEN "$@" | tr -s ' ' | sed 's/^/ /'
}
# * **Errors** will be printed in red.
log_error() {
echo_color $COLOR_RED "$@"|sed 's/^ *//'|sed 's/^/ /' 1>&2
}
# If **deploy** encounters an unrecoverable error, it will log an error and exit with status 1.
abort() {
echo
log_error "$@"
echo
exit 1
}
prompt_is_yes() {
[[ "$1" =~ ^(1|y|yes|t|true)$ ]]
}
prompt_is_no() {
[[ "$1" =~ ^(0|n|no|f|false)$ ]]
}
reverse() {
tac 2> /dev/null || tail -r
}
script_arg() {
local i="$1"
test -z "$i" && i=0
echo "${DEPLOY_ARGS[$i]}"
}
script_arg_count() {
echo ${#DEPLOY_ARGS[@]}
}
script_has_args() {
test ${#DEPLOY_ARGS[@]} -gt 0
}
script_has_option() {
value_is_in "$1" "$(script_option_names)"
}
script_option_names() {
echo "$DEPLOY_OPTIONS_VALUE_MAP"|sed 's/ .*$//'|tr '\n' ' '
}
script_shift() {
local by=$1
test -z "$by" && by=1
DEPLOY_ARGS=("${DEPLOY_ARGS[@]:$by}")
}
script_remove_args() {
local slice_index="$1"
local slice_length="$2"
test -z "$slice_length" && slice_length=1
local arg_count="${#DEPLOY_ARGS[@]}"
local rest_index=$((slice_index+slice_length))
local rest_length=$((arg_count-rest_index))
DEPLOY_ARGS=("${DEPLOY_ARGS[@]:0:$slice_index}" "${DEPLOY_ARGS[@]:$rest_index:$rest_length}")
}
get_option_arity() {
local option_name="$1"
local arity="$(get_option_properties "$option_name"|cut -d ':' -f 3 -s)"
test -n "$arity" && echo 1 || echo 0
}
get_option_name() {
local short_or_long_option="$1"
if [[ "$short_or_long_option" =~ ^-?[A-Za-z]$ ]]; then
local letter="$(echo "$short_or_long_option"|sed 's/^-//')"
echo "${DEPLOY_OPTIONS_MAP[@]}"|tr ' ' '\n'|grep "^${letter}:"|sed 's/^[^:]*://'|sed 's/:.*$//'
else
echo "$short_or_long_option"|sed 's/^-*//'
fi
}
get_option_properties() {
local option_name="$1"
echo "${DEPLOY_OPTIONS_MAP[@]}"|tr ' ' '\n'|grep -E "^[a-zA-Z]?:${option_name}(:|$)"
}
describe_option() {
local option_name="$1"
local description="--$option_name"
local short_option_letter="$(get_option_properties "$option_name"|sed 's/:.*$//')"
test -n "$short_option_letter" && description="-$short_option_letter, $description"
local option_value_name="$(get_option_properties "$option_name"|cut -d ':' -f 3 -s)"
test -n "$option_value_name" && description="$description <$option_value_name>"
echo "$description"
}
value_is_in() {
local value="$1"
shift
local args="$@"
for arg in $args; do
if [[ "$arg" == "$value" ]]; then
return 0
fi
done
return 1
}
validate_color() {
local color="$DEPLOY_COLOR"
[[ "$color" =~ ^(always|never|auto)$ ]] \
|| abort "invalid color option '$color': allowed values are always, never or auto"
}
validate_config_file() {
local file="$DEPLOY_CONFIG"
test -e "$file" || abort "invalid config file: $file does not exist"
test -f "$file" || abort "invalid config file: $file is not a file"
test -r "$file" || abort "invalid config file: $file cannot be read"
}
validate_identity_file() {
local file="$(get_last_config_key_value identity)"
test -z "$file" && return
test -e "$file" || abort "invalid identity file: $file does not exist"
test -f "$file" || abort "invalid identity file: $file is not a file"
test -r "$file" || abort "invalid identity file: $file cannot be read"
}
validate_keep() {
local keep="$(get_last_config_key_value keep)"
test -z "$keep" \
|| [[ "$keep" == "all" ]] \
|| [[ "$keep" =~ ^[1-9][0-9]*$ ]] \
|| abort "keep must be \"all\" or a positive integer greater than zero"
}
validate_port_number() {
local port="$(get_last_config_key_value port)"
test -z "$port" \
|| { [[ "$port" =~ ^[1-9][0-9]*$ ]] && test $port -ge 0 && test $port -le 65535; } \
|| abort "port must be an integer between 0 and 65535"
}
validate_working_directory() {
local dir="$DEPLOY_CHDIR"
test -z "$dir" && return
test -e "$dir" || abort "invalid working directory: '$dir' does not exist"
test -d "$dir" || abort "invalid working directory: '$dir' is not a directory"
test -x "$dir" || abort "invalid working directory: '$dir' cannot be accessed"
}
validate_options() {
whitelist_options "$@"
value_is_in color "$@" && validate_color
value_is_in config "$@" && validate_config_file
value_is_in identity "$@" && validate_identity_file
value_is_in keep "$@" && validate_keep
value_is_in port "$@" && validate_port_number
}
validate_default_options() {
validate_options chdir color config "$@"
}
validate_deploy_options() {
validate_default_options forward-agent host identity path port tty user "$@"
}
validate_update_options() {
validate_options color update-path update-prefix yes
}
whitelist_options() {
local allowed_options="$@"
for option_name in $(script_option_names); do
value_is_in "$option_name" "$allowed_options" \
|| abort "option $(describe_option "$option_name") is not applicable to command '$DEPLOY_COMMAND'"
done
}
main $@
# ## About this page
#
# Documentation generated with [Docco](http://ashkenas.com/docco/).
#