Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app that manages a fleet of T5 Plutonium servers running on Komodo. Same push and beta workflow as the shell script, with a live SSE pipeline view, a !rt-token-gated auth modal that hard-pauses deployments on stale tokens, per-file audit diffs, and a Komodo-container deployment target. - Push/beta engines port the exact filter, held-by-beta, conflict, and stale-backup rules from the shell script - SQLite (better-sqlite3) replaces script_metadata.json; a one-shot importer moves legacy state over on first boot - 16 vitest tests cover push add/update/skip, beta open→rollback, open→promote, conflict guards, sweep frozen/manual branches, and IW4M login/status parsing - docker/ + komodo/stack.toml deploy as a managed container on the same host as the game servers, /root/zombies bind-mounted at the same path the shell script used
1497 lines
53 KiB
Bash
1497 lines
53 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# push_scripts.sh - push shared GSC scripts out to every T5 server
|
|
#
|
|
# source: /root/zombies/shared/scripts
|
|
# staging: /root/zombies/shared/scripts/betas <- drop scripts to test here
|
|
# open: /root/zombies/shared/scripts/betas/<beta> <- one folder per open beta
|
|
# dest: /root/zombies/t5/<server>/runtime/plutonium/storage/t5/raw/scripts/sp
|
|
# metadata: script_metadata.json next to this script <- source of truth for betas
|
|
#
|
|
# The betas folder is never pushed to a server.
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
SRC="${SRC:-/root/zombies/shared/scripts}"
|
|
SERVERS_ROOT="${SERVERS_ROOT:-/root/zombies/t5}"
|
|
REL_DEST="runtime/plutonium/storage/t5/raw/scripts/sp"
|
|
METADATA="${METADATA:-$SCRIPT_DIR/script_metadata.json}"
|
|
LEGACY_STATE_DIR="${STATE_DIR:-/root/zombies/.betas}"
|
|
BETA_SRC="$SRC/betas"
|
|
|
|
# integrations: read from env, or from push_scripts.env next to this script
|
|
ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/push_scripts.env}"
|
|
if [[ -r "$ENV_FILE" ]]; then
|
|
# shellcheck disable=SC1090
|
|
set -a; source "$ENV_FILE"; set +a
|
|
fi
|
|
|
|
KOMODO_ADDRESS="${KOMODO_ADDRESS:-}"
|
|
KOMODO_API_KEY="${KOMODO_API_KEY:-}"
|
|
KOMODO_API_SECRET="${KOMODO_API_SECRET:-}"
|
|
IW4M_ADDRESS="${IW4M_ADDRESS:-}"
|
|
IW4M_CLIENT_ID="${IW4M_CLIENT_ID:-}"
|
|
IW4M_PASSWORD="${IW4M_PASSWORD:-}"
|
|
HTTP_TIMEOUT="${HTTP_TIMEOUT:-10}"
|
|
DEPLOY_KEEP_DAYS="${DEPLOY_KEEP_DAYS:-7}"
|
|
|
|
DEPLOY_ACTION="push"
|
|
IW4M_STATUS_CACHE=""
|
|
IW4M_JAR=""
|
|
IW4M_ERR=""
|
|
CHANGED_SERVERS=()
|
|
|
|
MODE=push
|
|
DRY_RUN=0
|
|
DELETE=0
|
|
CREATE=0
|
|
BETA_NAME=""
|
|
|
|
# ------------------------------------------------------------------ style ---
|
|
|
|
if [[ -t 1 && "${NO_COLOR:-}" == "" ]]; then
|
|
B=$'\e[1m'; D=$'\e[2m'; R=$'\e[0m'
|
|
GN=$'\e[32m'; YL=$'\e[33m'
|
|
else
|
|
B=""; D=""; R=""; GN=""; YL=""
|
|
fi
|
|
WIDTH=66
|
|
|
|
rule() { printf '%s%s%s\n' "$D" "$(printf '─%.0s' $(seq 1 $WIDTH))" "$R"; }
|
|
|
|
hdr() {
|
|
local title="$1" note="${2:-}" pad
|
|
pad=$(( WIDTH - ${#title} - ${#note} - 1 ))
|
|
(( pad < 1 )) && pad=1
|
|
printf ' %s%s%s%*s%s%s%s\n' "$B" "$title" "$R" "$pad" "" "$D" "$note" "$R"
|
|
}
|
|
|
|
row() {
|
|
local col="$1" name="$2" note="${3:-}" pad
|
|
pad=$(( WIDTH - ${#name} - ${#note} - 3 ))
|
|
(( pad < 1 )) && pad=1
|
|
printf ' %s●%s %s%*s%s%s%s\n' "$col" "$R" "$name" "$pad" "" "$D" "$note" "$R"
|
|
}
|
|
|
|
field() { printf ' %s%-9s%s %s\n' "$D" "$1" "$R" "$2"; }
|
|
detail(){ printf ' %s%-8s%s %s\n' "$D" "$1" "$R" "$2"; }
|
|
warn() { printf ' %s! %s%s\n' "$YL" "$1" "$R"; }
|
|
note() { printf ' %s%s%s\n' "$D" "$1" "$R"; }
|
|
|
|
field_wrap() {
|
|
local label="$1" text="$2"
|
|
printf '%s\n' "$text" | fold -s -w $(( WIDTH - 12 )) \
|
|
| awk -v l="$label" -v d="$D" -v r="$R" \
|
|
'NR==1 {printf " %s%-9s%s %s\n", d, l, r, $0; next}
|
|
{printf " %s%-9s%s %s\n", d, "", r, $0}'
|
|
}
|
|
|
|
ago() {
|
|
local when now diff
|
|
when="$(date -d "$1" +%s 2>/dev/null || printf '0')"
|
|
[[ "$when" == "0" ]] && { printf '%s' "$1"; return; }
|
|
now="$(date +%s)"; diff=$(( now - when ))
|
|
if (( diff < 60 )); then printf 'just now'
|
|
elif (( diff < 3600 )); then printf '%dm ago' $(( diff / 60 ))
|
|
elif (( diff < 86400 )); then printf '%dh ago' $(( diff / 3600 ))
|
|
else printf '%dd ago' $(( diff / 86400 )); fi
|
|
}
|
|
|
|
when_str() {
|
|
local pretty
|
|
pretty="$(date -d "$1" '+%b %-d, %H:%M' 2>/dev/null || printf '%s' "$1")"
|
|
printf '%s (%s)' "$pretty" "$(ago "$1")"
|
|
}
|
|
|
|
plural() { [[ "$1" == "1" ]] && printf '%s' "$2" || printf '%ss' "$2"; }
|
|
now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
|
|
# ------------------------------------------------------------------- args ---
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
push_scripts.sh - push shared GSC scripts out to every T5 server
|
|
|
|
source: $SRC
|
|
staging: $BETA_SRC
|
|
dest: $SERVERS_ROOT/<server>/$REL_DEST
|
|
metadata: $METADATA
|
|
|
|
Usage: push_scripts.sh [options] [server ...]
|
|
|
|
-n, --dry-run show what would change, change nothing
|
|
-d, --delete delete files at the dest that no longer exist in source
|
|
-c, --create create the sp directory on servers that don't have one
|
|
-h, --help this
|
|
|
|
Betas:
|
|
|
|
Drop the scripts you want to test straight into $BETA_SRC,
|
|
then open a beta. Only those staged files go into it - the rest of the shared
|
|
folder keeps pushing to everyone as normal, and betas/ itself is never pushed.
|
|
|
|
-b, --beta NAME [server ...]
|
|
open a beta. Each staged script lands on the named servers as
|
|
script-beta.gsc, the live copy is parked as script.OLD, and the staged
|
|
files move to betas/<name>/ so the staging folder is free for the next one.
|
|
|
|
-ub, --update-beta NAME
|
|
push a new take on scripts already in an open beta. Stage the new file in
|
|
betas/, run this, and it replaces the copy in betas/<name>/ and on every
|
|
server in that beta. Backups and beta membership are left alone.
|
|
|
|
-r, --rollback NAME undo a beta: delete its scripts from the servers, restore
|
|
every .OLD, and delete betas/<name>/
|
|
-pb, --promote-beta NAME
|
|
keep a beta: clear the beta files and .OLD backups off the servers, move the
|
|
scripts back into the shared folder under their real names, delete
|
|
betas/<name>/, then push to every server
|
|
|
|
-lb, --list-betas show what is staged and every open beta
|
|
|
|
Restarts and deployments:
|
|
|
|
After anything lands on a server, that server is checked against IW4MAdmin.
|
|
Empty servers are restarted through Komodo straight away; servers with players
|
|
on them are left alone and queued, so they can be picked up later.
|
|
|
|
-ad, --active-deployments
|
|
re-check everything still queued, restart whatever is empty now, then
|
|
list what is still waiting. Good as a cron job.
|
|
-ld, --list-deployments show every deployment from the last $DEPLOY_KEEP_DAYS days
|
|
|
|
Credentials come from the environment or from $ENV_FILE:
|
|
KOMODO_ADDRESS, KOMODO_API_KEY, KOMODO_API_SECRET
|
|
IW4M_ADDRESS, and IW4M_CLIENT_ID + IW4M_PASSWORD if your webfront
|
|
requires a login to read /api/status
|
|
Without them the pushes still work, they just aren't checked or restarted.
|
|
|
|
Only one beta may own a given script on a given server at a time.
|
|
Open betas are tracked in $METADATA.
|
|
|
|
Server args match folder names; globs and comma lists both work:
|
|
|
|
push_scripts.sh # push to every server
|
|
push_scripts.sh -n # dry run
|
|
push_scripts.sh 't5_cotd_*' # one group
|
|
push_scripts.sh -b "New HUD Test" "t5_kino_1, t5_ascension_1"
|
|
push_scripts.sh -b "Plugin Test" # no servers = every server
|
|
push_scripts.sh -ub "New HUD Test"
|
|
push_scripts.sh -lb
|
|
push_scripts.sh -r "New HUD Test"
|
|
push_scripts.sh -pb "New HUD Test"
|
|
push_scripts.sh -ad
|
|
push_scripts.sh -ld
|
|
EOF
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
need_name() {
|
|
if [[ -z "${1:-}" || "${1:0:1}" == "-" ]]; then
|
|
echo "$2 needs a beta name, e.g. $2 \"New HUD Test\"" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-n|--dry-run) DRY_RUN=1 ;;
|
|
-d|--delete) DELETE=1 ;;
|
|
-c|--create) CREATE=1 ;;
|
|
-b|--beta) need_name "${2:-}" "$1"; MODE=beta; BETA_NAME="$2"; shift ;;
|
|
-ub|-up|--update-beta) need_name "${2:-}" "$1"; MODE=update; BETA_NAME="$2"; shift ;;
|
|
-r|--rollback) need_name "${2:-}" "$1"; MODE=rollback; BETA_NAME="$2"; shift ;;
|
|
-pb|--promote-beta) need_name "${2:-}" "$1"; MODE=promote; BETA_NAME="$2"; shift ;;
|
|
-lb|--list-betas) MODE=list ;;
|
|
-ad|--active-deployments) MODE=active ;;
|
|
-ld|--list-deployments) MODE=deployments ;;
|
|
-h|--help) usage 0 ;;
|
|
-*) echo "unknown option: $1" >&2; usage 1 ;;
|
|
*) break ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
PATTERNS=()
|
|
for arg in "$@"; do
|
|
IFS=',' read -r -a _parts <<<"$arg"
|
|
for p in "${_parts[@]}"; do
|
|
p="$(printf '%s' "$p" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
if [[ -n "$p" ]]; then PATTERNS+=("$p"); fi
|
|
done
|
|
done
|
|
if [[ ${#PATTERNS[@]} -eq 0 ]]; then PATTERNS=("*"); fi
|
|
|
|
if [[ "$MODE" != list ]]; then
|
|
[[ -d "$SERVERS_ROOT" ]] || { echo "servers dir not found: $SERVERS_ROOT" >&2; exit 1; }
|
|
fi
|
|
[[ -d "$SRC" ]] || { echo "source dir not found: $SRC" >&2; exit 1; }
|
|
|
|
# --------------------------------------------------------------- metadata ---
|
|
|
|
require_jq() {
|
|
command -v jq >/dev/null 2>&1 && return 0
|
|
echo "jq is needed to read $METADATA" >&2
|
|
echo "install it with: apt-get install -y jq" >&2
|
|
exit 1
|
|
}
|
|
|
|
meta_exists() { [[ -f "$METADATA" ]]; }
|
|
|
|
meta_q() { require_jq; jq -r "$@" "$METADATA"; }
|
|
|
|
meta_apply() {
|
|
require_jq
|
|
local tmp
|
|
tmp="$(mktemp "${METADATA}.XXXXXX")"
|
|
if jq "$@" "$METADATA" >"$tmp"; then
|
|
mv -f "$tmp" "$METADATA"
|
|
else
|
|
rm -f "$tmp"
|
|
echo "could not update $METADATA" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
meta_touch() {
|
|
meta_apply --arg now "$(now_iso)" --arg src "$SRC" --arg stg "$BETA_SRC" \
|
|
--arg root "$SERVERS_ROOT" --arg rel "$REL_DEST" \
|
|
'.updated = $now
|
|
| .paths = {shared_scripts:$src, staging:$stg,
|
|
servers_root:$root, server_script_path:$rel}'
|
|
}
|
|
|
|
# build the file, importing any legacy .beta manifests on first run
|
|
meta_init() {
|
|
if meta_exists; then
|
|
if ! meta_q 'has("servers") and has("deployments")' | grep -qx true; then
|
|
meta_apply '.servers //= {} | .deployments //= []'
|
|
fi
|
|
return 0
|
|
fi
|
|
require_jq
|
|
jq -n --arg now "$(now_iso)" --arg src "$SRC" --arg stg "$BETA_SRC" \
|
|
--arg root "$SERVERS_ROOT" --arg rel "$REL_DEST" \
|
|
'{schema:1, generated_by:"push_scripts.sh", updated:$now,
|
|
paths:{shared_scripts:$src, staging:$stg,
|
|
servers_root:$root, server_script_path:$rel},
|
|
betas:{}, servers:{}, deployments:[]}' >"$METADATA"
|
|
migrate_legacy
|
|
}
|
|
|
|
migrate_legacy() {
|
|
local f slug name created base rel sha server had n=0
|
|
[[ -d "$LEGACY_STATE_DIR" ]] || return 0
|
|
for f in "$LEGACY_STATE_DIR"/*.beta; do
|
|
[[ -e "$f" ]] || continue
|
|
slug="$(basename "$f" .beta)"
|
|
name="$(awk -F'\t' '$1=="name"{print $2; exit}' "$f")"
|
|
created="$(awk -F'\t' '$1=="created"{print $2; exit}' "$f")"
|
|
beta_create "$slug" "$name" "$created"
|
|
while IFS=$'\t' read -r _ base rel sha; do
|
|
beta_add_script "$slug" "$base" "$rel" "$sha"
|
|
done < <(awk -F'\t' '$1=="staged"' "$f")
|
|
while IFS=$'\t' read -r _ server rel had; do
|
|
beta_add_deploy "$slug" "$rel" "$server" "$had"
|
|
done < <(awk -F'\t' '$1=="file"' "$f")
|
|
beta_sync_servers "$slug"
|
|
mv "$f" "$f.migrated"
|
|
n=$((n + 1))
|
|
done
|
|
if [[ $n -gt 0 ]]; then
|
|
echo
|
|
note "imported $n open $(plural "$n" beta) from $LEGACY_STATE_DIR into $METADATA"
|
|
note "the old manifests were renamed to *.beta.migrated"
|
|
fi
|
|
}
|
|
|
|
beta_create() {
|
|
meta_apply --arg slug "$1" --arg name "$2" --arg opened "$3" --arg folder "betas/$1" \
|
|
'.betas[$slug] = {name:$name, folder:$folder, opened:$opened,
|
|
updated:$opened, revision:1, servers:[], scripts:[]}'
|
|
}
|
|
|
|
beta_add_script() { # slug staged_as target sha
|
|
meta_apply --arg slug "$1" --arg staged "$2" --arg target "$3" --arg sha "$4" \
|
|
--arg bfile "$(beta_file "$3")" --arg backup "$(old_file "$3")" \
|
|
'.betas[$slug].scripts += [{staged_as:$staged, target:$target,
|
|
beta_file:$bfile, backup:$backup,
|
|
sha256:$sha, revision:1, deployed:[]}]'
|
|
}
|
|
|
|
beta_add_deploy() { # slug target server had_original(0|1)
|
|
local had=false
|
|
[[ "$4" == "1" || "$4" == "true" ]] && had=true
|
|
meta_apply --arg slug "$1" --arg target "$2" --arg srv "$3" --argjson had "$had" \
|
|
'(.betas[$slug].scripts[] | select(.target==$target) | .deployed)
|
|
+= [{server:$srv, had_original:$had}]'
|
|
}
|
|
|
|
beta_sync_servers() {
|
|
meta_apply --arg slug "$1" \
|
|
'.betas[$slug].servers = ([.betas[$slug].scripts[].deployed[].server] | unique)'
|
|
}
|
|
|
|
beta_bump_script() { # slug target sha
|
|
meta_apply --arg slug "$1" --arg target "$2" --arg sha "$3" --arg now "$(now_iso)" \
|
|
'(.betas[$slug].scripts[] | select(.target==$target) | .sha256) = $sha
|
|
| (.betas[$slug].scripts[] | select(.target==$target) | .revision) += 1
|
|
| .betas[$slug].revision += 1
|
|
| .betas[$slug].updated = $now'
|
|
}
|
|
|
|
beta_drop() { meta_apply --arg slug "$1" 'del(.betas[$slug])'; }
|
|
|
|
beta_slugs() { meta_exists || return 0; meta_q '.betas | keys[]'; }
|
|
beta_has() { meta_exists || return 1; meta_q --arg s "$1" '.betas | has($s)' | grep -qx true; }
|
|
beta_field() { meta_q --arg s "$1" --arg k "$2" '.betas[$s][$k] // ""'; }
|
|
beta_servers() { meta_q --arg s "$1" '.betas[$s].servers[]?'; }
|
|
|
|
# staged_as <TAB> target <TAB> beta_file <TAB> backup <TAB> revision
|
|
beta_scripts() {
|
|
meta_q --arg s "$1" \
|
|
'.betas[$s].scripts[]? | [.staged_as,.target,.beta_file,.backup,(.revision|tostring)] | @tsv'
|
|
}
|
|
|
|
# server <TAB> target <TAB> beta_file <TAB> backup <TAB> had_original
|
|
beta_deploys() {
|
|
meta_q --arg s "$1" \
|
|
'.betas[$s].scripts[]? as $x | $x.deployed[]?
|
|
| [.server,$x.target,$x.beta_file,$x.backup,(.had_original|tostring)] | @tsv'
|
|
}
|
|
|
|
owner_of() { # server target -> slug of the beta holding it
|
|
local out
|
|
meta_exists || return 1
|
|
out="$(meta_q --arg srv "$1" --arg t "$2" \
|
|
'[.betas | to_entries[]
|
|
| select(any(.value.scripts[]?;
|
|
.target==$t and any(.deployed[]?; .server==$srv)))
|
|
| .key][0] // empty')"
|
|
[[ -n "$out" ]] || return 1
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
held_by_beta() { # server -> target <TAB> slug
|
|
meta_exists || return 0
|
|
meta_q --arg srv "$1" \
|
|
'.betas | to_entries[] as $b | $b.value.scripts[]?
|
|
| select(any(.deployed[]?; .server==$srv))
|
|
| "\(.target)\t\($b.key)"'
|
|
}
|
|
|
|
resolve_beta() {
|
|
local slug
|
|
slug="$(slugify "$BETA_NAME")"
|
|
if ! beta_has "$slug"; then
|
|
echo "no open beta called '$BETA_NAME' in $METADATA" >&2
|
|
echo "run 'push_scripts.sh -lb' to see what's open" >&2
|
|
exit 1
|
|
fi
|
|
printf '%s\n' "$slug"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- helpers ---
|
|
|
|
slugify() {
|
|
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//; s/-$//'
|
|
}
|
|
|
|
dest_of() { printf '%s/%s/%s\n' "$SERVERS_ROOT" "$1" "$REL_DEST"; }
|
|
join_list() { sort -u | awk 'NR==1{printf "%s",$0;next}{printf ", %s",$0} END{if(NR)print ""}'; }
|
|
|
|
sha_of() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | awk '{print $1}'
|
|
else
|
|
printf '%s\n' '-'
|
|
fi
|
|
}
|
|
|
|
list_source_files() {
|
|
( cd "$SRC" && find . -path './betas' -prune -o -type f \
|
|
! -path './.git/*' ! -name '*.swp' ! -name '*~' \
|
|
! -name '*.OLD' ! -name '*-beta.*' -printf '%P\n' | sort )
|
|
}
|
|
|
|
staged_files() {
|
|
[[ -d "$BETA_SRC" ]] || return 0
|
|
( cd "$BETA_SRC" && find . -maxdepth 1 -type f \
|
|
! -name '*.swp' ! -name '*~' ! -name '.*' -printf '%P\n' | sort )
|
|
}
|
|
|
|
find_targets() {
|
|
( cd "$SRC" && find . -path './betas' -prune -o -type f -name "$1" -printf '%P\n' | sort )
|
|
}
|
|
|
|
all_servers() {
|
|
local dir
|
|
for dir in "$SERVERS_ROOT"/*/; do
|
|
[[ -d "$dir" ]] || continue
|
|
basename "$dir"
|
|
done
|
|
}
|
|
|
|
match_servers() {
|
|
local dir server p
|
|
for dir in "$SERVERS_ROOT"/*/; do
|
|
[[ -d "$dir" ]] || continue
|
|
server="$(basename "$dir")"
|
|
for p in "${PATTERNS[@]}"; do
|
|
# shellcheck disable=SC2053
|
|
if [[ "$server" == $p ]]; then printf '%s\n' "$server"; break; fi
|
|
done
|
|
done
|
|
}
|
|
|
|
beta_file() {
|
|
local rel="$1" dir base stem ext=""
|
|
dir="$(dirname "$rel")"; base="$(basename "$rel")"
|
|
if [[ "$base" == *.* ]]; then stem="${base%.*}"; ext=".${base##*.}"; else stem="$base"; fi
|
|
if [[ "$dir" == "." ]]; then printf '%s-beta%s\n' "$stem" "$ext"
|
|
else printf '%s/%s-beta%s\n' "$dir" "$stem" "$ext"; fi
|
|
}
|
|
|
|
old_file() {
|
|
local rel="$1" dir base stem
|
|
dir="$(dirname "$rel")"; base="$(basename "$rel")"
|
|
if [[ "$base" == *.* ]]; then stem="${base%.*}"; else stem="$base"; fi
|
|
if [[ "$dir" == "." ]]; then printf '%s.OLD\n' "$stem"
|
|
else printf '%s/%s.OLD\n' "$dir" "$stem"; fi
|
|
}
|
|
|
|
ensure_dest() {
|
|
local server="$1" dest
|
|
dest="$(dest_of "$server")"
|
|
if [[ ! -d "$dest" ]]; then
|
|
if [[ $CREATE -eq 1 && $DRY_RUN -eq 0 ]]; then
|
|
mkdir -p "$dest"
|
|
else
|
|
row "$YL" "$server" "no sp folder - use -c"
|
|
return 1
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ----------------------------------------------------------- integrations ---
|
|
|
|
have_komodo() { [[ -n "$KOMODO_ADDRESS" && -n "$KOMODO_API_KEY" && -n "$KOMODO_API_SECRET" ]]; }
|
|
have_iw4m() { [[ -n "$IW4M_ADDRESS" ]]; }
|
|
integrations_on() { have_komodo && have_iw4m; }
|
|
|
|
# body, then the http status code on the last line
|
|
komodo_call() { # path json-body
|
|
curl -sS -m "$HTTP_TIMEOUT" -X POST "${KOMODO_ADDRESS%/}/$1" \
|
|
-H 'Content-Type: application/json' \
|
|
-H "X-Api-Key: $KOMODO_API_KEY" \
|
|
-H "X-Api-Secret: $KOMODO_API_SECRET" \
|
|
-w '\n%{http_code}' \
|
|
-d "$2" 2>/dev/null
|
|
}
|
|
|
|
# POST /api/client/<id>/login, keep the session cookie for the rest of the run
|
|
iw4m_login() {
|
|
local jar code
|
|
if [[ -z "$IW4M_CLIENT_ID" || -z "$IW4M_PASSWORD" ]]; then
|
|
IW4M_ERR="iw4madmin needs a login"
|
|
return 1
|
|
fi
|
|
[[ -n "$IW4M_JAR" ]] && return 0
|
|
|
|
jar="$(mktemp -t push_scripts_iw4m.XXXXXX)" || return 1
|
|
chmod 600 "$jar"
|
|
code="$(curl -sS -m "$HTTP_TIMEOUT" -o /dev/null -w '%{http_code}' \
|
|
-c "$jar" -X POST \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$(jq -nc --arg p "$IW4M_PASSWORD" '{password:$p}')" \
|
|
"${IW4M_ADDRESS%/}/api/client/${IW4M_CLIENT_ID}/login" 2>/dev/null)" || {
|
|
rm -f "$jar"; IW4M_ERR="iw4madmin unreachable"; return 1; }
|
|
|
|
if [[ "$code" != 2* ]]; then
|
|
rm -f "$jar"
|
|
if [[ "$code" == "401" ]]; then
|
|
IW4M_ERR="iw4madmin login rejected"
|
|
else
|
|
IW4M_ERR="iw4madmin login failed ($code)"
|
|
fi
|
|
return 1
|
|
fi
|
|
IW4M_JAR="$jar"
|
|
return 0
|
|
}
|
|
|
|
iw4m_logout() {
|
|
[[ -n "$IW4M_JAR" ]] || return 0
|
|
curl -sS -m "$HTTP_TIMEOUT" -o /dev/null -b "$IW4M_JAR" -X POST \
|
|
"${IW4M_ADDRESS%/}/api/client/${IW4M_CLIENT_ID}/logout" >/dev/null 2>&1 || true
|
|
rm -f "$IW4M_JAR"
|
|
IW4M_JAR=""
|
|
}
|
|
trap iw4m_logout EXIT
|
|
|
|
iw4m_fetch_status() {
|
|
local out
|
|
if [[ -n "$IW4M_JAR" ]]; then
|
|
out="$(curl -sS -m "$HTTP_TIMEOUT" -b "$IW4M_JAR" \
|
|
"${IW4M_ADDRESS%/}/api/status" 2>/dev/null)" || return 1
|
|
else
|
|
out="$(curl -sS -m "$HTTP_TIMEOUT" "${IW4M_ADDRESS%/}/api/status" 2>/dev/null)" || return 1
|
|
fi
|
|
jq -e 'type=="array"' >/dev/null 2>&1 <<<"$out" || return 1
|
|
IW4M_STATUS_CACHE="$out"
|
|
return 0
|
|
}
|
|
|
|
# pull /api/status once per run and keep it in memory. Most webfronts serve it
|
|
# without a login, so only sign in if the anonymous read doesn't work - that
|
|
# also avoids burning a one-shot !rt token when it isn't needed.
|
|
iw4m_load() {
|
|
have_iw4m || return 1
|
|
[[ -n "$IW4M_STATUS_CACHE" ]] && return 0
|
|
IW4M_ERR=""
|
|
if iw4m_fetch_status; then return 0; fi
|
|
IW4M_ERR="iw4madmin unreachable"
|
|
iw4m_login || return 1
|
|
IW4M_ERR=""
|
|
iw4m_fetch_status || { IW4M_ERR="iw4madmin unreachable"; return 1; }
|
|
return 0
|
|
}
|
|
|
|
server_reg_seed() {
|
|
local server
|
|
while read -r server; do
|
|
[[ -n "$server" ]] || continue
|
|
if meta_q --arg s "$server" '.servers | has($s)' | grep -qx true; then continue; fi
|
|
meta_apply --arg s "$server" --arg k "$(printf '%s' "$server" | tr '_' '-')" \
|
|
'.servers[$s] = {komodo_type:"deployment", komodo_name:$k,
|
|
komodo_server:"", iw4m_match:"", iw4m_value:""}'
|
|
done < <(all_servers)
|
|
}
|
|
|
|
server_reg() { meta_q --arg s "$1" --arg k "$2" '.servers[$s][$k] // ""'; }
|
|
server_reg_set() { meta_apply --arg s "$1" --arg k "$2" --arg v "$3" '.servers[$s][$k] = $v'; }
|
|
|
|
# how many players are on this server right now, blank if we can't tell
|
|
iw4m_players() {
|
|
local server="$1" match value out
|
|
[[ -n "$IW4M_STATUS_CACHE" ]] || return 1
|
|
match="$(server_reg "$server" iw4m_match)"
|
|
value="$(server_reg "$server" iw4m_value)"
|
|
if [[ -z "$match" || -z "$value" ]]; then
|
|
iw4m_autolink "$server" >/dev/null || return 1
|
|
match="$(server_reg "$server" iw4m_match)"
|
|
value="$(server_reg "$server" iw4m_value)"
|
|
fi
|
|
out="$(jq -r --arg m "$match" --arg v "$value" '
|
|
def norm: ascii_downcase | gsub("[^a-z0-9]";"");
|
|
[ .[] | {id:(.id // .Id), port:(.port // .Port),
|
|
name:((.name // .Name) // ""),
|
|
players:(.currentPlayers // .CurrentPlayers)} ]
|
|
| map(select(
|
|
if $m=="port" then ((.port|tostring)==$v)
|
|
elif $m=="id" then ((.id|tostring)==$v)
|
|
else ((.name|norm)==($v|norm)) end))
|
|
| if length==1 then (.[0].players|tostring) else empty end
|
|
' <<<"$IW4M_STATUS_CACHE" 2>/dev/null)" || return 1
|
|
[[ -n "$out" && "$out" != "null" ]] || return 1
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
# first run: try to work out which IW4MAdmin server a folder is, and remember it
|
|
iw4m_autolink() {
|
|
local server="$1" token hit
|
|
token="$(printf '%s' "$server" | tr -cs 'a-zA-Z0-9' ' ' \
|
|
| awk '{for(i=1;i<=NF;i++) if ($i !~ /^(t4|t5|t6|iw5|iw4|[0-9]+)$/) print $i}' \
|
|
| head -1 | tr 'A-Z' 'a-z')"
|
|
[[ -n "$token" ]] || return 1
|
|
hit="$(jq -r --arg t "$token" '
|
|
def norm: ascii_downcase | gsub("[^a-z0-9]";"");
|
|
[ .[] | select((((.name // .Name) // "") | norm) | contains($t)) | (.port // .Port) ]
|
|
| if length==1 then (.[0]|tostring) else empty end' \
|
|
<<<"$IW4M_STATUS_CACHE" 2>/dev/null)" || return 1
|
|
[[ -n "$hit" ]] || return 1
|
|
server_reg_set "$server" iw4m_match port
|
|
server_reg_set "$server" iw4m_value "$hit"
|
|
printf '%s\n' "$hit"
|
|
}
|
|
|
|
komodo_restart() {
|
|
local server="$1" ktype name ksrv path body resp code
|
|
ktype="$(server_reg "$server" komodo_type)"; [[ -n "$ktype" ]] || ktype=deployment
|
|
name="$(server_reg "$server" komodo_name)"
|
|
[[ -n "$name" ]] || return 2
|
|
case "$ktype" in
|
|
stack)
|
|
path="execute/RestartStack"
|
|
body="$(jq -nc --arg n "$name" '{stack:$n}')" ;;
|
|
container)
|
|
ksrv="$(server_reg "$server" komodo_server)"
|
|
[[ -n "$ksrv" ]] || return 2
|
|
path="execute/RestartContainer"
|
|
body="$(jq -nc --arg n "$name" --arg s "$ksrv" '{server:$s, container:$n}')" ;;
|
|
*)
|
|
path="execute/RestartDeployment"
|
|
body="$(jq -nc --arg n "$name" '{deployment:$n}')" ;;
|
|
esac
|
|
resp="$(komodo_call "$path" "$body")" || return 1
|
|
[[ -n "$resp" ]] || return 1
|
|
code="$(tail -n1 <<<"$resp")"
|
|
resp="$(sed '$d' <<<"$resp")"
|
|
[[ "$code" == 2* ]] || return 1
|
|
# note: jq's // treats false as empty, so test for the key explicitly
|
|
jq -e 'if type=="object" and has("success") then .success == true else true end' \
|
|
>/dev/null 2>&1 <<<"$resp" || return 1
|
|
return 0
|
|
}
|
|
|
|
# ----------------------------------------------------------- deployments ---
|
|
|
|
deploy_open() { # action (reads CHANGED_SERVERS), prints the new id
|
|
local action="$1" id server files state="pending"
|
|
id="dep-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
|
integrations_on || state="manual"
|
|
meta_apply --arg id "$id" --arg a "$action" --arg now "$(now_iso)" \
|
|
'.deployments += [{id:$id, action:$a, started:$now, servers:[]}]'
|
|
for entry in "${CHANGED_SERVERS[@]}"; do
|
|
IFS=$'\t' read -r server files <<<"$entry"
|
|
[[ -n "$server" ]] || continue
|
|
meta_apply --arg id "$id" --arg s "$server" --argjson f "${files:-0}" --arg st "$state" \
|
|
'(.deployments[] | select(.id==$id) | .servers) += [{
|
|
server:$s, files:$f, restart:$st, players:null,
|
|
checked:null, restarted:null, attempts:0, error:null}]'
|
|
done
|
|
printf '%s\n' "$id"
|
|
}
|
|
|
|
deploy_set() { # id server restart players error
|
|
meta_apply --arg id "$1" --arg s "$2" --arg r "$3" --arg p "$4" --arg e "$5" \
|
|
--arg now "$(now_iso)" \
|
|
'(.deployments[] | select(.id==$id) | .servers[] | select(.server==$s)) |= (
|
|
.restart = $r
|
|
| .checked = $now
|
|
| .players = (if $p=="" then null else ($p|tonumber) end)
|
|
| .error = (if $e=="" then null else $e end)
|
|
| .attempts = (.attempts + 1)
|
|
| (if $r=="done" then .restarted = $now else . end))'
|
|
}
|
|
|
|
deploy_prune() {
|
|
local cutoff
|
|
cutoff="$(date -u -d "$DEPLOY_KEEP_DAYS days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || printf '')"
|
|
[[ -n "$cutoff" ]] || return 0
|
|
meta_apply --arg cutoff "$cutoff" \
|
|
'.deployments = [ .deployments[]
|
|
| select((.started >= $cutoff)
|
|
or any(.servers[]; .restart=="pending" or .restart=="failed")) ]'
|
|
}
|
|
|
|
# id <TAB> server for every server still waiting on a restart
|
|
deploy_pending() {
|
|
local filter="${1:-}"
|
|
meta_exists || return 0
|
|
meta_q --arg id "$filter" \
|
|
'.deployments[]
|
|
| select($id=="" or .id==$id) as $d
|
|
| $d.servers[]
|
|
| select(.restart=="pending" or .restart=="failed")
|
|
| "\($d.id)\t\(.server)"'
|
|
}
|
|
|
|
# check each waiting server, restart the empty ones
|
|
deploy_sweep() {
|
|
local filter="${1:-}" line id server players rc
|
|
local checked=0 restarted=0 held=0 failed=0
|
|
local -a pending=() unmatched=()
|
|
declare -A SEEN_PLAYERS SEEN_RESULT
|
|
|
|
mapfile -t pending < <(deploy_pending "$filter")
|
|
[[ ${#pending[@]} -gt 0 ]] || return 0
|
|
|
|
if ! integrations_on; then
|
|
echo
|
|
hdr "RESTARTS" "not configured"
|
|
rule
|
|
note "set KOMODO_ADDRESS / KOMODO_API_KEY / KOMODO_API_SECRET / IW4M_ADDRESS"
|
|
note "in $ENV_FILE to let this restart containers for you"
|
|
rule
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
iw4m_load || true
|
|
|
|
echo
|
|
hdr "RESTARTS" "$(printf '%s\n' "${pending[@]}" | cut -f2 | sort -u | grep -c .) $(plural "$(printf '%s\n' "${pending[@]}" | cut -f2 | sort -u | grep -c .)" server)"
|
|
rule
|
|
|
|
for line in "${pending[@]}"; do
|
|
IFS=$'\t' read -r id server <<<"$line"
|
|
[[ -n "$server" ]] || continue
|
|
|
|
if [[ -n "${SEEN_RESULT[$server]:-}" ]]; then
|
|
# already handled this server in another deployment this run
|
|
deploy_set "$id" "$server" "${SEEN_RESULT[$server]}" "${SEEN_PLAYERS[$server]:-}" ""
|
|
continue
|
|
fi
|
|
checked=$((checked + 1))
|
|
|
|
if [[ -z "$IW4M_STATUS_CACHE" ]]; then
|
|
deploy_set "$id" "$server" "pending" "" "${IW4M_ERR:-iw4madmin unreachable}"
|
|
SEEN_RESULT[$server]="pending"
|
|
row "$YL" "$server" "${IW4M_ERR:-iw4madmin unreachable} - held"
|
|
held=$((held + 1)); continue
|
|
fi
|
|
|
|
if ! players="$(iw4m_players "$server")"; then
|
|
deploy_set "$id" "$server" "pending" "" "no iw4madmin match"
|
|
SEEN_RESULT[$server]="pending"
|
|
unmatched+=("$server")
|
|
row "$YL" "$server" "no iw4madmin match - held"
|
|
held=$((held + 1)); continue
|
|
fi
|
|
|
|
SEEN_PLAYERS[$server]="$players"
|
|
if [[ "$players" -gt 0 ]]; then
|
|
deploy_set "$id" "$server" "pending" "$players" ""
|
|
SEEN_RESULT[$server]="pending"
|
|
row "$D" "$server" "$players $(plural "$players" player) on - held"
|
|
held=$((held + 1)); continue
|
|
fi
|
|
|
|
rc=0; komodo_restart "$server" || rc=$?
|
|
case "$rc" in
|
|
0) deploy_set "$id" "$server" "done" "0" ""
|
|
SEEN_RESULT[$server]="done"
|
|
row "$GN" "$server" "empty - restarted"
|
|
restarted=$((restarted + 1)) ;;
|
|
2) deploy_set "$id" "$server" "failed" "0" "no komodo name"
|
|
SEEN_RESULT[$server]="failed"
|
|
row "$YL" "$server" "no komodo name - skipped"
|
|
failed=$((failed + 1)) ;;
|
|
*) deploy_set "$id" "$server" "failed" "0" "komodo restart failed"
|
|
SEEN_RESULT[$server]="failed"
|
|
row "$YL" "$server" "komodo restart failed"
|
|
failed=$((failed + 1)) ;;
|
|
esac
|
|
done
|
|
|
|
rule
|
|
note "$restarted restarted · $held held · $failed failed"
|
|
[[ $((held + failed)) -gt 0 ]] && note "check again later with: push_scripts.sh -ad"
|
|
|
|
if [[ "$IW4M_ERR" == *login* ]]; then
|
|
echo
|
|
note "set IW4M_CLIENT_ID and IW4M_PASSWORD in $ENV_FILE"
|
|
note "an in-game !rt token works but dies after 2 minutes and only once -"
|
|
note "use !setpassword in game for something that keeps working"
|
|
fi
|
|
|
|
if [[ ${#unmatched[@]} -gt 0 && -n "$IW4M_STATUS_CACHE" ]]; then
|
|
echo
|
|
note "couldn't work out which IW4MAdmin server these are:"
|
|
note " $(printf '%s\n' "${unmatched[@]}" | sort -u | join_list)"
|
|
note "set iw4m_match/iw4m_value for them in $METADATA using:"
|
|
jq -r '.[] | " port \((.port // .Port)) \((.name // .Name))"' \
|
|
<<<"$IW4M_STATUS_CACHE" 2>/dev/null | while IFS= read -r l; do
|
|
printf ' %s%s%s\n' "$D" "$l" "$R"
|
|
done
|
|
fi
|
|
echo
|
|
}
|
|
|
|
deploy_finish() { # action
|
|
local id
|
|
[[ $DRY_RUN -eq 0 ]] || return 0
|
|
[[ ${#CHANGED_SERVERS[@]} -gt 0 ]] || return 0
|
|
meta_init
|
|
server_reg_seed
|
|
id="$(deploy_open "$1")"
|
|
deploy_prune
|
|
if integrations_on; then
|
|
# sweep everything queued, not just this deployment: one restart makes
|
|
# every change waiting on that server live
|
|
deploy_sweep ""
|
|
else
|
|
note "restart checks are off - no KOMODO_ADDRESS / IW4M_ADDRESS in $ENV_FILE"
|
|
echo
|
|
fi
|
|
}
|
|
|
|
deploy_rows() { # all deployments, newest first
|
|
meta_exists || return 0
|
|
meta_q '[.deployments[]] | sort_by(.started) | reverse | .[]
|
|
| [ .id, .action, .started,
|
|
([.servers[] | select(.restart=="pending" or .restart=="failed")] | length),
|
|
(.servers | length) ] | @tsv'
|
|
}
|
|
|
|
deploy_servers_by_state() { # id state
|
|
meta_q --arg id "$1" --arg st "$2" \
|
|
'.deployments[] | select(.id==$id) | .servers[] | select(.restart==$st)
|
|
| if .players != null and .players > 0
|
|
then "\(.server) (\(.players)p)" else .server end'
|
|
}
|
|
|
|
do_list_deployments() {
|
|
local mode="${1:-all}" id action started npend ntotal line shown=0 active=0
|
|
meta_init
|
|
deploy_prune
|
|
|
|
local total
|
|
total="$(deploy_rows | grep -c . || true)"
|
|
active="$(deploy_rows | awk -F'\t' '$4>0' | grep -c . || true)"
|
|
|
|
echo
|
|
if [[ "$mode" == "active" ]]; then
|
|
hdr "ACTIVE DEPLOYMENTS" "$active of $total open"
|
|
else
|
|
hdr "DEPLOYMENTS" "last $DEPLOY_KEEP_DAYS days · $active active"
|
|
fi
|
|
rule
|
|
|
|
while IFS=$'\t' read -r id action started npend ntotal; do
|
|
[[ -n "$id" ]] || continue
|
|
if [[ "$mode" == "active" && "$npend" -eq 0 ]]; then continue; fi
|
|
shown=$((shown + 1))
|
|
if [[ "$npend" -gt 0 ]]; then
|
|
row "$YL" "$action" "$(when_str "$started")"
|
|
else
|
|
row "$GN" "$action" "$(when_str "$started")"
|
|
fi
|
|
line="$(deploy_servers_by_state "$id" done | join_list)"
|
|
[[ -n "$line" ]] && field_wrap "restarted" "$line"
|
|
line="$(deploy_servers_by_state "$id" pending | join_list)"
|
|
[[ -n "$line" ]] && field_wrap "waiting" "$line"
|
|
line="$(deploy_servers_by_state "$id" failed | join_list)"
|
|
[[ -n "$line" ]] && field_wrap "failed" "$line"
|
|
line="$(deploy_servers_by_state "$id" manual | join_list)"
|
|
[[ -n "$line" ]] && field_wrap "no check" "$line"
|
|
printf ' %s%-9s%s %s\n' "$D" "id" "$R" "${D}$id${R}"
|
|
echo
|
|
done < <(deploy_rows)
|
|
|
|
if [[ $shown -eq 0 ]]; then
|
|
if [[ "$mode" == "active" ]]; then note "nothing waiting on a restart"
|
|
else note "no deployments recorded yet"; fi
|
|
rule
|
|
echo
|
|
return 0
|
|
fi
|
|
rule
|
|
note "restart sweep: push_scripts.sh -ad"
|
|
echo
|
|
}
|
|
|
|
do_active_deployments() {
|
|
meta_init
|
|
if [[ $DRY_RUN -eq 0 ]]; then deploy_sweep ""; fi
|
|
do_list_deployments active
|
|
}
|
|
|
|
# ------------------------------------------------------------------- push ---
|
|
|
|
do_push() {
|
|
local total_servers=0 total_changed=0 total_files=0
|
|
local server dest out changes rel line slug verb path
|
|
local -a held excl rs_opts
|
|
declare -A HELD_SET
|
|
|
|
echo
|
|
if [[ $DRY_RUN -eq 1 ]]; then hdr "PUSH (dry run)" "$SRC"; else hdr "PUSH" "$SRC"; fi
|
|
rule
|
|
|
|
while read -r server; do
|
|
ensure_dest "$server" || continue
|
|
dest="$(dest_of "$server")"
|
|
total_servers=$((total_servers + 1))
|
|
|
|
held=(); excl=(); HELD_SET=()
|
|
mapfile -t held < <(held_by_beta "$server")
|
|
for line in ${held[@]+"${held[@]}"}; do
|
|
[[ -n "$line" ]] || continue
|
|
IFS=$'\t' read -r rel slug <<<"$line"
|
|
HELD_SET["$rel"]="$slug"
|
|
excl+=("--exclude=/$rel")
|
|
done
|
|
|
|
changes=0
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rs_opts=(-a --checksum --itemize-changes
|
|
--exclude '/betas/' --exclude '.git' --exclude '*.swp'
|
|
--exclude '*~' --exclude '*.OLD' --exclude '*-beta.*')
|
|
if [[ $DRY_RUN -eq 1 ]]; then rs_opts+=(--dry-run); fi
|
|
if [[ $DELETE -eq 1 ]]; then rs_opts+=(--delete); fi
|
|
out="$(rsync "${rs_opts[@]}" ${excl[@]+"${excl[@]}"} "$SRC"/ "$dest"/)"
|
|
out="$(printf '%s\n' "$out" | awk '
|
|
/^\*deleting/ { p=$0; sub(/^[^ ]+[[:space:]]+/,"",p); print "removed\t" p; next }
|
|
/^>f/ { p=$0; sub(/^[^ ]+[[:space:]]+/,"",p)
|
|
if ($1 ~ /\+\+\+\+\+\+\+/) print "added\t" p; else print "updated\t" p }
|
|
')"
|
|
changes="$(printf '%s' "$out" | grep -c . || true)"
|
|
else
|
|
out=""
|
|
while read -r rel; do
|
|
[[ -n "${HELD_SET[$rel]:-}" ]] && continue
|
|
if [[ ! -f "$dest/$rel" ]]; then verb=added
|
|
elif ! cmp -s "$SRC/$rel" "$dest/$rel"; then verb=updated
|
|
else continue; fi
|
|
if [[ $DRY_RUN -eq 0 ]]; then
|
|
mkdir -p "$dest/$(dirname "$rel")"
|
|
cp -p "$SRC/$rel" "$dest/$rel"
|
|
fi
|
|
out+="$verb"$'\t'"$rel"$'\n'
|
|
done < <(list_source_files)
|
|
changes="$(printf '%s' "$out" | grep -c . || true)"
|
|
fi
|
|
|
|
if [[ "$changes" -gt 0 ]]; then
|
|
CHANGED_SERVERS+=("$server"$'\t'"$changes")
|
|
row "$GN" "$server" "$changes $(plural "$changes" file)"
|
|
while IFS=$'\t' read -r verb path; do
|
|
[[ -n "$verb" ]] || continue
|
|
detail "$verb" "$path"
|
|
done <<<"$out"
|
|
total_changed=$((total_changed + 1))
|
|
total_files=$((total_files + changes))
|
|
else
|
|
row "$D" "$server" "up to date"
|
|
fi
|
|
|
|
if [[ ${#HELD_SET[@]} -gt 0 ]]; then
|
|
for rel in "${!HELD_SET[@]}"; do
|
|
detail "held" "$(printf '%-28s' "$rel")${D}beta: ${HELD_SET[$rel]}${R}"
|
|
done
|
|
fi
|
|
done < <(match_servers)
|
|
|
|
rule
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
note "would change $total_changed of $total_servers $(plural "$total_servers" server) · $total_files $(plural "$total_files" file)"
|
|
else
|
|
note "$total_changed of $total_servers $(plural "$total_servers" server) updated · $total_files $(plural "$total_files" file)"
|
|
fi
|
|
echo
|
|
deploy_finish "$DEPLOY_ACTION"
|
|
}
|
|
|
|
# ------------------------------------------------------------------- beta ---
|
|
|
|
do_beta() {
|
|
local slug beta_dir server dest base rel had owner bfile ofile entry
|
|
local -a staged=() hits=() targets=() servers=() conflicts=() blocked=() ambiguous=()
|
|
|
|
slug="$(slugify "$BETA_NAME")"
|
|
[[ -n "$slug" ]] || { echo "beta name needs at least one letter or number" >&2; exit 1; }
|
|
beta_dir="$BETA_SRC/$slug"
|
|
|
|
meta_init
|
|
if beta_has "$slug"; then
|
|
echo "a beta called '$(beta_field "$slug" name)' is already open" >&2
|
|
echo "update it (-ub), roll it back (-r) or promote it (-pb) first" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -e "$beta_dir" ]]; then
|
|
echo "$beta_dir already exists but no beta is open for it" >&2
|
|
echo "move or delete that folder first" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -d "$BETA_SRC" ]]; then
|
|
echo "no staging folder at $BETA_SRC" >&2
|
|
echo "create it and drop the scripts you want to test inside" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t staged < <(staged_files)
|
|
if [[ ${#staged[@]} -eq 0 ]]; then
|
|
echo "nothing staged in $BETA_SRC - drop the scripts you want to test in there first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for base in "${staged[@]}"; do
|
|
mapfile -t hits < <(find_targets "$base")
|
|
case ${#hits[@]} in
|
|
0) targets+=("$base"$'\t'"$base") ;;
|
|
1) targets+=("$base"$'\t'"${hits[0]}") ;;
|
|
*) ambiguous+=("$base"$'\t'"$(printf '%s\n' "${hits[@]}" | join_list)") ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#ambiguous[@]} -gt 0 ]]; then
|
|
echo "more than one script in the shared folder has these names, so I can't tell" >&2
|
|
echo "which one the staged copy is meant to replace:" >&2
|
|
for entry in "${ambiguous[@]}"; do
|
|
IFS=$'\t' read -r base rel <<<"$entry"
|
|
printf ' %-26s %s\n' "$base" "$rel" >&2
|
|
done
|
|
echo "nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t servers < <(match_servers)
|
|
if [[ ${#servers[@]} -eq 0 ]]; then
|
|
echo "no servers matched" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local -a plan=()
|
|
for server in "${servers[@]}"; do
|
|
ensure_dest "$server" || continue
|
|
dest="$(dest_of "$server")"
|
|
for entry in "${targets[@]}"; do
|
|
IFS=$'\t' read -r base rel <<<"$entry"
|
|
if owner="$(owner_of "$server" "$rel")"; then
|
|
conflicts+=("$server"$'\t'"$rel"$'\t'"$owner")
|
|
continue
|
|
fi
|
|
had=0
|
|
if [[ -f "$dest/$rel" ]]; then had=1; fi
|
|
ofile="$(old_file "$rel")"
|
|
if [[ $had -eq 1 && -e "$dest/$ofile" ]]; then
|
|
blocked+=("$server"$'\t'"$ofile")
|
|
continue
|
|
fi
|
|
plan+=("$server"$'\t'"$base"$'\t'"$rel"$'\t'"$had")
|
|
done
|
|
done
|
|
|
|
if [[ ${#conflicts[@]} -gt 0 ]]; then
|
|
echo "conflict - these scripts already belong to another beta:" >&2
|
|
for entry in "${conflicts[@]}"; do
|
|
IFS=$'\t' read -r server rel owner <<<"$entry"
|
|
printf ' %-16s %-26s beta: %s\n' "$server" "$rel" "$owner" >&2
|
|
done
|
|
echo "nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#blocked[@]} -gt 0 ]]; then
|
|
echo "stale backups in the way - no beta owns these, so they were never cleaned up:" >&2
|
|
for entry in "${blocked[@]}"; do
|
|
IFS=$'\t' read -r server ofile <<<"$entry"
|
|
printf ' %-16s %s\n' "$server" "$ofile" >&2
|
|
done
|
|
echo "move or delete them, then reopen the beta. nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#plan[@]} -eq 0 ]]; then
|
|
echo "no usable servers - nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local srv_list scr_list n_srv n_scr
|
|
srv_list="$(printf '%s\n' "${plan[@]}" | cut -f1 | join_list)"
|
|
scr_list="$(printf '%s\n' "${targets[@]}" | cut -f2 | join_list)"
|
|
n_srv="$(printf '%s\n' "${plan[@]}" | cut -f1 | sort -u | grep -c .)"
|
|
n_scr="${#targets[@]}"
|
|
|
|
echo
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
hdr "OPEN BETA $BETA_NAME (dry run)" "$n_scr $(plural "$n_scr" script) · $n_srv $(plural "$n_srv" server)"
|
|
rule
|
|
field_wrap "servers" "$srv_list"
|
|
field_wrap "scripts" "$scr_list"
|
|
field "deploy" "each lands as <name>-beta.gsc, live copy parked as .OLD"
|
|
field "move to" "betas/$slug/"
|
|
rule
|
|
note "nothing was changed"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
beta_create "$slug" "$BETA_NAME" "$(now_iso)"
|
|
for entry in "${targets[@]}"; do
|
|
IFS=$'\t' read -r base rel <<<"$entry"
|
|
beta_add_script "$slug" "$base" "$rel" "$(sha_of "$BETA_SRC/$base")"
|
|
done
|
|
|
|
for entry in "${plan[@]}"; do
|
|
IFS=$'\t' read -r server base rel had <<<"$entry"
|
|
dest="$(dest_of "$server")"
|
|
bfile="$(beta_file "$rel")"
|
|
ofile="$(old_file "$rel")"
|
|
mkdir -p "$dest/$(dirname "$rel")"
|
|
if [[ $had -eq 1 ]]; then mv "$dest/$rel" "$dest/$ofile"; fi
|
|
cp -p "$BETA_SRC/$base" "$dest/$bfile"
|
|
beta_add_deploy "$slug" "$rel" "$server" "$had"
|
|
done
|
|
beta_sync_servers "$slug"
|
|
meta_touch
|
|
|
|
mkdir -p "$beta_dir"
|
|
for entry in "${targets[@]}"; do
|
|
IFS=$'\t' read -r base rel <<<"$entry"
|
|
mv "$BETA_SRC/$base" "$beta_dir/$(basename "$(beta_file "$rel")")"
|
|
done
|
|
|
|
hdr "BETA OPEN $BETA_NAME" "$n_scr $(plural "$n_scr" script) · $n_srv $(plural "$n_srv" server)"
|
|
rule
|
|
field_wrap "servers" "$srv_list"
|
|
field_wrap "scripts" "$scr_list"
|
|
field "deployed" "as <name>-beta.gsc, live copies parked as .OLD"
|
|
field "folder" "betas/$slug/"
|
|
rule
|
|
note "staging folder is clear for the next beta"
|
|
note "update: push_scripts.sh -ub \"$BETA_NAME\""
|
|
note "roll back: push_scripts.sh -r \"$BETA_NAME\""
|
|
echo
|
|
|
|
CHANGED_SERVERS=()
|
|
while read -r server; do
|
|
[[ -n "$server" ]] || continue
|
|
CHANGED_SERVERS+=("$server"$'\t'"$(printf '%s\n' "${plan[@]}" | awk -F'\t' -v s="$server" '$1==s' | grep -c .)")
|
|
done < <(printf '%s\n' "${plan[@]}" | cut -f1 | sort -u)
|
|
deploy_finish "beta:$slug"
|
|
}
|
|
|
|
# ------------------------------------------------------------ update beta ---
|
|
|
|
do_update_beta() {
|
|
local slug beta_dir base staged_as target bfile backup rev server line entry
|
|
local touched=0
|
|
local -a staged=() matched=() unmatched=() missing=() known=()
|
|
|
|
meta_init
|
|
slug="$(resolve_beta)"
|
|
beta_dir="$BETA_SRC/$slug"
|
|
|
|
mapfile -t staged < <(staged_files)
|
|
if [[ ${#staged[@]} -eq 0 ]]; then
|
|
echo "nothing staged in $BETA_SRC - drop the new version in there first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# a staged file belongs to this beta if it matches how the script was staged
|
|
# originally, or the filename of the script it replaces
|
|
mapfile -t known < <(beta_scripts "$slug")
|
|
for base in "${staged[@]}"; do
|
|
entry=""
|
|
for line in ${known[@]+"${known[@]}"}; do
|
|
IFS=$'\t' read -r staged_as target bfile backup rev <<<"$line"
|
|
if [[ "$base" == "$staged_as" || "$base" == "$(basename "$target")" ]]; then
|
|
entry="$base"$'\t'"$target"$'\t'"$bfile"$'\t'"$rev"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -n "$entry" ]]; then matched+=("$entry"); else unmatched+=("$base"); fi
|
|
done
|
|
|
|
if [[ ${#matched[@]} -eq 0 ]]; then
|
|
echo "none of the staged files belong to '$BETA_NAME':" >&2
|
|
printf '%s\n' "${staged[@]}" | sed 's/^/ /' >&2
|
|
echo "that beta covers: $(beta_scripts "$slug" | cut -f2 | join_list)" >&2
|
|
echo "nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local scr_list srv_list n_scr n_srv
|
|
scr_list="$(printf '%s\n' "${matched[@]}" | cut -f2 | join_list)"
|
|
srv_list="$(beta_servers "$slug" | join_list)"
|
|
n_scr="${#matched[@]}"
|
|
n_srv="$(beta_servers "$slug" | grep -c . || true)"
|
|
|
|
echo
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
hdr "UPDATE BETA $BETA_NAME (dry run)" "$n_scr $(plural "$n_scr" script) · $n_srv $(plural "$n_srv" server)"
|
|
rule
|
|
field_wrap "replace" "$scr_list"
|
|
field_wrap "servers" "$srv_list"
|
|
field "keeps" "backups, membership and revision history"
|
|
[[ ${#unmatched[@]} -gt 0 ]] && field_wrap "skipped" "$(printf '%s\n' "${unmatched[@]}" | join_list)"
|
|
rule
|
|
note "nothing was changed"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
for entry in "${matched[@]}"; do
|
|
IFS=$'\t' read -r base target bfile rev <<<"$entry"
|
|
|
|
while IFS=$'\t' read -r server _ _ _ _; do
|
|
[[ -n "$server" ]] || continue
|
|
dest="$(dest_of "$server")"
|
|
if [[ ! -d "$dest" ]]; then
|
|
missing+=("$server: no sp folder")
|
|
continue
|
|
fi
|
|
mkdir -p "$dest/$(dirname "$target")"
|
|
cp -p "$BETA_SRC/$base" "$dest/$bfile"
|
|
done < <(beta_deploys "$slug" | awk -F'\t' -v t="$target" '$2==t')
|
|
|
|
mv -f "$BETA_SRC/$base" "$beta_dir/$(basename "$bfile")"
|
|
beta_bump_script "$slug" "$target" "$(sha_of "$beta_dir/$(basename "$bfile")")"
|
|
touched=$((touched + 1))
|
|
done
|
|
meta_touch
|
|
|
|
hdr "BETA UPDATED $BETA_NAME" "rev $(beta_field "$slug" revision)"
|
|
rule
|
|
field_wrap "replaced" "$scr_list"
|
|
field_wrap "servers" "$srv_list"
|
|
field "folder" "betas/$slug/"
|
|
[[ ${#unmatched[@]} -gt 0 ]] && field_wrap "skipped" "$(printf '%s\n' "${unmatched[@]}" | join_list)"
|
|
for line in ${missing[@]+"${missing[@]}"}; do warn "$line"; done
|
|
rule
|
|
note "backups and beta membership unchanged"
|
|
[[ ${#unmatched[@]} -gt 0 ]] && note "anything left staged is not part of this beta"
|
|
echo
|
|
|
|
CHANGED_SERVERS=()
|
|
while read -r server; do
|
|
[[ -n "$server" ]] || continue
|
|
CHANGED_SERVERS+=("$server"$'\t'"$n_scr")
|
|
done < <(beta_servers "$slug")
|
|
deploy_finish "update:$slug"
|
|
}
|
|
|
|
# --------------------------------------------------------------- rollback ---
|
|
|
|
do_rollback() {
|
|
local slug beta_dir server target bfile backup had dest line
|
|
local restored=0 removed=0 missing=0 dropped=0
|
|
local -a restored_list=() dropped_list=() warn_list=() touched_servers=()
|
|
|
|
meta_init
|
|
slug="$(resolve_beta)"
|
|
beta_dir="$BETA_SRC/$slug"
|
|
|
|
while IFS=$'\t' read -r server target bfile backup had; do
|
|
[[ -n "$server" ]] || continue
|
|
dest="$(dest_of "$server")"
|
|
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
if [[ "$had" == "true" ]]; then restored_list+=("$target"); else dropped_list+=("$target"); fi
|
|
removed=$((removed + 1))
|
|
continue
|
|
fi
|
|
|
|
touched_servers+=("$server")
|
|
if [[ -e "$dest/$bfile" ]]; then rm -f "$dest/$bfile"; removed=$((removed + 1)); fi
|
|
if [[ "$had" == "true" ]]; then
|
|
if [[ -e "$dest/$backup" ]]; then
|
|
mv "$dest/$backup" "$dest/$target"
|
|
restored=$((restored + 1)); restored_list+=("$target")
|
|
else
|
|
missing=$((missing + 1)); warn_list+=("$server: no $backup to restore")
|
|
fi
|
|
else
|
|
dropped_list+=("$target")
|
|
fi
|
|
done < <(beta_deploys "$slug")
|
|
|
|
if [[ -d "$beta_dir" ]]; then dropped="$(find "$beta_dir" -type f | grep -c . || true)"; fi
|
|
|
|
echo
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
hdr "ROLLBACK $BETA_NAME (dry run)" "$removed beta $(plural "$removed" file)"
|
|
rule
|
|
[[ ${#restored_list[@]} -gt 0 ]] && field_wrap "restore" "$(printf '%s\n' "${restored_list[@]}" | join_list)"
|
|
[[ ${#dropped_list[@]} -gt 0 ]] && field_wrap "drop" "$(printf '%s\n' "${dropped_list[@]}" | join_list)"
|
|
field "delete" "betas/$slug/ ($dropped $(plural "$dropped" script))"
|
|
rule
|
|
note "nothing was changed"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
rm -rf "$beta_dir"
|
|
beta_drop "$slug"
|
|
meta_touch
|
|
|
|
hdr "ROLLED BACK $BETA_NAME" "$removed beta $(plural "$removed" file) removed"
|
|
rule
|
|
[[ ${#restored_list[@]} -gt 0 ]] && field_wrap "restored" "$(printf '%s\n' "${restored_list[@]}" | join_list)"
|
|
[[ ${#dropped_list[@]} -gt 0 ]] && field_wrap "dropped" "$(printf '%s\n' "${dropped_list[@]}" | join_list)"
|
|
field "deleted" "betas/$slug/ ($dropped $(plural "$dropped" script))"
|
|
for line in ${warn_list[@]+"${warn_list[@]}"}; do warn "$line"; done
|
|
rule
|
|
note "servers are back on the shared version"
|
|
echo
|
|
|
|
CHANGED_SERVERS=()
|
|
while read -r server; do
|
|
[[ -n "$server" ]] || continue
|
|
CHANGED_SERVERS+=("$server"$'\t'"1")
|
|
done < <(printf '%s\n' ${touched_servers[@]+"${touched_servers[@]}"} | sort -u)
|
|
deploy_finish "rollback:$slug"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- promote ---
|
|
|
|
do_promote() {
|
|
local slug beta_dir server target bfile backup had dest staged_as rev
|
|
local cleaned=0 landed=0
|
|
local -a lost=() live_list=()
|
|
|
|
meta_init
|
|
slug="$(resolve_beta)"
|
|
beta_dir="$BETA_SRC/$slug"
|
|
|
|
while IFS=$'\t' read -r staged_as target bfile backup rev; do
|
|
[[ -n "$target" ]] || continue
|
|
[[ -f "$beta_dir/$(basename "$bfile")" ]] || lost+=("$(basename "$bfile")")
|
|
done < <(beta_scripts "$slug")
|
|
|
|
if [[ ${#lost[@]} -gt 0 ]]; then
|
|
echo "can't promote '$BETA_NAME' - these are missing from betas/$slug/:" >&2
|
|
printf '%s\n' "${lost[@]}" | sed 's/^/ /' >&2
|
|
echo "put them back or roll the beta back instead. nothing was changed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
while IFS=$'\t' read -r server target bfile backup had; do
|
|
[[ -n "$server" ]] || continue
|
|
dest="$(dest_of "$server")"
|
|
if [[ $DRY_RUN -eq 0 ]]; then
|
|
rm -f "$dest/$bfile"
|
|
[[ "$had" == "true" ]] && rm -f "$dest/$backup"
|
|
fi
|
|
cleaned=$((cleaned + 1))
|
|
done < <(beta_deploys "$slug")
|
|
|
|
while IFS=$'\t' read -r staged_as target bfile backup rev; do
|
|
[[ -n "$target" ]] || continue
|
|
live_list+=("$target")
|
|
if [[ $DRY_RUN -eq 0 ]]; then
|
|
mkdir -p "$SRC/$(dirname "$target")"
|
|
mv -f "$beta_dir/$(basename "$bfile")" "$SRC/$target"
|
|
fi
|
|
landed=$((landed + 1))
|
|
done < <(beta_scripts "$slug")
|
|
|
|
echo
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
hdr "PROMOTE $BETA_NAME (dry run)" "$landed $(plural "$landed" script)"
|
|
rule
|
|
field_wrap "go live" "$(printf '%s\n' "${live_list[@]}" | join_list)"
|
|
field "clear" "$cleaned beta $(plural "$cleaned" file) and backups off the servers"
|
|
field "delete" "betas/$slug/"
|
|
field "then" "push to every server"
|
|
rule
|
|
note "nothing was changed"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
rm -rf "$beta_dir"
|
|
beta_drop "$slug"
|
|
meta_touch
|
|
|
|
hdr "PROMOTED $BETA_NAME" "$landed $(plural "$landed" script)"
|
|
rule
|
|
field_wrap "now live" "$(printf '%s\n' "${live_list[@]}" | join_list)"
|
|
field "cleared" "$cleaned beta $(plural "$cleaned" file) and backups"
|
|
field "deleted" "betas/$slug/"
|
|
rule
|
|
|
|
PATTERNS=("*")
|
|
DEPLOY_ACTION="promote:$slug"
|
|
do_push
|
|
}
|
|
|
|
# ------------------------------------------------------------------- list ---
|
|
|
|
do_list() {
|
|
local slug name opened updated revision server target bfile backup had dest base
|
|
local open=0 nstaged=0
|
|
local -a staged=() hits=() slugs=() trouble=()
|
|
|
|
meta_init
|
|
mapfile -t staged < <(staged_files)
|
|
mapfile -t slugs < <(beta_slugs)
|
|
nstaged=${#staged[@]}
|
|
open=${#slugs[@]}
|
|
|
|
echo
|
|
hdr "BETAS" "$open open · $nstaged staged"
|
|
rule
|
|
|
|
if [[ $nstaged -gt 0 ]]; then
|
|
printf ' %sSTAGING%s %sready for the next beta you open%s\n' "$B" "$R" "$D" "$R"
|
|
for base in "${staged[@]}"; do
|
|
mapfile -t hits < <(find_targets "$base")
|
|
case ${#hits[@]} in
|
|
0) detail "new" "$base" ;;
|
|
1) if [[ "${hits[0]}" == "$base" ]]; then
|
|
detail "replaces" "$base"
|
|
else
|
|
detail "replaces" "${hits[0]} ${D}(staged as $base)${R}"
|
|
fi ;;
|
|
*) detail "ambiguous" "$base ${YL}matches ${hits[*]}${R}" ;;
|
|
esac
|
|
done
|
|
echo
|
|
fi
|
|
|
|
if [[ $open -eq 0 ]]; then
|
|
if [[ $nstaged -eq 0 ]]; then
|
|
note "nothing staged, nothing open"
|
|
note "drop scripts in betas/, then: push_scripts.sh -b \"Name\" <servers>"
|
|
else
|
|
note "open one with: push_scripts.sh -b \"Name\" <servers>"
|
|
fi
|
|
rule
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
for slug in "${slugs[@]}"; do
|
|
name="$(beta_field "$slug" name)"
|
|
opened="$(beta_field "$slug" opened)"
|
|
updated="$(beta_field "$slug" updated)"
|
|
revision="$(beta_field "$slug" revision)"
|
|
trouble=()
|
|
|
|
while IFS=$'\t' read -r server target bfile backup had; do
|
|
[[ -n "$server" ]] || continue
|
|
dest="$(dest_of "$server")"
|
|
[[ -e "$dest/$bfile" ]] || trouble+=("$server: $bfile missing on disk")
|
|
if [[ "$had" == "true" && ! -e "$dest/$backup" ]]; then
|
|
trouble+=("$server: backup $backup missing")
|
|
fi
|
|
done < <(beta_deploys "$slug")
|
|
|
|
if [[ ! -d "$BETA_SRC/$slug" ]] || [[ "$(find "$BETA_SRC/$slug" -type f | grep -c . || true)" -eq 0 ]]; then
|
|
trouble+=("betas/$slug/ is empty - promote would fail")
|
|
fi
|
|
|
|
if [[ ${#trouble[@]} -gt 0 ]]; then
|
|
row "$YL" "$name" "$(when_str "$opened")"
|
|
else
|
|
row "$GN" "$name" "$(when_str "$opened")"
|
|
fi
|
|
|
|
field_wrap "maps" "$(beta_deploys "$slug" | cut -f1 | join_list)"
|
|
field_wrap "scripts" "$(beta_scripts "$slug" | cut -f2 | join_list)"
|
|
field "folder" "betas/$slug/"
|
|
if [[ "$revision" != "1" ]]; then
|
|
field "revision" "$revision ${D}updated $(ago "$updated")${R}"
|
|
fi
|
|
for base in ${trouble[@]+"${trouble[@]}"}; do warn "$base"; done
|
|
echo
|
|
done
|
|
|
|
rule
|
|
note "update: push_scripts.sh -ub \"<name>\""
|
|
note "promote: push_scripts.sh -pb \"<name>\""
|
|
note "roll back: push_scripts.sh -r \"<name>\""
|
|
echo
|
|
}
|
|
|
|
# ------------------------------------------------------------------- main ---
|
|
|
|
case "$MODE" in
|
|
push) do_push ;;
|
|
beta) do_beta ;;
|
|
update) do_update_beta ;;
|
|
rollback) do_rollback ;;
|
|
promote) do_promote ;;
|
|
list) do_list ;;
|
|
active) do_active_deployments ;;
|
|
deployments) do_list_deployments all ;;
|
|
esac
|