diff --git a/push_scripts.sh b/push_scripts.sh deleted file mode 100644 index 68af72c..0000000 --- a/push_scripts.sh +++ /dev/null @@ -1,1496 +0,0 @@ -#!/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 diff --git a/scripts/seed-fixture.ts b/scripts/seed-fixture.ts deleted file mode 100644 index c6a73fa..0000000 --- a/scripts/seed-fixture.ts +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env node -/** - * Seed a test-fixtures/zombies/ tree so the app has something to point at - * during local dev on Windows/macOS (where /root/zombies doesn't exist). - * - * Since the upload-only rework, there's no shared/scripts source dir — the - * seed just creates empty server folders. Upload scripts via the /scripts UI - * to populate them. - * - * Usage: - * npm run seed:fixture - * SERVERS_ROOT=./test-fixtures/zombies/t5 \ - * BETA_ARCHIVE_DIR=./test-fixtures/zombies/beta-archive \ - * npm run dev - */ - -import { mkdir } from 'node:fs/promises'; -import { join } from 'node:path'; - -const root = process.env.FIXTURE_ROOT ?? './test-fixtures/zombies'; - -async function main(): Promise { - const t5 = join(root, 't5'); - const betaArchive = join(root, 'beta-archive'); - await mkdir(betaArchive, { recursive: true }); - - const rel = 'runtime/plutonium/storage/t5/raw/scripts/sp'; - for (const name of ['t5_zombie_theater_1', 't5_zombie_cosmodrome_1', 't5_zombie_coast_1']) { - await mkdir(join(t5, name, rel), { recursive: true }); - await mkdir(join(t5, name, 'logs'), { recursive: true }); - } - - console.log(`seeded ${root}`); - console.log(` set SERVERS_ROOT=${t5}`); - console.log(` set BETA_ARCHIVE_DIR=${betaArchive}`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -});