mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-07 07:18:18 +00:00
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.
This commit is contained in:
parent
d97b92f989
commit
8e6ffea1a4
62
bash/README.md
Normal file
62
bash/README.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# 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 <project>` 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 <p> [--detached]` | start local dev server (python/vite/nextjs) |
|
||||||
|
| `zkill <p>` | free the project's dev port |
|
||||||
|
| `zrestart <p> [--detached]` | kill + start in one command |
|
||||||
|
| `zstop <p>` | stop the project's compose stack on the server |
|
||||||
|
| `zdeploy <p> [--note "msg"]` | zip → scp → docker compose build/up → verify build version |
|
||||||
|
| `zec2 [<p>]` | TCP + HTTP + live-version reachability check |
|
||||||
|
| `zec2online [<p>]` | deep health check; auto-starts downed stacks |
|
||||||
|
| `zrepair <p>` | audit/repair container + proxy routing, smoke test |
|
||||||
|
| `zbackup [<p>] [--tag t]` | local source zip (+ pg_dump when .env has DATABASE_URL) |
|
||||||
|
| `zbackup_ec2 [<p>]` | 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-<project>.log`.
|
||||||
|
|
||||||
|
## Differences from the PowerShell versions
|
||||||
|
|
||||||
|
- `zsync <viteproject>` 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.
|
||||||
50
bash/setup_backup_schedule
Normal file
50
bash/setup_backup_schedule
Normal file
@ -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
|
||||||
131
bash/zbackup
Normal file
131
bash/zbackup
Normal file
@ -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 <project> [<project> ...]
|
||||||
|
# zbackup scripts # just this scripts folder ('scripts' is a reserved word)
|
||||||
|
# zbackup pyapp --tag "pre-migration"
|
||||||
|
#
|
||||||
|
# Output: <paths.backupsLocal>/<project>/<timestamp>_<project>[_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 <root>/.env, then <root>/backend/.env. Returns 0 and writes $2 on success.
|
||||||
|
local_pg_dump() { # <root> <out_path>
|
||||||
|
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() { # <key>
|
||||||
|
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"
|
||||||
47
bash/zbackup_and_sync
Normal file
47
bash/zbackup_and_sync
Normal file
@ -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 <project> [<project> ...]
|
||||||
|
#
|
||||||
|
# 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
|
||||||
99
bash/zbackup_ec2
Normal file
99
bash/zbackup_ec2
Normal file
@ -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 <project> [<project> ...]
|
||||||
|
#
|
||||||
|
# Output:
|
||||||
|
# <paths.backupsEc2>/<project>/<timestamp>_<project>_db.sql (projects with a db block)
|
||||||
|
# <paths.backupsEc2>/<project>/<timestamp>_<project>_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() { # <remote_path> <local_path>
|
||||||
|
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() { # <key>
|
||||||
|
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"
|
||||||
80
bash/zconfig.example.json
Normal file
80
bash/zconfig.example.json
Normal file
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
494
bash/zdeploy
Normal file
494
bash/zdeploy
Normal file
@ -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 <project> [<project> ...] [--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 <project> [<project> ...] | 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() { # <key>
|
||||||
|
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() { # <extra_zip_to_remove...>
|
||||||
|
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() { # <label>
|
||||||
|
printf '\n'; info "--- [Post-deploy] Reclaiming disk space ($1) ---"
|
||||||
|
local cmd
|
||||||
|
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+="; sudo find /var/lib/docker/containers/ -name '*-json.log' -size +50M -exec truncate -s 0 {} + 2>/dev/null || true"
|
||||||
|
cmd+="; df -h /"
|
||||||
|
zssh "$cmd" || warn " Post-deploy cleanup returned non-zero; continuing."
|
||||||
|
}
|
||||||
|
|
||||||
|
send_zip() { # <local_zip> <zip_name>
|
||||||
|
scp -i "$(zec2_pem)" "$1" "$(zec2_target):$REMOTE_HOME/" \
|
||||||
|
|| { err "SCP upload failed. Likely server disk space. Try: rm -f $REMOTE_HOME/$2"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_unzip() { # <zip_name> <dest_path>
|
||||||
|
zec2_step "unzip $1" \
|
||||||
|
"test -f $REMOTE_HOME/$1 || { echo 'missing $REMOTE_HOME/$1'; exit 2; }; unzip -t $REMOTE_HOME/$1 || exit 3; unzip -o $REMOTE_HOME/$1 -d $2; uc=\$?; if [ \$uc -gt 1 ]; then exit \$uc; fi; exit 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Deploy verification (build-version match, not just HTTP 200 — a 200 can be
|
||||||
|
# a stale cached build; the version match proves the new build is live) ─────
|
||||||
|
|
||||||
|
wait_verify_api() { # <key> <expected_label> <timeout_sec>
|
||||||
|
local key="$1" expected="$2" timeout="${3:-60}" domain live deadline
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
printf '\n'; info "--- [$key] Live build verification (expect $expected) ---"
|
||||||
|
deadline=$((SECONDS + timeout))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
live="$(http_get "http://$EC2_IP/api/build-version" "$domain" | jq -r '.build_version // empty' 2>/dev/null)"
|
||||||
|
if [ -n "$live" ]; then
|
||||||
|
if [ "$live" = "$expected" ]; then
|
||||||
|
ok " PASS - live build $live matches expected."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn " Live build is $live, expected $expected - waiting..."
|
||||||
|
else
|
||||||
|
dim " /api/build-version not ready yet - waiting..."
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
warn " WARNING: live build did not match $expected within ${timeout}s (a stale build may be cached)."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_verify_static() { # <key> <pre_product_version> <pre_build_number>
|
||||||
|
local key="$1" pv="$2" bn="$3" expected container domain live deadline raw
|
||||||
|
if [ -z "$pv" ] || [ -z "$bn" ]; then
|
||||||
|
printf '\n'; warn "--- [$key version] SKIPPED (no local build-version.json - see 'Enabling deploy verification' in README) ---"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
expected="v${pv}.$((bn + 1))"
|
||||||
|
container="$(zproj "$key" .remote.containerName)"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
printf '\n'; info "--- [$key] Live build verification (expect $expected) ---"
|
||||||
|
deadline=$((SECONDS + 45))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
if [ -n "$container" ]; then
|
||||||
|
# build-version.json may be blocked externally by the edge proxy;
|
||||||
|
# read it inside the running container instead.
|
||||||
|
raw="$(zssh "sudo docker exec $container cat /usr/share/nginx/html/build-version.json 2>/dev/null" 2>/dev/null)"
|
||||||
|
else
|
||||||
|
raw="$(http_get "http://$EC2_IP/build-version.json" "$domain")"
|
||||||
|
fi
|
||||||
|
live="$(printf '%s' "$raw" | jq -r '"v\(.productVersion).\(.buildNumber)"' 2>/dev/null)"
|
||||||
|
if [ -n "$live" ] && [ "$live" != "null" ] && [ "$live" != "v." ]; then
|
||||||
|
if [ "$live" = "$expected" ]; then
|
||||||
|
ok " PASS - live build $live matches expected."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn " Live build is $live, expected $expected - waiting..."
|
||||||
|
else
|
||||||
|
dim " Container not ready yet - waiting..."
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
warn " WARNING: live build did not match $expected within 45s (upload or Docker build may have failed, or a stale build is cached)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fast-forward the checkout before a deploy when deploy.gitPull is set — zdeploy
|
||||||
|
# zips the working tree, so a stale checkout would ship stale code while still
|
||||||
|
# bumping the build number. Abort rather than deploy uncertain code.
|
||||||
|
deploy_git_pull() { # <key>
|
||||||
|
local key="$1" root branch
|
||||||
|
[ "$(zproj "$key" .deploy.gitPull)" = "true" ] || return 0
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
if [ ! -d "$root/.git" ]; then
|
||||||
|
warn " gitPull set but '$root' is not a git repo - skipping pull."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '\n'; info "--- [0] git pull --ff-only ---"
|
||||||
|
branch="$(cd "$root" && git rev-parse --abbrev-ref HEAD)"
|
||||||
|
dim " Branch: $branch"
|
||||||
|
(cd "$root" && git pull --ff-only) || {
|
||||||
|
err "git pull --ff-only failed in '$root' (branch '$branch'). Resolve it, then re-run - refusing to deploy possibly-stale code."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
dim " Now at: $(cd "$root" && git log -1 --oneline)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Kind handlers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
deploy_python() { # <key>
|
||||||
|
local key="$1" start=$SECONDS root remote_path compose_dir app_svc zip_name zip_local
|
||||||
|
local bv="" has_version_tool=0 excl=() attempt out label domain
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
remote_path="$(zproj "$key" .remote.path)"
|
||||||
|
compose_dir="$(zremote_compose_dir "$key")"
|
||||||
|
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="app"
|
||||||
|
zip_name="$(deploy_zip_name "$key")"
|
||||||
|
zip_local="$TEMP_ROOT/$zip_name"
|
||||||
|
label="$(zproj "$key" .label)"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
[ -f "$root/scripts/build_version_tool.py" ] && has_version_tool=1
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "=== $label deploy (python) ==="
|
||||||
|
dim "Local zip: $zip_local"
|
||||||
|
|
||||||
|
printf '\n'; info "--- [1] Zipping $label ---"
|
||||||
|
find "$root" -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
|
||||||
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
||||||
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
||||||
|
|
||||||
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "--- [2] Uploading zip ---"
|
||||||
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
rm -f "$zip_local"
|
||||||
|
|
||||||
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
||||||
|
zec2_step "apt-get install unzip" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
||||||
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
||||||
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
||||||
|
zec2_step "backup .env if present" "if [ -f $remote_path/.env ]; then cp $remote_path/.env $REMOTE_HOME/.env.${key}_bak; fi" || return 1
|
||||||
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
||||||
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
||||||
|
zec2_step "restore .env from backup" "if [ -f $REMOTE_HOME/.env.${key}_bak ]; then cp $REMOTE_HOME/.env.${key}_bak $remote_path/.env; fi" || return 1
|
||||||
|
zec2_step "require compose directory" "test -d $compose_dir" || return 1
|
||||||
|
zec2_step "docker compose build $app_svc" "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose build $app_svc" || return 1
|
||||||
|
zec2_step "docker compose up -d" "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
||||||
|
zec2_step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
||||||
|
|
||||||
|
if [ "$has_version_tool" -eq 1 ]; then
|
||||||
|
printf '\n'; info "--- [4] Incrementing build version ---"
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
out="$(zssh "cd $compose_dir && sudo docker compose exec -T $app_svc python scripts/build_version_tool.py bump" 2>/dev/null | tail -1)"
|
||||||
|
[ -n "$out" ] && { bv="$out"; break; }
|
||||||
|
warn " Attempt $attempt failed, retrying in 3s..."
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
[ -n "$bv" ] || { err "Build version bump failed after 5 attempts"; return 1; }
|
||||||
|
|
||||||
|
(cd "$root" && { python3 scripts/build_version_tool.py set "$bv" >/dev/null 2>&1 || python scripts/build_version_tool.py set "$bv" >/dev/null 2>&1; })
|
||||||
|
zssh "echo '$bv' | sudo tee $remote_path/.build_version > /dev/null"
|
||||||
|
if [ -f "$root/scripts/build_changelog_tool.py" ]; then
|
||||||
|
(cd "$root" && { python3 scripts/build_changelog_tool.py append --version "$bv" --note "$NOTE" >/dev/null 2>&1 \
|
||||||
|
|| python scripts/build_changelog_tool.py append --version "$bv" --note "$NOTE" >/dev/null 2>&1; })
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "--- [5] Restarting app to pick up new version ---"
|
||||||
|
zssh "cd $compose_dir && sudo COMPOSE_BAKE=false docker compose restart $app_svc" \
|
||||||
|
|| { err "App restart after build bump failed"; return 1; }
|
||||||
|
|
||||||
|
wait_verify_api "$key" "$bv" 30 || true
|
||||||
|
else
|
||||||
|
printf '\n'; info "--- [4] Basic reachability check (no build_version_tool - see 'Enabling deploy verification' in README) ---"
|
||||||
|
local deadline=$((SECONDS + 30)) code up=0
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
sleep 3
|
||||||
|
code="$(http_code "http://$EC2_IP/" "$domain")"
|
||||||
|
if [ "$code" != "000" ] && [ "$code" -lt 500 ]; then up=1; break; fi
|
||||||
|
dim " App not ready yet - waiting..."
|
||||||
|
done
|
||||||
|
if [ "$up" -eq 1 ]; then ok " App is responding."; else warn " WARNING: app did not respond within 30s."; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
post_cleanup "$key"
|
||||||
|
|
||||||
|
local elapsed=$((SECONDS - start))
|
||||||
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
||||||
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
||||||
|
[ -n "$bv" ] && note "Build Version: $bv"
|
||||||
|
info "Change Note: $NOTE"
|
||||||
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_vite() { # <key>
|
||||||
|
local key="$1" start=$SECONDS root remote_path zip_name zip_local excl=()
|
||||||
|
local pre_pv="" pre_bn="" label domain edge_pc
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
remote_path="$(zproj "$key" .remote.path)"
|
||||||
|
zip_name="$(deploy_zip_name "$key")"
|
||||||
|
zip_local="$TEMP_ROOT/$zip_name"
|
||||||
|
label="$(zproj "$key" .label)"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "=== $label deploy (vite/static) ==="
|
||||||
|
dim "Local zip: $zip_local"
|
||||||
|
|
||||||
|
printf '\n'; info "--- [1] Zipping site ---"
|
||||||
|
if [ -f "$root/build-version.json" ]; then
|
||||||
|
pre_pv="$(jq -r '.productVersion // empty' "$root/build-version.json")"
|
||||||
|
pre_bn="$(jq -r '.buildNumber // empty' "$root/build-version.json")"
|
||||||
|
[ -n "$pre_pv" ] && dim " Pre-zip build label: v${pre_pv}.${pre_bn} (server-side build will bump +1)"
|
||||||
|
fi
|
||||||
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
||||||
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
||||||
|
|
||||||
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "--- [2] Uploading zip ---"
|
||||||
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
rm -f "$zip_local"
|
||||||
|
|
||||||
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
||||||
|
zec2_step "apt-get install unzip" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
||||||
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
||||||
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
||||||
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
||||||
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
||||||
|
zec2_step "require compose file" "test -f $remote_path/docker-compose.yml" || return 1
|
||||||
|
zec2_step "docker compose build" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose build" || return 1
|
||||||
|
zec2_step "docker compose up -d" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
||||||
|
|
||||||
|
local ek; ek="$(zedge_key)"
|
||||||
|
if [ -n "$ek" ]; then
|
||||||
|
edge_pc="$(zproj "$ek" .proxyContainer)"
|
||||||
|
[ -n "$edge_pc" ] && { zec2_step "reload edge nginx (flush DNS cache for new container IP)" "sudo docker exec $edge_pc nginx -s reload" || return 1; }
|
||||||
|
fi
|
||||||
|
zec2_step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
||||||
|
|
||||||
|
wait_verify_static "$key" "$pre_pv" "$pre_bn"
|
||||||
|
post_cleanup "$key"
|
||||||
|
|
||||||
|
local elapsed=$((SECONDS - start))
|
||||||
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
||||||
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
||||||
|
info "Change Note: $NOTE"
|
||||||
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_next() { # <key>
|
||||||
|
local key="$1" start=$SECONDS root remote_path app_svc zip_name zip_local excl=()
|
||||||
|
local pre_pv="" pre_bn="" label domain db_user db_name prod_port
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
remote_path="$(zproj "$key" .remote.path)"
|
||||||
|
app_svc="$(zproj "$key" .remote.appService)"; [ -n "$app_svc" ] || app_svc="web"
|
||||||
|
zip_name="$(deploy_zip_name "$key")"
|
||||||
|
zip_local="$TEMP_ROOT/$zip_name"
|
||||||
|
label="$(zproj "$key" .label)"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "=== $label deploy (nextjs) ==="
|
||||||
|
dim "Local zip: $zip_local"
|
||||||
|
|
||||||
|
if [ -f "$root/public/build-version.json" ]; then
|
||||||
|
pre_pv="$(jq -r '.productVersion // empty' "$root/public/build-version.json")"
|
||||||
|
pre_bn="$(jq -r '.buildNumber // empty' "$root/public/build-version.json")"
|
||||||
|
[ -n "$pre_pv" ] && dim " Pre-zip build label: v${pre_pv}.${pre_bn} (server-side build will bump +1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "--- [1] Zipping project files ---"
|
||||||
|
mapfile -t excl < <(z_archive_excludes "$key" 0)
|
||||||
|
z_archive "$root" "$zip_local" 0 "-" "${excl[@]}" || return 1
|
||||||
|
|
||||||
|
preflight_cleanup "$REMOTE_HOME/$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "--- [2] Uploading zip ---"
|
||||||
|
send_zip "$zip_local" "$zip_name" || { rm -f "$zip_local"; return 1; }
|
||||||
|
rm -f "$zip_local"
|
||||||
|
|
||||||
|
printf '\n'; info "--- [3] Unzipping and rebuilding on the server ---"
|
||||||
|
zec2_step "ensure unzip installed" "sudo apt-get update -qq && sudo apt-get install -y unzip" || return 1
|
||||||
|
zec2_step "ensure stack root" "sudo mkdir -p $STACK_ROOT && sudo chown ${EC2_USER}:${EC2_USER} $STACK_ROOT" || return 1
|
||||||
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
||||||
|
zec2_step "backup .env if present" "if [ -f $remote_path/.env ]; then cp $remote_path/.env $REMOTE_HOME/.env.${key}_bak; fi" || return 1
|
||||||
|
zec2_step "replace project directory" "sudo rm -rf $remote_path && sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
||||||
|
remote_unzip "$zip_name" "$remote_path" || return 1
|
||||||
|
zec2_step "restore .env from backup" "if [ -f $REMOTE_HOME/.env.${key}_bak ]; then cp $REMOTE_HOME/.env.${key}_bak $remote_path/.env; fi" || return 1
|
||||||
|
|
||||||
|
printf '\n'; info "--- [4] Docker compose rebuild ---"
|
||||||
|
zec2_step "docker compose down" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose down" || return 1
|
||||||
|
zec2_step "docker compose build" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose build" || return 1
|
||||||
|
zec2_step "docker compose up -d" "cd $remote_path && sudo COMPOSE_BAKE=false docker compose up -d" || return 1
|
||||||
|
|
||||||
|
db_user="$(zproj "$key" .db.user)"; db_name="$(zproj "$key" .db.name)"
|
||||||
|
if [ -n "$db_user" ] && [ -n "$db_name" ]; then
|
||||||
|
zec2_step "wait for postgres ready" \
|
||||||
|
"cd $remote_path && for i in \$(seq 1 30); do sudo docker compose exec -T db pg_isready -U $db_user -d $db_name >/dev/null 2>&1 && break; sleep 2; done" || return 1
|
||||||
|
fi
|
||||||
|
if [ "$(zproj "$key" .migrations)" = "prisma" ]; then
|
||||||
|
zec2_step "apply prisma migrations" "cd $remote_path && sudo docker compose exec -T $app_svc npx prisma migrate deploy" || return 1
|
||||||
|
fi
|
||||||
|
zec2_step "record deploy timestamp; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remote_path/.last_deploy_utc > /dev/null && rm -f $REMOTE_HOME/$zip_name" || return 1
|
||||||
|
|
||||||
|
printf '\n'; info "--- [5] Verifying deployment ---"
|
||||||
|
prod_port="$(zproj "$key" .ports.prod)"
|
||||||
|
if [[ "$prod_port" =~ ^[0-9]+$ ]]; then
|
||||||
|
local direct="http://${EC2_IP}:${prod_port}/" deadline=$((SECONDS + 60)) code verified=0
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
sleep 4
|
||||||
|
code="$(http_code "$direct")"
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
ok " PASS - app is responding at $direct"
|
||||||
|
verified=1; break
|
||||||
|
fi
|
||||||
|
dim " App not ready yet - waiting..."
|
||||||
|
done
|
||||||
|
[ "$verified" -eq 1 ] || warn " WARNING: no response at $direct within 60s (is the port open in the security group?)."
|
||||||
|
fi
|
||||||
|
if [ -n "$pre_pv" ]; then
|
||||||
|
wait_verify_api "$key" "v${pre_pv}.$((pre_bn + 1))" 60 || true
|
||||||
|
else
|
||||||
|
warn " (No public/build-version.json - version verification skipped. See 'Enabling deploy verification' in README.)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
post_cleanup "$key"
|
||||||
|
|
||||||
|
local elapsed=$((SECONDS - start))
|
||||||
|
printf '\n'; ok "--- [Done] $label deployed! ---"
|
||||||
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
||||||
|
info "Change Note: $NOTE"
|
||||||
|
dim "Deploy Time: $(printf '%02d:%02d' $((elapsed / 60)) $((elapsed % 60))) (${elapsed}s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_edge() { # <key>
|
||||||
|
local key="$1" root remote_path pc label cert_mount="" rm_stale="" f
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
remote_path="$(zproj "$key" .remote.path)"
|
||||||
|
pc="$(zproj "$key" .proxyContainer)"
|
||||||
|
label="$(zproj "$key" .label)"
|
||||||
|
|
||||||
|
printf '\n'; info "=== $label deploy (edge nginx ingress) ==="
|
||||||
|
[ -d "$root" ] || { err "Edge root not found: $root"; return 1; }
|
||||||
|
for f in docker-compose.yml nginx.conf; do
|
||||||
|
[ -f "$root/$f" ] || { err "Missing $root/$f"; return 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
||||||
|
zec2_step "ensure edge dir" "sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
||||||
|
|
||||||
|
# Ship every top-level file in the edge folder — nginx.conf, compose, css,
|
||||||
|
# htpasswd, whatever the proxy serves. Subdirectories (logs, certs) stay put.
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
info " >> uploading $(basename "$f")"
|
||||||
|
scp -i "$(zec2_pem)" "$f" "$(zec2_target):$remote_path/" \
|
||||||
|
|| { err "SCP failed for $(basename "$f")"; return 1; }
|
||||||
|
done < <(find "$root" -maxdepth 1 -type f ! -name nul -print0)
|
||||||
|
|
||||||
|
local certs; certs="$(zproj "$key" .certsSource)"
|
||||||
|
[ -n "$certs" ] && cert_mount="-v $certs:/etc/letsencrypt/:ro "
|
||||||
|
zec2_step "validate new nginx.conf" \
|
||||||
|
"sudo docker run --rm -v $remote_path/nginx.conf:/etc/nginx/nginx.conf:ro ${cert_mount}nginx:1.27-alpine nginx -t -c /etc/nginx/nginx.conf" || return 1
|
||||||
|
|
||||||
|
# A container from an older compose project may still hold the proxy name;
|
||||||
|
# docker refuses a second create with the same name, so remove it first.
|
||||||
|
[ -n "$pc" ] && rm_stale="; sudo docker rm -f $pc 2>/dev/null || true"
|
||||||
|
zec2_step "edge: compose down + remove stale proxy" "cd $remote_path && sudo docker compose down 2>/dev/null || true$rm_stale" || return 1
|
||||||
|
zec2_step "edge compose up -d" "cd $remote_path && sudo docker compose up -d" || return 1
|
||||||
|
[ -n "$pc" ] && { zec2_step "edge nginx reload" "sudo docker exec $pc nginx -s reload || true" || return 1; }
|
||||||
|
zec2_step "fix nginx-logs permissions (if present)" \
|
||||||
|
"if [ -d $remote_path/nginx-logs ]; then sudo chmod 777 $remote_path/nginx-logs; sudo chmod 666 $remote_path/nginx-logs/*.log 2>/dev/null || true; fi" || return 1
|
||||||
|
ok "--- [Done] Edge proxy deploy finished ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_docker() { # <key>
|
||||||
|
local key="$1" root remote_path label f domain
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
remote_path="$(zproj "$key" .remote.path)"
|
||||||
|
label="$(zproj "$key" .label)"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
|
||||||
|
printf '\n'; info "=== $label deploy (docker compose) ==="
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
[ -f "$root/docker-compose.yml" ] || { err "Missing $root/docker-compose.yml"; return 1; }
|
||||||
|
|
||||||
|
zec2_step "ensure shared web network" "sudo docker network create web 2>/dev/null || true" || return 1
|
||||||
|
zec2_step "ensure project dir" "sudo mkdir -p $remote_path && sudo chown ${EC2_USER}:${EC2_USER} $remote_path" || return 1
|
||||||
|
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
info " >> uploading $(basename "$f")"
|
||||||
|
scp -i "$(zec2_pem)" "$f" "$(zec2_target):$remote_path/" \
|
||||||
|
|| { err "SCP failed for $(basename "$f")"; return 1; }
|
||||||
|
done < <(find "$root" -maxdepth 1 -type f ! -name nul -print0)
|
||||||
|
|
||||||
|
zec2_step "docker compose pull" "cd $remote_path && sudo docker compose pull" || return 1
|
||||||
|
zec2_step "docker compose up -d" "cd $remote_path && sudo docker compose up -d" || return 1
|
||||||
|
|
||||||
|
post_cleanup "$key"
|
||||||
|
printf '\n'; ok "--- [Done] $label deploy finished ---"
|
||||||
|
[ -n "$domain" ] && warn "Site: https://$domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
deploy_git_pull "$key" || exit 1
|
||||||
|
kind="$(zproj "$key" .kind)"
|
||||||
|
case "$kind" in
|
||||||
|
python) deploy_python "$key" || exit 1 ;;
|
||||||
|
vite) deploy_vite "$key" || exit 1 ;;
|
||||||
|
nextjs) deploy_next "$key" || exit 1 ;;
|
||||||
|
edge) deploy_edge "$key" || exit 1 ;;
|
||||||
|
docker) deploy_docker "$key" || exit 1 ;;
|
||||||
|
*) err "No deploy handler for kind '$kind' (project '$key'). Add a deploy_<kind> function in zdeploy."; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
93
bash/zec2
Normal file
93
bash/zec2
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zec2 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zec2 # every project with a "domain" in zconfig.json
|
||||||
|
# zec2 <project> [<project> ...]
|
||||||
|
# zec2 viteapp --host 203.0.113.10
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require curl
|
||||||
|
|
||||||
|
projects=(); host_override=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--host) host_override="${2:-}"; shift 2 ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
HOST="${host_override:-$(zec2_ip)}"
|
||||||
|
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
mapfile -t projects < <(zproj_keys_with_domain)
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
warn "No projects with a 'domain' configured in zconfig.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_reachability() { # <label> <port> <host-header>
|
||||||
|
local label="$1" port="$2" hh="$3" code
|
||||||
|
printf '\n'; info "=== zec2: $label ==="
|
||||||
|
dim " Target: ${HOST}:${port}${hh:+ (Host: $hh)}"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
warn " [1/2] TCP connect to port ${port}..."
|
||||||
|
if tcp_check "$HOST" "$port"; then
|
||||||
|
ok " OK - port ${port} is open (TCP succeeded)"
|
||||||
|
else
|
||||||
|
err " FAIL - port ${port} did not accept TCP (check firewall/security group + app on the server)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn " [2/2] HTTP GET http://${HOST}:${port}/ ..."
|
||||||
|
code="$(http_code "http://${HOST}:${port}/" "$hh")"
|
||||||
|
if [ "$code" = "000" ]; then
|
||||||
|
err " FAIL - no HTTP response (connection dropped or timed out)"
|
||||||
|
return 1
|
||||||
|
elif [ "$code" -lt 400 ]; then
|
||||||
|
ok " OK - HTTP $code"
|
||||||
|
else
|
||||||
|
warn " HTTP response: $code (connection worked; app may redirect or require auth)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info " Done ($label)."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
show_live_version() { # <key>
|
||||||
|
local label
|
||||||
|
label="$(remote_version_label "$1" "$HOST")"
|
||||||
|
[ "$label" != "unknown" ] && dim " Live build: $label" \
|
||||||
|
|| dim " (Could not read live version endpoint - see 'Enabling deploy verification' in README)"
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_code=0
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
if [ -z "$domain" ]; then
|
||||||
|
printf '\n'; warn "Skipping '$key' - no domain configured."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
label="$(zproj "$key" .label)"; [ -n "$label" ] || label="$key"
|
||||||
|
if check_reachability "$label" 80 "$domain"; then
|
||||||
|
show_live_version "$key"
|
||||||
|
else
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
info "If TCP fails: open inbound TCP in the server's firewall/security group."
|
||||||
|
info "If TCP OK but HTTP fails: SSH in and check docker/nginx (curl -sI http://127.0.0.1/)."
|
||||||
|
printf '\n'
|
||||||
|
exit "$exit_code"
|
||||||
148
bash/zec2online
Normal file
148
bash/zec2online
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zec2online — deep health check: verify apps are live AND running the expected
|
||||||
|
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zec2online # every project with a "domain" in zconfig.json
|
||||||
|
# zec2online <project> [<project> ...]
|
||||||
|
#
|
||||||
|
# A plain HTTP-200 check passes even when a stale cached build is live — the
|
||||||
|
# local-vs-server version match is what proves the deployed build is the one running.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require curl ssh
|
||||||
|
|
||||||
|
projects=(); host_override=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--host) host_override="${2:-}"; shift 2 ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
HOST="${host_override:-$(zec2_ip)}"
|
||||||
|
EDGE_KEY="$(zedge_key)"
|
||||||
|
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
mapfile -t projects < <(zproj_keys_with_domain)
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
warn "No projects with a 'domain' configured in zconfig.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
compare_versions() { # <key> <local> <server>
|
||||||
|
local key="$1" local_v="$2" server_v="$3"
|
||||||
|
if [ "$local_v" = "unknown" ] && [ "$server_v" = "unknown" ]; then
|
||||||
|
dim " Version: unable to determine local or server version (see 'Enabling deploy verification' in README)"
|
||||||
|
elif [ "$local_v" = "unknown" ]; then
|
||||||
|
note " Server: $server_v"; dim " Local: unknown (could not read local build version)"
|
||||||
|
elif [ "$server_v" = "unknown" ]; then
|
||||||
|
note " Local: $local_v"; dim " Server: unknown (could not read server build version)"
|
||||||
|
elif [ "$local_v" = "$server_v" ]; then
|
||||||
|
ok " Version: $server_v (local and server match)"
|
||||||
|
else
|
||||||
|
note " Local: $local_v"; note " Server: $server_v"
|
||||||
|
warn " WARNING: local and server versions differ - redeploy with: zdeploy $key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_http_online() { # <host-header>
|
||||||
|
local code
|
||||||
|
code="$(http_code "http://${HOST}/" "$1")"
|
||||||
|
if [ "$code" = "000" ]; then return 1; fi
|
||||||
|
if [ "$code" -lt 500 ]; then
|
||||||
|
if [ "$code" -ge 300 ]; then dim " HTTP $code - redirect from app, server is live."
|
||||||
|
else dim " HTTP $code - service reachable."; fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn " HTTP $code - unexpected server response."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
show_diagnostics() { # <key> <reason>
|
||||||
|
local key="$1" reason="$2" compose_dir cmd pc
|
||||||
|
[ -f "$(zec2_pem)" ] || { dim " Diagnostics skipped: PEM key not found at $(zec2_pem)"; return; }
|
||||||
|
compose_dir="$(zremote_compose_dir "$key")"
|
||||||
|
printf '\n'; note " --- $key diagnostics ($reason) ---"
|
||||||
|
cmd="echo '== services =='; cd $compose_dir 2>/dev/null && sudo docker compose ps"
|
||||||
|
cmd+="; echo '== uptime / df =='; uptime; df -h /"
|
||||||
|
cmd+="; echo '== oom =='; dmesg -T 2>/dev/null | grep -iE 'oom|killed' | tail -n 10"
|
||||||
|
cmd+="; echo '== app logs (80) =='; cd $compose_dir 2>/dev/null && sudo docker compose logs --tail 80"
|
||||||
|
if [ -n "$EDGE_KEY" ]; then
|
||||||
|
pc="$(zproj "$EDGE_KEY" .proxyContainer)"
|
||||||
|
if [ -n "$pc" ]; then
|
||||||
|
cmd+="; echo '== edge proxy state =='; sudo docker ps --filter name=$pc --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||||
|
cmd+="; echo '== edge proxy logs (40) =='; sudo docker logs --tail 40 $pc 2>&1 | tail -n 40"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
zssh "$cmd" || warn " Diagnostics SSH failed."
|
||||||
|
note " --- end $key diagnostics ---"; printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
online_check() { # <key>
|
||||||
|
local key="$1" label domain compose_dir start_cmd edge_dir deadline
|
||||||
|
label="$(zproj "$key" .label)"; [ -n "$label" ] || label="$key"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
|
||||||
|
printf '\n'; info "=== zec2online: $label ($key) ==="
|
||||||
|
dim " Target: http://${HOST}/ (Host: $domain)"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
warn ' [1] Checking TCP + HTTP...'
|
||||||
|
if test_http_online "$domain"; then
|
||||||
|
compare_versions "$key" "$(local_version_label "$key")" "$(remote_version_label "$key" "$HOST")"
|
||||||
|
ok " UP - $label is running."
|
||||||
|
printf '\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
err ' DOWN - Service not responding.'
|
||||||
|
show_diagnostics "$key" 'DOWN before recovery'
|
||||||
|
|
||||||
|
warn ' [2] Starting stack (and edge proxy if defined)...'
|
||||||
|
compose_dir="$(zremote_compose_dir "$key")"
|
||||||
|
[ -f "$(zec2_pem)" ] || { err " Cannot start: PEM key not found at: $(zec2_pem)"; return 1; }
|
||||||
|
start_cmd="sudo docker network create web 2>/dev/null || true"
|
||||||
|
start_cmd+="; if [ -f $compose_dir/docker-compose.yml ]; then cd $compose_dir && sudo docker compose up -d 2>&1 | tail -10; else echo 'compose missing at $compose_dir'; exit 2; fi"
|
||||||
|
if [ -n "$EDGE_KEY" ]; then
|
||||||
|
edge_dir="$(zproj "$EDGE_KEY" .remote.path)"
|
||||||
|
[ -n "$edge_dir" ] && start_cmd+="; if [ -f $edge_dir/docker-compose.yml ]; then cd $edge_dir && sudo docker compose up -d 2>&1 | tail -10; fi"
|
||||||
|
fi
|
||||||
|
zssh "$start_cmd" || { err " SSH failed. Check connectivity and PEM key."; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; warn ' [3] Waiting for service (up to 30s)...'
|
||||||
|
deadline=$((SECONDS + 30))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
sleep 3
|
||||||
|
dim ' checking...'
|
||||||
|
if test_http_online "$domain"; then
|
||||||
|
printf '\n'
|
||||||
|
compare_versions "$key" "$(local_version_label "$key")" "$(remote_version_label "$key" "$HOST")"
|
||||||
|
ok " UP - $label is now running."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n'; err ' TIMEOUT - Service did not respond within 30 seconds.'
|
||||||
|
show_diagnostics "$key" 'recovery TIMEOUT'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_code=0
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
if [ -z "$(zproj "$key" .domain)" ]; then
|
||||||
|
printf '\n'; warn "Skipping '$key' - no domain configured."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
online_check "$key" || exit_code=1
|
||||||
|
done
|
||||||
|
exit "$exit_code"
|
||||||
239
bash/zhelpers.sh
Normal file
239
bash/zhelpers.sh
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zhelpers.sh — shared library sourced by every z-script (bash port). Not run directly.
|
||||||
|
|
||||||
|
# ---- config location --------------------------------------------------------
|
||||||
|
_ZDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# zconfig.json lives next to the scripts; override with $ZCONFIG.
|
||||||
|
ZCONFIG="${ZCONFIG:-$_ZDIR/zconfig.json}"
|
||||||
|
|
||||||
|
# ---- colours (only when stdout is a TTY) ------------------------------------
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
C_RESET=$'\033[0m'; C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'
|
||||||
|
C_YELLOW=$'\033[33m'; C_RED=$'\033[31m'; C_GRAY=$'\033[90m'; C_MAGENTA=$'\033[35m'
|
||||||
|
else
|
||||||
|
C_RESET= C_CYAN= C_GREEN= C_YELLOW= C_RED= C_GRAY= C_MAGENTA=
|
||||||
|
fi
|
||||||
|
info() { printf '%s%s%s\n' "$C_CYAN" "$*" "$C_RESET"; }
|
||||||
|
ok() { printf '%s%s%s\n' "$C_GREEN" "$*" "$C_RESET"; }
|
||||||
|
warn() { printf '%s%s%s\n' "$C_YELLOW" "$*" "$C_RESET"; }
|
||||||
|
err() { printf '%s%s%s\n' "$C_RED" "$*" "$C_RESET" >&2; }
|
||||||
|
dim() { printf '%s%s%s\n' "$C_GRAY" "$*" "$C_RESET"; }
|
||||||
|
note() { printf '%s%s%s\n' "$C_MAGENTA" "$*" "$C_RESET"; }
|
||||||
|
|
||||||
|
# ---- dependency guard -------------------------------------------------------
|
||||||
|
z_require() {
|
||||||
|
local miss=0 t
|
||||||
|
for t in "$@"; do
|
||||||
|
command -v "$t" >/dev/null 2>&1 || { err "Missing required tool: $t"; miss=1; }
|
||||||
|
done
|
||||||
|
[ "$miss" -eq 0 ] || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- config accessors (jq) --------------------------------------------------
|
||||||
|
z_need_config() {
|
||||||
|
z_require jq
|
||||||
|
if [ ! -f "$ZCONFIG" ]; then
|
||||||
|
err "zconfig.json not found at $ZCONFIG"
|
||||||
|
dim " Copy zconfig.example.json to zconfig.json and fill in your values."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
zq() { jq -r "$1" "$ZCONFIG"; }
|
||||||
|
# All project keys, config order, skipping _comment-style keys.
|
||||||
|
zproj_keys() { zq '.projects | keys_unsorted[] | select(startswith("_") | not)'; }
|
||||||
|
zproj_csv() { zproj_keys | paste -sd',' -; }
|
||||||
|
# Keys of projects that have a "domain" (deployed / publicly reachable).
|
||||||
|
zproj_keys_with_domain() {
|
||||||
|
jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.domain != null) | .key' "$ZCONFIG"
|
||||||
|
}
|
||||||
|
# Field of a project: zproj <key> <dotpath> e.g. zproj sp .kind | zproj sp .ports.dev
|
||||||
|
zproj() { jq -r --arg k "$1" ".projects[\$k]$2 // empty" "$ZCONFIG"; }
|
||||||
|
zproj_require() {
|
||||||
|
if [ "$(jq -r --arg k "$1" '(.projects[$k] != null)' "$ZCONFIG")" != "true" ]; then
|
||||||
|
err "Unknown project key '$1'. Available: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
zec2_ip() { zq '.ec2.ip'; }
|
||||||
|
zec2_user() { zq '.ec2.user'; }
|
||||||
|
zec2_pem() { zq '.ec2.pemKey'; }
|
||||||
|
zec2_target() { printf '%s@%s' "$(zec2_user)" "$(zec2_ip)"; }
|
||||||
|
# Remote compose directory: remote.composeDir if set, else remote.path.
|
||||||
|
zremote_compose_dir() {
|
||||||
|
local d
|
||||||
|
d="$(zproj "$1" .remote.composeDir)"
|
||||||
|
[ -n "$d" ] || d="$(zproj "$1" .remote.path)"
|
||||||
|
printf '%s' "$d"
|
||||||
|
}
|
||||||
|
# First project of kind 'edge' (or empty).
|
||||||
|
zedge_key() {
|
||||||
|
jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind == "edge") | .key' "$ZCONFIG" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- SSH helpers ------------------------------------------------------------
|
||||||
|
# Run a bash command on the server. zssh is bare; zec2_step adds a label and
|
||||||
|
# exits non-zero loudly (mirrors Invoke-Ec2Step).
|
||||||
|
zssh() {
|
||||||
|
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 -i "$(zec2_pem)" "$(zec2_target)" "$@"
|
||||||
|
}
|
||||||
|
zec2_step() {
|
||||||
|
local label="$1"; shift
|
||||||
|
dim " >> $label"
|
||||||
|
zssh "$@"
|
||||||
|
local rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
err "Remote step failed: '$label' (exit $rc)."
|
||||||
|
return $rc
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- HTTP / TCP helpers -----------------------------------------------------
|
||||||
|
# HTTP status code for a URL with optional Host header ("000" on connect fail).
|
||||||
|
http_code() {
|
||||||
|
local url="$1" host_header="${2:-}" args=(-s -o /dev/null -w '%{http_code}' -L --max-time 15)
|
||||||
|
[ -n "$host_header" ] && args+=(-H "Host: $host_header")
|
||||||
|
curl "${args[@]}" "$url" 2>/dev/null || printf '000'
|
||||||
|
}
|
||||||
|
# GET a URL body (with Host header); empty on failure.
|
||||||
|
http_get() {
|
||||||
|
local url="$1" host_header="${2:-}" args=(-s --max-time 10)
|
||||||
|
[ -n "$host_header" ] && args+=(-H "Host: $host_header")
|
||||||
|
curl "${args[@]}" "$url" 2>/dev/null
|
||||||
|
}
|
||||||
|
# TCP connect check via bash /dev/tcp (uses `timeout` when available).
|
||||||
|
tcp_check() {
|
||||||
|
local host="$1" port="$2"
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout 5 bash -c "exec 3<>/dev/tcp/$host/$port" 2>/dev/null
|
||||||
|
else
|
||||||
|
bash -c "exec 3<>/dev/tcp/$host/$port" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- build-version helpers --------------------------------------------------
|
||||||
|
# "v<productVersion>.<buildNumber>" from a build-version.json file, or "unknown".
|
||||||
|
json_build_label() {
|
||||||
|
local f="$1"
|
||||||
|
[ -f "$f" ] || { printf 'unknown'; return; }
|
||||||
|
jq -r '"v\(.productVersion).\(.buildNumber)"' "$f" 2>/dev/null || printf 'unknown'
|
||||||
|
}
|
||||||
|
# Local build label by project kind (mirrors Get-LocalVersionLabel).
|
||||||
|
local_version_label() {
|
||||||
|
local key="$1" kind root out
|
||||||
|
kind="$(zproj "$key" .kind)"; root="$(zproj "$key" .localRoot)"
|
||||||
|
case "$kind" in
|
||||||
|
python)
|
||||||
|
if [ -f "$root/scripts/build_version_tool.py" ]; then
|
||||||
|
out="$(cd "$root" && { python3 scripts/build_version_tool.py get 2>/dev/null || python scripts/build_version_tool.py get 2>/dev/null; } | tail -1)"
|
||||||
|
[ -n "$out" ] && { printf '%s' "$out"; return; }
|
||||||
|
fi
|
||||||
|
[ -f "$root/.build_version" ] && { tail -1 "$root/.build_version" | tr -d '[:space:]'; return; }
|
||||||
|
printf 'unknown' ;;
|
||||||
|
vite) json_build_label "$root/build-version.json" ;;
|
||||||
|
nextjs) json_build_label "$root/public/build-version.json" ;;
|
||||||
|
*) printf 'unknown' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
# Live/server build label by kind (HTTP endpoints; python falls back to SSH).
|
||||||
|
remote_version_label() {
|
||||||
|
local key="$1" host="$2" kind domain body label
|
||||||
|
kind="$(zproj "$key" .kind)"; domain="$(zproj "$key" .domain)"
|
||||||
|
if [ "$kind" = "vite" ]; then
|
||||||
|
body="$(http_get "http://$host/build-version.json" "$domain")"
|
||||||
|
label="$(printf '%s' "$body" | jq -r '"v\(.productVersion).\(.buildNumber)"' 2>/dev/null)"
|
||||||
|
[ -n "$label" ] && [ "$label" != "null" ] && { printf '%s' "$label"; return; }
|
||||||
|
else
|
||||||
|
body="$(http_get "http://$host/api/build-version" "$domain")"
|
||||||
|
label="$(printf '%s' "$body" | jq -r '.build_version // empty' 2>/dev/null)"
|
||||||
|
[ -n "$label" ] && { printf '%s' "$label"; return; }
|
||||||
|
if [ "$kind" = "python" ] && [ -f "$(zec2_pem)" ]; then
|
||||||
|
label="$(zssh "cat $(zproj "$key" .remote.path)/.build_version 2>/dev/null || true" 2>/dev/null | tail -1 | tr -d '[:space:]')"
|
||||||
|
[ -n "$label" ] && { printf '%s' "$label"; return; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- MOTD -------------------------------------------------------------------
|
||||||
|
# Print a random banner from <root>/motd/*.txt (the PowerShell version keeps a
|
||||||
|
# shuffled rotation state; the bash port picks at random — same spirit, simpler).
|
||||||
|
show_project_motd() {
|
||||||
|
local root="$1" files=()
|
||||||
|
[ -d "$root/motd" ] || return 0
|
||||||
|
while IFS= read -r -d '' f; do files+=("$f"); done \
|
||||||
|
< <(find "$root/motd" -maxdepth 1 -name '*.txt' -type f -print0 2>/dev/null)
|
||||||
|
[ "${#files[@]}" -gt 0 ] || return 0
|
||||||
|
printf '\n'; cat "${files[RANDOM % ${#files[@]}]}"; printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- archive builder --------------------------------------------------------
|
||||||
|
# Junk filtered out of every deploy/backup zip (mirrors the PowerShell lists).
|
||||||
|
_Z_JUNK_DIRS=(.git .svn .hg __pycache__ .pytest_cache .mypy_cache .ruff_cache .tox
|
||||||
|
node_modules .npm .yarn .pnpm-store .next .nuxt .svelte-kit .turbo .parcel-cache
|
||||||
|
.cache .idea .vscode coverage htmlcov .nyc_output)
|
||||||
|
_Z_JUNK_EXTS=(swp swo swn tmp orig rej bak backup old pyc pyo pyd tsbuildinfo log
|
||||||
|
db sqlite sqlite3 zip dmg arj gz tgz tar rar 7z iso bz2 xz lz lzma cab jar war ear z zst zstd)
|
||||||
|
_Z_SCRIPT_EXTS=(ps1 cmd bat)
|
||||||
|
_Z_JUNK_NAMES=(.DS_Store Thumbs.db desktop.ini)
|
||||||
|
|
||||||
|
# Top-level exclude names for a project's archive (mirrors Get-ArchiveExcludes).
|
||||||
|
# Usage: z_archive_excludes <key> [for_backup:0|1] -> one name per line
|
||||||
|
z_archive_excludes() {
|
||||||
|
local key="$1" for_backup="${2:-0}" kind
|
||||||
|
kind="$(zproj "$key" .kind)"
|
||||||
|
printf '%s\n' .git .idea .vscode .claude tmp nul .DS_Store backups
|
||||||
|
case "$kind" in
|
||||||
|
python)
|
||||||
|
printf '%s\n' .venv venv __pycache__ .pytest_cache .nicegui archive dist build htmlcov
|
||||||
|
[ "$for_backup" -eq 1 ] || printf '%s\n' uploads ;; # deploys exclude user uploads; backups keep them
|
||||||
|
vite) printf '%s\n' node_modules dist ;;
|
||||||
|
nextjs) printf '%s\n' node_modules .next .env .env.local .env.production .vercel coverage out build next-env.d.ts ;;
|
||||||
|
esac
|
||||||
|
jq -r --arg k "$key" '.projects[$k].deploy.exclude // [] | .[]' "$ZCONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a zip from a source directory.
|
||||||
|
# Usage: z_archive <src_dir> <dest_zip> <include_scripts:0|1> <extra_file|-> [top_excludes...]
|
||||||
|
z_archive() {
|
||||||
|
local src="$1" dest="$2" include_scripts="$3" extra="$4"; shift 4
|
||||||
|
z_require zip
|
||||||
|
[ -d "$src" ] || { err "Source path is not a directory: $src"; return 1; }
|
||||||
|
rm -f "$dest"; mkdir -p "$(dirname "$dest")"
|
||||||
|
|
||||||
|
local pat=() e n
|
||||||
|
for n in "$@"; do [ -n "$n" ] && pat+=("$n" "$n/*"); done
|
||||||
|
for n in "${_Z_JUNK_DIRS[@]}"; do pat+=("$n/*" "*/$n/*"); done
|
||||||
|
for e in "${_Z_JUNK_EXTS[@]}"; do pat+=("*.${e}"); done
|
||||||
|
if [ "$include_scripts" -ne 1 ]; then
|
||||||
|
for e in "${_Z_SCRIPT_EXTS[@]}"; do pat+=("*.${e}"); done
|
||||||
|
fi
|
||||||
|
for n in "${_Z_JUNK_NAMES[@]}"; do pat+=("$n" "*/$n"); done
|
||||||
|
|
||||||
|
( cd "$src" && zip -rq "$dest" . -x "${pat[@]}" ) || { err "zip failed for $src"; return 1; }
|
||||||
|
if [ -n "$extra" ] && [ "$extra" != "-" ] && [ -f "$extra" ]; then
|
||||||
|
zip -jq "$dest" "$extra" || warn " could not add extra file: $extra"
|
||||||
|
fi
|
||||||
|
dim " Archive: $dest ($(z_size_mb "$dest") MB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# File size in MB (portable: bytes via wc).
|
||||||
|
z_size_mb() { awk -v b="$(wc -c < "$1" 2>/dev/null || echo 0)" 'BEGIN{printf "%.2f", b/1048576}'; }
|
||||||
|
|
||||||
|
# Percent-decode a URL component (for DATABASE_URL passwords).
|
||||||
|
z_urldecode() { local d="${1//+/ }"; printf '%b' "${d//%/\\x}"; }
|
||||||
|
|
||||||
|
# ---- port operations (Linux/macOS: needs lsof) ------------------------------
|
||||||
|
# Kill processes LISTENING on a TCP port. Progress -> stderr; killed count -> stdout.
|
||||||
|
kill_listeners() {
|
||||||
|
local port="$1" pids pid n=0
|
||||||
|
pids="$(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null | sort -u)"
|
||||||
|
for pid in $pids; do
|
||||||
|
dim " Killing PID $pid (port $port)" >&2
|
||||||
|
if kill "$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null; then n=$((n + 1)); fi
|
||||||
|
done
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
59
bash/zkill
Normal file
59
bash/zkill
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zkill — stop local dev-server listeners for a project (bash port of ZKillOnly.ps1).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zkill <project> [<project> ...] [--port N] [--kill-all]
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require lsof
|
||||||
|
|
||||||
|
projects=(); port_override=0; kill_all=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-p|--port) port_override="${2:-0}"; shift 2 ;;
|
||||||
|
--kill-all) kill_all=1; shift ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
warn "Usage: zkill <project> [<project> ...] [--port N] [--kill-all]"
|
||||||
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
label="$(zproj "$key" .label)"; [ -n "$label" ] || label="$key"
|
||||||
|
|
||||||
|
if [ "$port_override" -gt 0 ] 2>/dev/null; then
|
||||||
|
port="$port_override"
|
||||||
|
else
|
||||||
|
port="$(zproj "$key" .ports.dev)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "=== zkill ($label) ==="
|
||||||
|
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -gt 0 ]; then
|
||||||
|
dim "Port: $port"
|
||||||
|
killed="$(kill_listeners "$port")"
|
||||||
|
else
|
||||||
|
warn "No dev port configured for '$key' - skipping port kill."
|
||||||
|
killed=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TODO (parity with ZKillOnly.ps1 -KillAll): also stop stray project processes.
|
||||||
|
if [ "${killed:-0}" -gt 0 ]; then
|
||||||
|
note " $killed process(es) stopped"
|
||||||
|
else
|
||||||
|
ok " Nothing to kill"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '\n'; info "Kill complete."
|
||||||
79
bash/zrepair
Normal file
79
bash/zrepair
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zrepair — audit and repair container/proxy routing on the server, then smoke test.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zrepair <project> [<project> ...]
|
||||||
|
#
|
||||||
|
# For each project: shows compose status, starts the stack if it's down,
|
||||||
|
# validates the edge proxy's nginx config (once, if an edge project is defined),
|
||||||
|
# and smoke-tests the live domain.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require ssh curl
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
printf '\n'; warn "Usage: zrepair <project> [<project> ...]"
|
||||||
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOST="$(zec2_ip)"
|
||||||
|
|
||||||
|
printf '\n'; info "=== Routing repair & diagnostics ==="
|
||||||
|
dim "Target host: $HOST"
|
||||||
|
|
||||||
|
# Validate the edge proxy config once up front, if one is defined.
|
||||||
|
EDGE_KEY="$(zedge_key)"
|
||||||
|
if [ -n "$EDGE_KEY" ]; then
|
||||||
|
pc="$(zproj "$EDGE_KEY" .proxyContainer)"
|
||||||
|
if [ -n "$pc" ]; then
|
||||||
|
printf '\n'; warn " [edge] Validating nginx config in proxy container '$pc'..."
|
||||||
|
zec2_step "nginx -t in $pc" "sudo docker exec $pc nginx -t 2>&1" || exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for key in "$@"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
label="$(zproj "$key" .label)"; [ -n "$label" ] || label="$key"
|
||||||
|
compose_dir="$(zremote_compose_dir "$key")"
|
||||||
|
domain="$(zproj "$key" .domain)"
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
info "========================================="
|
||||||
|
info "=== Repairing $label ($key) ==="
|
||||||
|
info "========================================="
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
warn " [1/3] Compose status on the server..."
|
||||||
|
zec2_step "Check compose status" \
|
||||||
|
"cd $compose_dir || exit 1; echo 'Running containers:'; sudo docker compose ps" || exit 1
|
||||||
|
|
||||||
|
warn " [2/3] Ensuring stack is up..."
|
||||||
|
zec2_step "Ensure stack is up" \
|
||||||
|
"cd $compose_dir || exit 1; if ! sudo docker compose ps | grep -q Up; then echo ' [Action] Stack is down. Starting...'; sudo docker compose up -d; else echo ' [OK] Stack is active and UP.'; fi" || exit 1
|
||||||
|
|
||||||
|
if [ -n "$domain" ]; then
|
||||||
|
warn " [3/3] Smoke test for https://$domain..."
|
||||||
|
code="$(http_code "http://$HOST/" "$domain")"
|
||||||
|
if [ "$code" = "000" ]; then
|
||||||
|
err " FAIL - smoke test failed: no HTTP response"
|
||||||
|
elif [ "$code" -lt 400 ]; then
|
||||||
|
ok " PASS - responded with HTTP $code (Online)"
|
||||||
|
elif [ "$code" -lt 500 ]; then
|
||||||
|
ok " PASS - responded with HTTP $code (Online/Redirect or auth)"
|
||||||
|
else
|
||||||
|
warn " WARNING - unexpected HTTP $code"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn " [3/3] No domain configured - skipping smoke test."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n'; ok "=== Repair & diagnostics completed ==="
|
||||||
|
printf '\n'
|
||||||
53
bash/zrestart
Normal file
53
bash/zrestart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zrestart — kill then restart dev servers for any project in zconfig.json.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zrestart <project> [<project> ...] [--port N] [--kill-all] [--no-restart] [--detached] [--bind-host host]
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$_HERE/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
|
||||||
|
projects=(); port_override=0; kill_all=0; no_restart=0; detached=0; bind_host="127.0.0.1"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-p|--port) port_override="${2:-0}"; shift 2 ;;
|
||||||
|
--kill-all) kill_all=1; shift ;;
|
||||||
|
--no-restart) no_restart=1; shift ;;
|
||||||
|
-d|--detached) detached=1; shift ;;
|
||||||
|
--bind-host) bind_host="${2:-127.0.0.1}"; shift 2 ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
printf '\n'; warn "Usage: zrestart <project> [<project> ...] [--port N] [--kill-all] [--no-restart] [--detached] [--bind-host host]"
|
||||||
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "=== zrestart ==="
|
||||||
|
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
info "--- $key ---"
|
||||||
|
|
||||||
|
kill_args=("$key")
|
||||||
|
[ "$port_override" -gt 0 ] 2>/dev/null && kill_args+=(--port "$port_override")
|
||||||
|
[ "$kill_all" -eq 1 ] && kill_args+=(--kill-all)
|
||||||
|
"$_HERE/zkill" "${kill_args[@]}" || exit $?
|
||||||
|
|
||||||
|
if [ "$no_restart" -eq 0 ]; then
|
||||||
|
sleep 1
|
||||||
|
start_args=("$key")
|
||||||
|
[ "$port_override" -gt 0 ] 2>/dev/null && start_args+=(--port "$port_override")
|
||||||
|
[ "$bind_host" != "127.0.0.1" ] && start_args+=(--bind-host "$bind_host")
|
||||||
|
[ "$detached" -eq 1 ] && start_args+=(--detached)
|
||||||
|
"$_HERE/zstart" "${start_args[@]}" || exit $?
|
||||||
|
fi
|
||||||
|
done
|
||||||
106
bash/zsetup_mail
Normal file
106
bash/zsetup_mail
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zsetup_mail — create admin@ and noreply@ mailboxes in a docker-mailserver
|
||||||
|
# container on the server, and print the DNS records + SMTP/IMAP settings to use.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zsetup_mail --domain yourdomain.com [--mail-host mail.yourdomain.com] [--host <ip>] [--pem <path>]
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require ssh
|
||||||
|
|
||||||
|
domain=""; mail_host=""; ec2_host=""; pem=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--domain) domain="${2:-}"; shift 2 ;;
|
||||||
|
--mail-host) mail_host="${2:-}"; shift 2 ;;
|
||||||
|
--host) ec2_host="${2:-}"; shift 2 ;;
|
||||||
|
--pem) pem="${2:-}"; shift 2 ;;
|
||||||
|
*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$ec2_host" ] || ec2_host="$(zec2_ip)"
|
||||||
|
[ -n "$pem" ] || pem="$(zec2_pem)"
|
||||||
|
|
||||||
|
if [ -z "$domain" ]; then
|
||||||
|
printf '\n'; warn "Usage: zsetup_mail --domain yourdomain.com [--mail-host mail.yourdomain.com]"
|
||||||
|
dim " Creates admin@ and noreply@ mailboxes on the server's mailserver container."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -n "$mail_host" ] || mail_host="mail.$domain"
|
||||||
|
|
||||||
|
info "=== Mail Setup Utility ==="
|
||||||
|
[ -f "$pem" ] || { err "PEM key not found: $pem"; exit 1; }
|
||||||
|
dim "Using PEM Key: $pem"
|
||||||
|
dim "Target Host : $ec2_host"
|
||||||
|
dim "Domain Name : $domain"
|
||||||
|
|
||||||
|
gen_password() {
|
||||||
|
LC_ALL=C tr -dc 'a-zA-Z0-9!@#%^&*' < /dev/urandom | head -c 16
|
||||||
|
}
|
||||||
|
admin_pw="$(gen_password)"
|
||||||
|
noreply_pw="$(gen_password)"
|
||||||
|
|
||||||
|
remote() {
|
||||||
|
ssh -o ConnectTimeout=15 -o StrictHostKeyChecking=no -i "$pem" "$(zec2_user)@$ec2_host" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
printf '\n'; warn "[1/3] Connecting to the mail server to provision mailboxes..."
|
||||||
|
|
||||||
|
dim "Adding email account: admin@$domain..."
|
||||||
|
if remote "sudo docker exec mailserver setup email add admin@$domain '$admin_pw'"; then
|
||||||
|
ok "Account admin@$domain added successfully."
|
||||||
|
else
|
||||||
|
warn "Could not add admin account (it might already exist or docker setup failed)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
dim "Adding email account: noreply@$domain..."
|
||||||
|
if remote "sudo docker exec mailserver setup email add noreply@$domain '$noreply_pw'"; then
|
||||||
|
ok "Account noreply@$domain added successfully."
|
||||||
|
else
|
||||||
|
warn "Could not add noreply account (it might already exist or docker setup failed)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; warn "[2/3] DNS Configuration Requirements"
|
||||||
|
dim "Add or update the following records in your DNS zone for ${domain}:"
|
||||||
|
info "--------------------------------------------------------------------------------"
|
||||||
|
printf '%s\n' "1. MX Record (Inbound mail routing):"
|
||||||
|
ok " - Name : (leave blank or @)"
|
||||||
|
ok " - Type : MX"
|
||||||
|
warn " - Value : 10 $mail_host"
|
||||||
|
printf '%s\n' "2. TXT Record (SPF authorization):"
|
||||||
|
ok " - Name : (leave blank or @)"
|
||||||
|
ok " - Type : TXT"
|
||||||
|
warn " - Value : \"v=spf1 mx ~all\""
|
||||||
|
printf '%s\n' "3. A Record for Webmail:"
|
||||||
|
ok " - Name : webmail"
|
||||||
|
ok " - Type : A"
|
||||||
|
warn " - Value : $ec2_host"
|
||||||
|
printf '%s\n' "4. A Record for Mail Admin:"
|
||||||
|
ok " - Name : mail-admin"
|
||||||
|
ok " - Type : A"
|
||||||
|
warn " - Value : $ec2_host"
|
||||||
|
info "--------------------------------------------------------------------------------"
|
||||||
|
|
||||||
|
printf '\n'; warn "[3/3] Application Connection Credentials"
|
||||||
|
dim "Use these connection settings in your app config or .env:"
|
||||||
|
info "--------------------------------------------------------------------------------"
|
||||||
|
printf '%s\n' "SMTP Hostname : $mail_host"
|
||||||
|
printf '%s\n' "SMTP Port (STARTTLS) : 587"
|
||||||
|
printf '%s\n' "SMTP Port (SSL/TLS) : 465"
|
||||||
|
printf '%s\n' "IMAP Hostname : $mail_host"
|
||||||
|
printf '%s\n' "IMAP Port (SSL/TLS) : 993"
|
||||||
|
info "--------------------------------------------------------------------------------"
|
||||||
|
ok "Email User 1 : admin@$domain"
|
||||||
|
warn "Password : $admin_pw"
|
||||||
|
printf '\n'
|
||||||
|
ok "Email User 2 : noreply@$domain"
|
||||||
|
warn "Password : $noreply_pw"
|
||||||
|
info "--------------------------------------------------------------------------------"
|
||||||
|
ok "Completed successfully!"
|
||||||
172
bash/zstart
Normal file
172
bash/zstart
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zstart — start local dev servers for any project defined in zconfig.json.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zstart <project> [<project> ...] [--port N] [--bind-host host] [--detached]
|
||||||
|
#
|
||||||
|
# Handlers by kind: python (python -m <startModule>, prefers .venv),
|
||||||
|
# vite (npm run dev -- --host --port), nextjs (npm run dev with PORT).
|
||||||
|
# Detached servers log to /tmp/zstart-<project>.log ; stop them with zkill.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
|
||||||
|
projects=(); port_override=0; bind_host="127.0.0.1"; detached=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-p|--port) port_override="${2:-0}"; shift 2 ;;
|
||||||
|
--bind-host) bind_host="${2:-127.0.0.1}"; shift 2 ;;
|
||||||
|
-d|--detached) detached=1; shift ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
printf '\n'; warn "Usage: zstart <project> [<project> ...] [--port N] [--detached] [--bind-host host]"
|
||||||
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn_if_privileged_port() { # <port>
|
||||||
|
if [ "$1" -lt 1024 ] && [ "$(id -u)" -ne 0 ]; then
|
||||||
|
err "WARNING: Port $1 requires root privileges to bind. Run with sudo!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detach() { # <key> <workdir> <cmd...>
|
||||||
|
local key="$1" dir="$2"; shift 2
|
||||||
|
local log="/tmp/zstart-${key}.log"
|
||||||
|
( cd "$dir" && nohup "$@" >"$log" 2>&1 & )
|
||||||
|
ok "Started in detached mode (logs: $log)."
|
||||||
|
dim "Use zkill $key to stop it."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional per-project pre-start steps from zconfig.json:
|
||||||
|
# "start": { "gitPull": true, "env": { "SOME_FLAG": "1" } }
|
||||||
|
start_prep() { # <key>
|
||||||
|
local key="$1" root kv name val
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
while IFS=$'\t' read -r name val; do
|
||||||
|
[ -n "$name" ] || continue
|
||||||
|
export "$name=$val"
|
||||||
|
dim " env $name=$val"
|
||||||
|
done < <(jq -r --arg k "$key" '.projects[$k].start.env // {} | to_entries[] | "\(.key)\t\(.value)"' "$ZCONFIG")
|
||||||
|
if [ "$(zproj "$key" .start.gitPull)" = "true" ] && [ -d "$root/.git" ]; then
|
||||||
|
local last
|
||||||
|
last="$( (cd "$root" && git pull --ff-only 2>&1) | tail -1 )"
|
||||||
|
dim " git pull: $last"
|
||||||
|
(cd "$root" && git rev-parse HEAD >/dev/null 2>&1) || warn " Auto-pull failed - run 'git pull' manually if needed."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_python() { # <key> <port>
|
||||||
|
local key="$1" port="$2" root module exe bv=""
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
module="$(zproj "$key" .startModule)"
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
[ -n "$module" ] || { err "Project '$key' (kind=python) needs 'startModule' in zconfig.json (e.g. \"startModule\": \"pyapp.main\")."; return 1; }
|
||||||
|
|
||||||
|
# Prefer the project venv (Unix layout; Scripts/ fallback covers Windows-made venvs).
|
||||||
|
if [ -x "$root/.venv/bin/python" ]; then exe="$root/.venv/bin/python"
|
||||||
|
elif [ -x "$root/.venv/Scripts/python.exe" ]; then exe="$root/.venv/Scripts/python.exe"
|
||||||
|
elif command -v python3 >/dev/null 2>&1; then exe="python3"
|
||||||
|
else exe="python"; fi
|
||||||
|
|
||||||
|
# Optional convention: scripts/build_version_tool.py bumps the version on dev start.
|
||||||
|
if [ -f "$root/scripts/build_version_tool.py" ]; then
|
||||||
|
bv="$(cd "$root" && "$exe" scripts/build_version_tool.py bump 2>/dev/null | tail -1)"
|
||||||
|
[ -n "$bv" ] || bv="$(cd "$root" && "$exe" scripts/build_version_tool.py get 2>/dev/null | tail -1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "=== zstart ($(zproj "$key" .label)) ==="
|
||||||
|
info "Starting python -m $module on port $port..."
|
||||||
|
[ -n "$bv" ] && note "Build Version: $bv"
|
||||||
|
show_project_motd "$root"
|
||||||
|
dim "Press Ctrl+C to stop the server"
|
||||||
|
warn_if_privileged_port "$port"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "$detached" -eq 1 ]; then
|
||||||
|
detach "$key" "$root" "$exe" -m "$module"
|
||||||
|
else
|
||||||
|
( cd "$root" && exec "$exe" -m "$module" )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_vite() { # <key> <port>
|
||||||
|
local key="$1" port="$2" root
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
[ -f "$root/package.json" ] || { err "package.json not found in $root"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "=== zstart ($(zproj "$key" .label) - Vite) ==="
|
||||||
|
dim "Directory: $root"
|
||||||
|
info "URL: http://${bind_host}:${port}/"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ ! -d "$root/node_modules" ]; then
|
||||||
|
warn "node_modules missing - running npm install..."
|
||||||
|
(cd "$root" && npm install) || { err "npm install failed"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
show_project_motd "$root"
|
||||||
|
dim "Press Ctrl+C to stop the dev server"
|
||||||
|
warn_if_privileged_port "$port"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "$detached" -eq 1 ]; then
|
||||||
|
detach "$key" "$root" npm run dev -- --host "$bind_host" --port "$port"
|
||||||
|
else
|
||||||
|
( cd "$root" && exec npm run dev -- --host "$bind_host" --port "$port" )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_next() { # <key> <port>
|
||||||
|
local key="$1" port="$2" root
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||||
|
[ -f "$root/package.json" ] || { err "package.json not found in $root"; return 1; }
|
||||||
|
|
||||||
|
printf '\n'; info "=== zstart ($(zproj "$key" .label) - Next.js) ==="
|
||||||
|
info "Starting on port $port..."
|
||||||
|
printf '\n'
|
||||||
|
show_project_motd "$root"
|
||||||
|
dim "Press Ctrl+C to stop the server"
|
||||||
|
warn_if_privileged_port "$port"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
export PORT="$port"
|
||||||
|
if [ "$detached" -eq 1 ]; then
|
||||||
|
detach "$key" "$root" npm run dev
|
||||||
|
else
|
||||||
|
( cd "$root" && exec npm run dev )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
kind="$(zproj "$key" .kind)"
|
||||||
|
if [ "$port_override" -gt 0 ] 2>/dev/null; then port="$port_override"
|
||||||
|
else port="$(zproj "$key" .ports.dev)"; [[ "$port" =~ ^[0-9]+$ ]] || port=3000; fi
|
||||||
|
|
||||||
|
start_prep "$key"
|
||||||
|
|
||||||
|
case "$kind" in
|
||||||
|
python) start_python "$key" "$port" || rc=1 ;;
|
||||||
|
vite) start_vite "$key" "$port" || rc=1 ;;
|
||||||
|
nextjs) start_next "$key" "$port" || rc=1 ;;
|
||||||
|
*)
|
||||||
|
printf '\n'
|
||||||
|
warn "Project '$key' has kind '$kind' - no local dev server to start."
|
||||||
|
dim "Supported kinds for zstart: python, vite, nextjs"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exit "$rc"
|
||||||
65
bash/zstart_docker
Normal file
65
bash/zstart_docker
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zstart_docker — bring up a local docker compose stack from <scriptsRoot>/docker/.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zstart_docker [--build] [--attached] [--solo]
|
||||||
|
#
|
||||||
|
# --build rebuild images before starting
|
||||||
|
# --attached stream logs in the foreground (default is detached)
|
||||||
|
# --solo use docker-compose.solo.yml (app only, no proxy)
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$_HERE/zhelpers.sh"
|
||||||
|
z_require docker
|
||||||
|
|
||||||
|
build=0; attached=0; solo=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--build) build=1; shift ;;
|
||||||
|
--attached) attached=1; shift ;;
|
||||||
|
--solo) solo=1; shift ;;
|
||||||
|
*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
info "=== zstart_docker ==="
|
||||||
|
|
||||||
|
compose_file="docker-compose.yml"
|
||||||
|
[ "$solo" -eq 1 ] && compose_file="docker-compose.solo.yml"
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
printf '\n'
|
||||||
|
err "ERROR: Docker Engine is not running or this shell cannot reach it."
|
||||||
|
printf '\n'
|
||||||
|
info "Try:"
|
||||||
|
dim " 1. Start the Docker daemon (systemctl start docker / open Docker Desktop)."
|
||||||
|
dim " 2. Check your user is in the docker group: groups | grep docker"
|
||||||
|
printf '\n'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
compose_path="$_HERE/docker/$compose_file"
|
||||||
|
if [ ! -f "$compose_path" ]; then
|
||||||
|
err "ERROR: Missing $compose_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$_HERE/.env" ] || warn "WARNING: .env not found at $_HERE/.env - compose may still start if env vars are set elsewhere."
|
||||||
|
|
||||||
|
dc_args=(compose -f "$compose_file" up)
|
||||||
|
[ "$attached" -eq 0 ] && dc_args+=(-d)
|
||||||
|
[ "$build" -eq 1 ] && dc_args+=(--build)
|
||||||
|
|
||||||
|
dim "Running: docker ${dc_args[*]}"
|
||||||
|
(cd "$_HERE/docker" && docker "${dc_args[@]}") || exit $?
|
||||||
|
|
||||||
|
printf '\n'; ok "Stack: http://localhost/"
|
||||||
|
if [ "$attached" -eq 0 ]; then
|
||||||
|
dim 'Logs: docker compose -f docker/docker-compose.yml logs -f'
|
||||||
|
dim 'Stop: docker compose -f docker/docker-compose.yml down'
|
||||||
|
fi
|
||||||
37
bash/zstop
Normal file
37
bash/zstop
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zstop — stop docker compose stacks on the server without removing data or files.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zstop <project> [<project> ...]
|
||||||
|
#
|
||||||
|
# Data volumes are preserved. Run zdeploy <project> to bring a stack back up.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
z_require ssh
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
printf '\n'; warn "Usage: zstop <project> [<project> ...]"
|
||||||
|
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for key in "$@"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
label="$(zproj "$key" .label)"; [ -n "$label" ] || label="$key"
|
||||||
|
compose_dir="$(zremote_compose_dir "$key")"
|
||||||
|
|
||||||
|
printf '\n'; info "--- Stopping $label ---"
|
||||||
|
if zssh "cd $compose_dir && sudo docker compose down 2>&1 || true"; then
|
||||||
|
ok " $label stopped."
|
||||||
|
else
|
||||||
|
warn " WARNING: docker compose down returned non-zero for $label"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n'; dim "Done. Data volumes are intact. Run zdeploy <project> to restart."
|
||||||
96
bash/zsync
Normal file
96
bash/zsync
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# zsync — copy new backup files offsite; or build + mirror a vite project's dist.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zsync # new files: backups folder -> paths.oneDriveBackups
|
||||||
|
# zsync <viteproject> --dest /mnt/mirror/dist
|
||||||
|
#
|
||||||
|
# The no-args mode copies only files that don't exist at the destination yet
|
||||||
|
# (never overwrites, never deletes). The project mode runs npm run build, then
|
||||||
|
# mirrors dist/ to --dest (or $ZSYNC_DEST) with rsync --delete.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||||
|
z_need_config
|
||||||
|
|
||||||
|
projects=(); dest="${ZSYNC_DEST:-}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dest) dest="${2:-}"; shift 2 ;;
|
||||||
|
-*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
*) projects+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- no-args mode: backups -> offsite, new files only ------------------------
|
||||||
|
if [ "${#projects[@]}" -eq 0 ]; then
|
||||||
|
BACKUPS_ROOT="$(dirname "$(zq '.paths.backupsLocal')")"
|
||||||
|
OFFSITE_ROOT="$(zq '.paths.oneDriveBackups')"
|
||||||
|
|
||||||
|
printf '\n'; info "=== zsync (backups -> offsite) ==="
|
||||||
|
dim " Source: $BACKUPS_ROOT"
|
||||||
|
dim " Destination: $OFFSITE_ROOT"
|
||||||
|
dim " Mode: new files and folders only"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ -z "$OFFSITE_ROOT" ]; then
|
||||||
|
warn " paths.oneDriveBackups is not set in zconfig.json - nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ ! -d "$BACKUPS_ROOT" ]; then
|
||||||
|
warn " Source not found (skipped): $BACKUPS_ROOT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$OFFSITE_ROOT"
|
||||||
|
|
||||||
|
copied=0; skipped=0
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
rel="${f#"$BACKUPS_ROOT"/}"
|
||||||
|
target="$OFFSITE_ROOT/$rel"
|
||||||
|
if [ -e "$target" ]; then skipped=$((skipped + 1)); continue; fi
|
||||||
|
mkdir -p "$(dirname "$target")"
|
||||||
|
cp -p "$f" "$target"
|
||||||
|
ok " Copied: $rel ($(z_size_mb "$f") MB)"
|
||||||
|
copied=$((copied + 1))
|
||||||
|
done < <(find "$BACKUPS_ROOT" -type f -print0 2>/dev/null)
|
||||||
|
|
||||||
|
printf '\n'; info " Done: $copied copied, $skipped already present"
|
||||||
|
printf '\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- project mode: build + mirror a vite project's dist ----------------------
|
||||||
|
z_require rsync
|
||||||
|
for key in "${projects[@]}"; do
|
||||||
|
zproj_require "$key"
|
||||||
|
kind="$(zproj "$key" .kind)"
|
||||||
|
if [ "$kind" != "vite" ]; then
|
||||||
|
printf '\n'; warn "zsync project mode only supports vite kind (builds and mirrors dist/). '$key' is kind '$kind'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
root="$(zproj "$key" .localRoot)"
|
||||||
|
|
||||||
|
if [ -z "$dest" ]; then
|
||||||
|
printf '\n'; info "=== zsync ($(zproj "$key" .label)) ==="
|
||||||
|
warn "No destination set."
|
||||||
|
dim "Set ZSYNC_DEST or run: zsync $key --dest /path/to/folder"
|
||||||
|
dim "This runs npm run build, then mirrors dist -> destination (rsync --delete)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'; info "=== zsync ($(zproj "$key" .label)) ==="
|
||||||
|
info "Build + mirror dist -> $dest"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
(cd "$root" && npm run build) || { err "npm run build failed"; exit 1; }
|
||||||
|
[ -d "$root/dist" ] || { err "dist/ missing after build."; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$dest"
|
||||||
|
rsync -a --delete "$root/dist/" "$dest/" || { err "rsync failed"; exit 1; }
|
||||||
|
|
||||||
|
printf '\n'; ok "Sync complete."
|
||||||
|
done
|
||||||
Loading…
Reference in New Issue
Block a user