From 8e6ffea1a4b31d76d4f1d35e3acacd8d8eb0ebba Mon Sep 17 00:00:00 2001 From: KellyMichels Date: Thu, 16 Jul 2026 14:07:02 -0500 Subject: [PATCH] feat(bash): native bash port of all z-scripts for Linux/macOS/WSL Full port: zhelpers.sh library (jq config, ssh, http/tcp, archive builder, build-version, motd, port-kill), all commands (zstart/zkill/zrestart/zstop, zdeploy with 5 kind handlers, zec2/zec2online/zrepair, zbackup/zbackup_ec2/ zsync/zbackup_and_sync, zstart_docker, zsetup_mail, setup_backup_schedule via cron), Unix-path zconfig.example.json, and a bash/README.md. Verified: bash -n clean on all scripts; archive exclusions, config semantics, and zec2 tested against the real config and live server from Git Bash. Needs a Linux/macOS/WSL shakedown for lsof/rsync/nohup paths before merging. --- bash/README.md | 62 +++++ bash/setup_backup_schedule | 50 ++++ bash/zbackup | 131 ++++++++++ bash/zbackup_and_sync | 47 ++++ bash/zbackup_ec2 | 99 ++++++++ bash/zconfig.example.json | 80 ++++++ bash/zdeploy | 494 +++++++++++++++++++++++++++++++++++++ bash/zec2 | 93 +++++++ bash/zec2online | 148 +++++++++++ bash/zhelpers.sh | 239 ++++++++++++++++++ bash/zkill | 59 +++++ bash/zrepair | 79 ++++++ bash/zrestart | 53 ++++ bash/zsetup_mail | 106 ++++++++ bash/zstart | 172 +++++++++++++ bash/zstart_docker | 65 +++++ bash/zstop | 37 +++ bash/zsync | 96 +++++++ 18 files changed, 2110 insertions(+) create mode 100644 bash/README.md create mode 100644 bash/setup_backup_schedule create mode 100644 bash/zbackup create mode 100644 bash/zbackup_and_sync create mode 100644 bash/zbackup_ec2 create mode 100644 bash/zconfig.example.json create mode 100644 bash/zdeploy create mode 100644 bash/zec2 create mode 100644 bash/zec2online create mode 100644 bash/zhelpers.sh create mode 100644 bash/zkill create mode 100644 bash/zrepair create mode 100644 bash/zrestart create mode 100644 bash/zsetup_mail create mode 100644 bash/zstart create mode 100644 bash/zstart_docker create mode 100644 bash/zstop create mode 100644 bash/zsync diff --git a/bash/README.md b/bash/README.md new file mode 100644 index 0000000..cd04255 --- /dev/null +++ b/bash/README.md @@ -0,0 +1,62 @@ + + +# Token Savers — bash port (Linux / macOS / WSL) + +Native bash versions of the z-scripts. Same commands, same `zconfig.json` schema, +same behavior — no PowerShell required. + +## Requirements + +- bash 4+, `jq`, `curl`, `zip`, OpenSSH (`ssh`/`scp`) +- `lsof` (for `zkill`/`zrestart`), `rsync` (for `zsync ` mirror mode) +- Docker + docker compose on the remote host for the deploy scripts + +Debian/Ubuntu: `sudo apt install jq curl zip lsof rsync` +macOS: `brew install jq` (the rest ships with the OS) + +## Install + +```bash +git clone https://github.com/kellymichels/zscripts-token-savers.git ~/zscripts +cd ~/zscripts/bash +cp zconfig.example.json zconfig.json # then fill in your values +chmod +x z* setup_backup_schedule +echo 'export PATH="$HOME/zscripts/bash:$PATH"' >> ~/.bashrc && source ~/.bashrc +``` + +## Commands + +Same set as the PowerShell versions — the project key in `zconfig.json` IS the +command argument: + +| Command | What it does | +|---|---| +| `zstart

