#!/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/ <- one folder per open beta # dest: /root/zombies/t5//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 </$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// 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// 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// -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//, 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 target beta_file backup revision beta_scripts() { meta_q --arg s "$1" \ '.betas[$s].scripts[]? | [.staged_as,.target,.beta_file,.backup,(.revision|tostring)] | @tsv' } # server target beta_file backup 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 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//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 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 -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 -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\" " else note "open one with: push_scripts.sh -b \"Name\" " 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 \"\"" note "promote: push_scripts.sh -pb \"\"" note "roll back: push_scripts.sh -r \"\"" 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