[--detached]` | start local dev server (python/vite/nextjs) | +| `zkill

` | free the project's dev port | +| `zrestart

[--detached]` | kill + start in one command | +| `zstop

` | stop the project's compose stack on the server | +| `zdeploy

[--note "msg"]` | zip → scp → docker compose build/up → verify build version | +| `zec2 [

]` | TCP + HTTP + live-version reachability check | +| `zec2online [

]` | deep health check; auto-starts downed stacks | +| `zrepair

` | audit/repair container + proxy routing, smoke test | +| `zbackup [

] [--tag t]` | local source zip (+ pg_dump when .env has DATABASE_URL) | +| `zbackup_ec2 [

]` | pull db dump + data dirs down from the server | +| `zsync` | copy new backup files offsite (never overwrites) | +| `zbackup_and_sync` | both of the above; cron-friendly | +| `setup_backup_schedule --install` | daily backup cron job | +| `zsetup_mail --domain d` | provision docker-mailserver mailboxes + print DNS records | +| `zstart_docker` | bring up the local compose stack in `bash/docker/` | + +Flags use GNU style (`--port 3000`, `--detached`) instead of PowerShell style +(`-Port`, `-Detached`). Detached dev servers log to `/tmp/zstart-.log`. + +## Differences from the PowerShell versions + +- `zsync ` mirrors `dist/` with `rsync -a --delete` (robocopy /MIR equivalent). +- Scheduling uses cron (`setup_backup_schedule`) instead of Windows Task Scheduler. +- The MOTD banner picks a random `motd/*.txt` instead of tracking a shuffle rotation. +- Windows-only helpers (`.cmd` launchers) don't exist — scripts are directly executable. diff --git a/bash/setup_backup_schedule b/bash/setup_backup_schedule new file mode 100644 index 0000000..d2e5b60 --- /dev/null +++ b/bash/setup_backup_schedule @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# setup_backup_schedule — schedule daily zbackup_and_sync via cron. +# (The PowerShell version creates a Windows Scheduled Task; this is the cron equivalent.) +# +# Usage: +# setup_backup_schedule # print the crontab line to add +# setup_backup_schedule --install # append it to your crontab (2:00 AM daily) +# setup_backup_schedule --time 03:30 --install + +set -uo pipefail +_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$_HERE/zhelpers.sh" + +install=0; at_time="02:00" +while [ $# -gt 0 ]; do + case "$1" in + --install) install=1; shift ;; + --time) at_time="${2:-02:00}"; shift 2 ;; + *) err "Unknown option: $1"; exit 1 ;; + esac +done + +hh="${at_time%%:*}"; mm="${at_time##*:}" +if ! [[ "$hh" =~ ^[0-9]{1,2}$ && "$mm" =~ ^[0-9]{2}$ ]] || [ "$hh" -gt 23 ] || [ "$mm" -gt 59 ]; then + err "Invalid --time '$at_time' (expected HH:MM, 24h)"; exit 1 +fi + +cron_line="${mm#0} ${hh#0} * * * $_HERE/zbackup_and_sync >> \$HOME/zbackup_and_sync.log 2>&1" + +info "=== Backup schedule (cron) ===" +dim "Daily at $at_time:" +printf '\n %s\n\n' "$cron_line" + +if [ "$install" -eq 1 ]; then + z_require crontab + if crontab -l 2>/dev/null | grep -qF "$_HERE/zbackup_and_sync"; then + warn "A crontab entry for zbackup_and_sync already exists - not adding a duplicate." + dim "Edit it with: crontab -e" + exit 0 + fi + ( crontab -l 2>/dev/null; printf '%s\n' "$cron_line" ) | crontab - \ + || { err "Failed to update crontab."; exit 1; } + ok "Installed. Verify with: crontab -l" +else + dim "Run with --install to add it to your crontab, or add it manually with: crontab -e" +fi diff --git a/bash/zbackup b/bash/zbackup new file mode 100644 index 0000000..407ce67 --- /dev/null +++ b/bash/zbackup @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# zbackup — local backups: zip project sources (plus a Postgres dump when the +# project's .env has a DATABASE_URL) into the backups folder. +# +# Usage: +# zbackup # every project in zconfig.json + this scripts folder +# zbackup [ ...] +# zbackup scripts # just this scripts folder ('scripts' is a reserved word) +# zbackup pyapp --tag "pre-migration" +# +# Output: //_[_tag].zip + +set -uo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh" +z_need_config +z_require zip + +projects=(); tag="" +while [ $# -gt 0 ]; do + case "$1" in + --tag) tag="${2:-}"; shift 2 ;; + -*) err "Unknown option: $1"; exit 1 ;; + *) projects+=("$1"); shift ;; + esac +done + +BACKUP_ROOT="$(zq '.paths.backupsLocal')" +include_scripts=0 +if [ "${#projects[@]}" -eq 0 ]; then + mapfile -t projects < <(zproj_keys) + include_scripts=1 +else + filtered=() + for p in "${projects[@]}"; do + if [ "$p" = "scripts" ]; then include_scripts=1; else filtered+=("$p"); fi + done + projects=("${filtered[@]+"${filtered[@]}"}") +fi + +ts() { date +%Y%m%d-%H%M%S; } +tag_suffix() { [ -n "$tag" ] && printf '_%s' "$(printf '%s' "$tag" | tr -s '[:space:]' '_')"; } + +# Dump the project's Postgres database if its .env declares a DATABASE_URL. +# Checks /.env, then /backend/.env. Returns 0 and writes $2 on success. +local_pg_dump() { # + local root="$1" out="$2" env_file db_url + env_file="$root/.env"; [ -f "$env_file" ] || env_file="$root/backend/.env" + [ -f "$env_file" ] || return 1 + db_url="$(grep -m1 '^DATABASE_URL=' "$env_file" | cut -d= -f2- | tr -d '[:space:]')" + [ -n "$db_url" ] || return 1 + db_url="$(printf '%s' "$db_url" | sed -E 's|^postgresql\+[^:]+://|postgresql://|')" + if [[ "$db_url" =~ ^postgresql://([^:]+):([^@]+)@([^:]+):([0-9]+)/([^?]+) ]]; then + local user="${BASH_REMATCH[1]}" pass="${BASH_REMATCH[2]}" host="${BASH_REMATCH[3]}" + local port="${BASH_REMATCH[4]}" db="${BASH_REMATCH[5]}" + command -v pg_dump >/dev/null 2>&1 || { err ' pg_dump not installed - skipping PG backup'; return 1; } + if PGPASSWORD="$(z_urldecode "$pass")" pg_dump -h "$host" -p "$port" -U "$user" -d "$db" -F p -f "$out" 2>/dev/null \ + && [ -f "$out" ]; then + ok " PostgreSQL dump: $(awk -v b="$(wc -c < "$out")" 'BEGIN{printf "%.1f", b/1024}') KB" + return 0 + fi + err " pg_dump failed" + return 1 + fi + err ' Could not parse DATABASE_URL - skipping PG backup' + return 1 +} + +declare -a summary +exit_code=0 + +backup_project() { # + local key="$1" root out_dir zip_path dump_dir db_dump extra="-" + zproj_require "$key" + root="$(zproj "$key" .localRoot)" + out_dir="$BACKUP_ROOT/$key"; mkdir -p "$out_dir" + zip_path="$out_dir/$(ts)_${key}$(tag_suffix).zip" + + printf '\n'; info "=== [$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')] Local backup ($(zproj "$key" .label)) ===" + dim " Output: $zip_path" + [ -d "$root" ] || { err " Root not found: $root"; return 1; } + + dump_dir="$(mktemp -d "${TMPDIR:-/tmp}/zbackup_${key}_XXXXXX")" + db_dump="$dump_dir/database_pg.sql" + warn " [1/3] Checking for a local database to dump..." + if local_pg_dump "$root" "$db_dump"; then extra="$db_dump" + else dim " No local DATABASE_URL - source-only backup."; fi + + warn " [2/3] Archiving source..." + local excl=() + mapfile -t excl < <(z_archive_excludes "$key" 1) + if ! z_archive "$root" "$zip_path" 0 "$extra" "${excl[@]}"; then + rm -rf "$dump_dir"; return 1 + fi + rm -rf "$dump_dir" + warn " [3/3] Done." + summary+=("$key -> $zip_path ($(z_size_mb "$zip_path") MB)") +} + +backup_scripts() { + local out_dir zip_path scripts_root + scripts_root="$(zq '.paths.scriptsRoot')" + out_dir="$BACKUP_ROOT/scripts"; mkdir -p "$out_dir" + zip_path="$out_dir/$(ts)_scripts$(tag_suffix).zip" + + printf '\n'; info "=== [SCRIPTS] Local backup (this scripts folder) ===" + dim " Output: $zip_path" + [ -d "$scripts_root" ] || { err " Scripts root not found: $scripts_root"; return 1; } + + warn " [1/2] Archiving source..." + z_archive "$scripts_root" "$zip_path" 1 "-" .git archive tmp nul || return 1 + warn " [2/2] Done." + summary+=("scripts -> $zip_path ($(z_size_mb "$zip_path") MB)") +} + +for key in "${projects[@]+"${projects[@]}"}"; do + backup_project "$key" || { summary+=("$key -> FAILED"); exit_code=1; } +done +if [ "$include_scripts" -eq 1 ]; then + backup_scripts || { summary+=("scripts -> FAILED"); exit_code=1; } +fi + +printf '\n'; info "=== Backup summary ===" +for line in "${summary[@]+"${summary[@]}"}"; do + case "$line" in *FAILED*) err " $line" ;; *) ok " $line" ;; esac +done +printf '\n' +exit "$exit_code" diff --git a/bash/zbackup_and_sync b/bash/zbackup_and_sync new file mode 100644 index 0000000..557e739 --- /dev/null +++ b/bash/zbackup_and_sync @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# zbackup_and_sync — run backups, then sync the backups folder offsite. +# +# Usage: +# zbackup_and_sync # backup everything + sync +# zbackup_and_sync [ ...] +# +# Cron example (see setup_backup_schedule): +# 0 2 * * * /path/to/zscripts/bash/zbackup_and_sync >> ~/zbackup.log 2>&1 + +set -uo pipefail +_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$_HERE/zhelpers.sh" +z_need_config + +printf '\n' +info "============================================" +info " Backup & Sync - $(date '+%Y-%m-%d %H:%M:%S')" +info "============================================" +printf '\n' + +warn "[1/2] Running backups..." +"$_HERE/zbackup" "$@" +backup_rc=$? +if [ $backup_rc -ne 0 ]; then + err "Backup failed (exit code: $backup_rc)" + exit $backup_rc +fi + +printf '\n'; warn "[2/2] Syncing offsite..." +"$_HERE/zsync" +sync_rc=$? + +printf '\n' +info "============================================" +if [ $sync_rc -eq 0 ]; then + ok " Backup & Sync Complete [OK]" +else + warn " Sync had issues (exit code: $sync_rc)" +fi +info "============================================" +printf '\n' +exit $sync_rc diff --git a/bash/zbackup_ec2 b/bash/zbackup_ec2 new file mode 100644 index 0000000..e28f1d7 --- /dev/null +++ b/bash/zbackup_ec2 @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# zbackup_ec2 — pull backups down from the server: a Postgres dump for projects +# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist). +# +# Usage: +# zbackup_ec2 # every project with a remote.path +# zbackup_ec2 [ ...] +# +# Output: +# //__db.sql (projects with a db block) +# //__files.zip + +set -uo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh" +z_need_config +z_require ssh scp + +projects=("$@") +if [ "${#projects[@]}" -eq 0 ]; then + mapfile -t projects < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.remote.path != null) | .key' "$ZCONFIG") +fi + +EC2_BACKUP_ROOT="$(zq '.paths.backupsEc2')" +REMOTE_HOME="/home/$(zec2_user)" + +scp_down() { # + scp -o StrictHostKeyChecking=no -i "$(zec2_pem)" "$(zec2_target):$1" "$2" \ + || { err "SCP download failed: $1 -> $2"; return 1; } +} + +declare -a summary +exit_code=0 + +ec2_backup() { # + local key="$1" remote_path compose_dir ts out_dir remote_db remote_zip local_db local_zip + local db_user db_name has_db=0 line + zproj_require "$key" + remote_path="$(zproj "$key" .remote.path)" + compose_dir="$(zremote_compose_dir "$key")" + ts="$(date +%Y%m%d-%H%M%S)" + out_dir="$EC2_BACKUP_ROOT/$key"; mkdir -p "$out_dir" + remote_db="$REMOTE_HOME/ec2_${key}_db_$ts.sql" + remote_zip="$REMOTE_HOME/ec2_${key}_files_$ts.zip" + local_db="$out_dir/${ts}_${key}_db.sql" + local_zip="$out_dir/${ts}_${key}_files.zip" + db_user="$(zproj "$key" .db.user)"; db_name="$(zproj "$key" .db.name)" + [ -n "$db_user" ] && [ -n "$db_name" ] && has_db=1 + + printf '\n'; info "=== [$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')] Server backup ($(zproj "$key" .label)) ===" + + if [ "$has_db" -eq 1 ]; then + warn " [1/4] PostgreSQL dump on the server..." + zec2_step "pg_dump $db_name" \ + "cd $compose_dir && sudo docker compose exec -T db pg_dump -U $db_user -d $db_name > $remote_db" || return 1 + else + dim " [1/4] No db config block - skipping database dump." + fi + + warn " [2/4] Zipping server-side data (uploads/archive/dist if present)..." + local zip_cmd + zip_cmd="sudo apt-get install -y zip >/dev/null 2>&1" + zip_cmd+="; FILES=''" + zip_cmd+="; test -d $remote_path/uploads && FILES=\"\$FILES $remote_path/uploads\"" + zip_cmd+="; test -d $remote_path/archive && FILES=\"\$FILES $remote_path/archive\"" + zip_cmd+="; test -d $remote_path/dist && FILES=\"\$FILES $remote_path/dist\"" + zip_cmd+="; test -f $remote_path/build-version.json && FILES=\"\$FILES $remote_path/build-version.json\"" + zip_cmd+="; if [ -z \"\$FILES\" ]; then echo 'no data dirs found' | sudo zip $remote_zip - >/dev/null; else sudo zip -r $remote_zip \$FILES; fi" + if ! zec2_step "zip $key files" "$zip_cmd"; then + warn " WARNING: files zip failed - writing placeholder and continuing." + zec2_step "placeholder zip" "echo 'zip failed' | sudo zip $remote_zip - >/dev/null" || true + fi + + warn " [3/4] Downloading to local..." + [ "$has_db" -eq 1 ] && { scp_down "$remote_db" "$local_db" || return 1; } + scp_down "$remote_zip" "$local_zip" || return 1 + + warn " [4/4] Cleaning up remote..." + zec2_step "remove remote temp files" "rm -f $remote_db $remote_zip" || true + + line="$key ->" + [ "$has_db" -eq 1 ] && [ -f "$local_db" ] && line+=" $local_db ($(z_size_mb "$local_db") MB) |" + line+=" $local_zip ($(z_size_mb "$local_zip") MB)" + summary+=("$line") +} + +for key in "${projects[@]+"${projects[@]}"}"; do + ec2_backup "$key" || { summary+=("$key -> FAILED"); exit_code=1; } +done + +printf '\n'; info "=== Server backup summary ===" +for line in "${summary[@]+"${summary[@]}"}"; do + case "$line" in *FAILED*) err " $line" ;; *) ok " $line" ;; esac +done +printf '\n' +exit "$exit_code" diff --git a/bash/zconfig.example.json b/bash/zconfig.example.json new file mode 100644 index 0000000..f445fc8 --- /dev/null +++ b/bash/zconfig.example.json @@ -0,0 +1,80 @@ +{ + "_comment": "Copy this file to zconfig.json and fill in your values. zconfig.json is gitignored — never commit it.", + + "ec2": { + "ip": "YOUR_SERVER_IP", + "user": "YOUR_SSH_USER", + "pemKey": "/home/youruser/.ssh/YourKey.pem", + "stackRoot": "/home/YOUR_SSH_USER/stack" + }, + + "paths": { + "temp": "/tmp/zscripts", + "backupsLocal": "/home/youruser/backups/projects", + "backupsEc2": "/home/youruser/backups/ec2", + "scriptsRoot": "/home/youruser/zscripts/bash", + "oneDriveBackups": "" + }, + + "projects": { + "_comment": "Rename these keys to your own project names — the key IS the command argument: zstart pyapp, zdeploy viteapp, zbackup nextapp. Add as many projects as you like. Keys starting with _ are ignored.", + + "pyapp": { + "label": "My Python App", + "kind": "python", + "localRoot": "/home/youruser/code/pyapp", + "startModule": "pyapp.main", + "ports": { "dev": 8080 }, + "domain": "pyapp.yourdomain.com", + "start": { + "_comment": "Optional zstart pre-steps: gitPull runs 'git pull --ff-only' first; env sets variables for the server process.", + "gitPull": true, + "env": { "MYAPP_DEBUG": "1" } + }, + "db": { "user": "pyapp_user", "name": "pyapp_db" }, + "remote": { + "path": "/home/YOUR_SSH_USER/stack/pyapp", + "composeDir": "/home/YOUR_SSH_USER/stack/pyapp/docker", + "appService": "app" + }, + "deploy": { "zipName": "PyAppDeploy.zip", "gitPull": true, "exclude": ["docs"] } + }, + + "viteapp": { + "label": "My Vite Site", + "kind": "vite", + "localRoot": "/home/youruser/code/viteapp", + "ports": { "dev": 5173 }, + "domain": "www.yourdomain.com", + "remote": { + "path": "/home/YOUR_SSH_USER/stack/viteapp", + "containerName": "viteapp" + }, + "deploy": { "zipName": "ViteAppDeploy.zip" } + }, + + "nextapp": { + "label": "My Next.js App", + "kind": "nextjs", + "localRoot": "/home/youruser/code/nextapp", + "ports": { "dev": 4173, "prod": 3000 }, + "domain": "app.yourdomain.com", + "migrations": "prisma", + "db": { "user": "nextapp_user", "name": "nextapp_db" }, + "remote": { + "path": "/home/YOUR_SSH_USER/stack/nextapp", + "appService": "web" + }, + "deploy": { "zipName": "NextAppDeploy.zip" } + }, + + "edge": { + "label": "Edge Proxy", + "kind": "edge", + "localRoot": "/home/youruser/code/edge", + "proxyContainer": "edge-proxy", + "certsSource": "/etc/letsencrypt", + "remote": { "path": "/home/YOUR_SSH_USER/stack/edge" } + } + } +} diff --git a/bash/zdeploy b/bash/zdeploy new file mode 100644 index 0000000..20b2ce8 --- /dev/null +++ b/bash/zdeploy @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# zdeploy — deploy any project defined in zconfig.json to the server. +# Each project runs its own docker compose stack; the handler is picked by the +# project's "kind": python | vite | nextjs | edge | docker. +# +# Usage: +# zdeploy [ ...] [--note "message"] +# zdeploy all # every project (edge kinds first), stop at first failure +# +# Flow (python/vite/nextjs): zip source -> free server disk space -> scp up -> +# unzip into remote.path (preserving server-side .env) -> docker compose build + up +# -> verify the live site reports the new build version. Zips are always deleted. + +set -uo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh" +z_need_config +z_require ssh scp zip curl jq + +projects=(); NOTE="Build deployed" +while [ $# -gt 0 ]; do + case "$1" in + --note) NOTE="${2:-Build deployed}"; shift 2 ;; + *) projects+=("${1#-}"); shift ;; # tolerate switch-style keys (zdeploy -myproject) + esac +done + +if [ "${#projects[@]}" -eq 0 ]; then + printf '\n'; warn "Usage: zdeploy [ ...] | all [--note \"message\"]" + dim " Projects in zconfig.json: $(zproj_csv)" + dim " 'all' deploys everything (edge kinds first) and stops at the first failure." + exit 1 +fi + +EC2_IP="$(zec2_ip)" +STACK_ROOT="$(zq '.ec2.stackRoot')" +REMOTE_HOME="/home/$(zec2_user)" +EC2_USER="$(zec2_user)" +TEMP_ROOT="$(zq '.paths.temp')" +{ [ -z "$TEMP_ROOT" ] || [ "$TEMP_ROOT" = "null" ]; } && TEMP_ROOT="${TMPDIR:-/tmp}" +mkdir -p "$TEMP_ROOT" + +# 'all' -> every project, edge kinds first (the proxy must route before apps ship). +if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then + mapfile -t edge_keys < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind == "edge") | .key' "$ZCONFIG") + mapfile -t rest_keys < <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind != "edge") | .key' "$ZCONFIG") + projects=("${edge_keys[@]+"${edge_keys[@]}"}" "${rest_keys[@]+"${rest_keys[@]}"}") + info "Deploying all projects: $(printf '%s ' "${projects[@]}")" +fi + +deploy_zip_name() { # + local zn; zn="$(zproj "$1" .deploy.zipName)" + [ -n "$zn" ] && printf '%s' "$zn" || printf '%sDeploy.zip' "$1" +} + +# Pre-upload cleanup: remove stale deploy zips, prune docker, truncate big logs, +# fail if under 1.5 GB free. +preflight_cleanup() { # + printf '\n'; info "--- [Preflight] Freeing disk space on the server ---" + local rm_clause="true" + [ $# -gt 0 ] && rm_clause="rm -f $*" + local cmd + cmd="echo '--- df / before cleanup ---'; df -h /" + cmd+="; echo '--- removing stale deploy artifacts ---'; $rm_clause" + cmd+="; echo '--- pruning docker build cache + dangling images + stopped containers ---'" + cmd+="; sudo docker container prune -f >/dev/null 2>&1 || true" + cmd+="; sudo docker builder prune -f >/dev/null 2>&1 || true" + cmd+="; sudo docker image prune -af >/dev/null 2>&1 || true" + cmd+="; echo '--- truncating large container logs ---'" + cmd+="; sudo find /var/lib/docker/containers/ -name '*-json.log' -size +50M -exec truncate -s 0 {} + 2>/dev/null || true" + cmd+="; echo '--- df / after cleanup ---'; df -h /" + cmd+='; avail_mb=$(df --output=avail -BM / | tail -n 1 | tr -dc 0-9)' + cmd+='; [ -z "$avail_mb" ] && avail_mb=0' + cmd+='; echo available_mb=$avail_mb' + cmd+='; if [ "$avail_mb" -lt 1500 ]; then echo "ERROR: less than 1.5 GB free on /. Grow the root volume or run: sudo docker system prune -af" >&2; exit 11; fi' + zssh "$cmd" || { err "Server pre-flight cleanup failed. Root volume too full (need ~1.5 GB free, ideally 3+)."; return 1; } +} + +# Post-deploy cleanup: prune what this deploy created; running stacks untouched. +post_cleanup() { #