mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-08 07:28:18 +00:00
chore: hold the bash port back from the public repo until it is tested
The bash port and its bats suite are removed from the tree while they get more
testing. Everything they were referenced from is cleaned up so nothing dangles:
- README drops the 'Linux / macOS / WSL' pointer to bash/README.md (a dead
link once the folder is gone) and the aside that ZCONFIG is honoured by
both ports.
- CHANGELOG drops the two bash mentions.
- ZHelpers.ps1's ZCONFIG comment no longer cites zhelpers.sh.
- .gitattributes drops the now-dead bash/**, tests/bash/** and *.bats rules,
keeping the PowerShell CRLF rules.
No behaviour change to the PowerShell scripts; the Pester suite is untouched
and still passes 169/169.
Deliberately NOT a history rewrite: the port stays in this repo's history and
in full in the private mirror, so it can be restored with a revert when the
testing is done. Nothing here is secret - it is unfinished, not sensitive.
This commit is contained in:
parent
65c5886459
commit
ddece471c9
9
.gitattributes
vendored
9
.gitattributes
vendored
@ -1,12 +1,3 @@
|
||||
# Bash scripts must stay LF — CRLF breaks the shebang on Linux/macOS/WSL.
|
||||
bash/** text eol=lf
|
||||
|
||||
# The bats suites are bash too, and they live outside bash/ — without this a
|
||||
# Windows checkout gets CRLF and `bats tests/bash` fails there (including when
|
||||
# run through WSL against the Windows working copy).
|
||||
tests/bash/** text eol=lf
|
||||
*.bats text eol=lf
|
||||
|
||||
# PowerShell/cmd are happiest with CRLF on Windows.
|
||||
*.ps1 text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
@ -28,9 +28,9 @@ Notable changes to the Evomedia.net Token Savers.
|
||||
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
||||
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
||||
testing — reintroducing each historical bug turns the suite red.
|
||||
- **`ZCONFIG` environment variable (PowerShell)** — overrides the path to
|
||||
`zconfig.json`, matching the bash port, which has always honored it. Closes a
|
||||
parity gap and gives the test suite a seam for injecting a fixture config.
|
||||
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
||||
a run can target an alternate config. Also gives the test suite a seam for
|
||||
injecting a fixture.
|
||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
||||
@ -46,8 +46,7 @@ Notable changes to the Evomedia.net Token Savers.
|
||||
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
||||
every project that has a `ports.dev` (edge/docker stacks with no local dev
|
||||
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
||||
the one-shot "stop everything I've got running locally". Ported to both the
|
||||
PowerShell and bash versions.
|
||||
the one-shot "stop everything I've got running locally".
|
||||
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
||||
not published through the edge proxy can declare
|
||||
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
||||
|
||||
@ -14,8 +14,6 @@ Every command is a tiny PowerShell script driven by a single JSON config file. T
|
||||
|
||||
**Requirements:** Windows, PowerShell 5.1+, OpenSSH client (`ssh`/`scp`, ships with Windows 10/11), and Docker + docker compose on the remote host for the deploy scripts.
|
||||
|
||||
**Linux / macOS / WSL:** a native bash port of every command lives in [`bash/`](bash/README.md) — same config schema, same commands, no PowerShell needed.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
@ -49,7 +47,7 @@ notepad zconfig.json
|
||||
|
||||
The example config ships with sample projects named by their kind — `pyapp`, `viteapp`, `nextapp`, `edge`, `analytics`. **Rename the keys to your own project names**; the key is what you type as the command argument. Add as many projects as you like — no script edits ever needed.
|
||||
|
||||
Set the `ZCONFIG` environment variable to point at a config somewhere else (both the PowerShell scripts and the bash port honor it) — handy for a second machine profile, or for running against a scratch config without touching your real one.
|
||||
Set the `ZCONFIG` environment variable to point at a config somewhere else — handy for a second machine profile, or for running against a scratch config without touching your real one.
|
||||
|
||||
### Config reference
|
||||
|
||||
|
||||
@ -34,9 +34,8 @@ $script:JunkDirNames = @(
|
||||
$script:ZConfigCache = $null
|
||||
|
||||
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
||||
# ZCONFIG environment variable, matching the bash port (zhelpers.sh does the
|
||||
# same). Useful for pointing a run at an alternate config, and it is the seam
|
||||
# the test suite uses to inject a fixture.
|
||||
# ZCONFIG environment variable. Useful for pointing a run at an alternate
|
||||
# config, and it is the seam the test suite uses to inject a fixture.
|
||||
function Get-ZConfigPath {
|
||||
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
||||
return (Join-Path $PSScriptRoot "zconfig.json")
|
||||
|
||||
107
bash/README.md
107
bash/README.md
@ -1,107 +0,0 @@
|
||||
<!--
|
||||
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 3.2+ (the stock macOS bash works), `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` (bash, curl, zip, ssh, lsof and rsync already ship with macOS)
|
||||
|
||||
## 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 |
|
||||
|---|---|
|
||||
| `zsetup <p>` | create the project's python venv + install deps (or `npm install`) |
|
||||
| `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`.
|
||||
|
||||
## Usage tracking
|
||||
|
||||
Every run prints a footer with its own output volume:
|
||||
|
||||
```
|
||||
--- 1,048 lines / 69,009 chars / ~19,717 tokens est. (Claude Code) ---
|
||||
```
|
||||
|
||||
and (when `jq` is available) appends one JSON line per run to `tokens.jsonl`,
|
||||
so you can see how much infrastructure output you keep out of an AI agent's
|
||||
context over time. Fields: `ts, script, projects, lines, chars, est, model`
|
||||
(`est` ≈ `chars / 3.5`). Nested runs (the `zkill`/`zstart` inside `zrestart`)
|
||||
are counted once, not double.
|
||||
|
||||
Where it's written, first match wins:
|
||||
|
||||
1. `$ZTOKENS_DATA`
|
||||
2. `ztokens.dataDir` in `zconfig.json`
|
||||
3. the sibling `../ztokens/data` directory, if present
|
||||
4. `~/.ztokens/data` (created on first run)
|
||||
|
||||
Set `ZTOKENS_MODEL` to tag records with a specific model; it defaults to
|
||||
`est. chars/3.5`.
|
||||
|
||||
## Tests
|
||||
|
||||
The bash port has its own [bats](https://github.com/bats-core/bats-core) suite, mirroring the PowerShell Pester tests assertion-for-assertion where the two are meant to agree — the exclude lists, config accessors, `z_path` translation, and argument handling:
|
||||
|
||||
```bash
|
||||
bats tests/bash
|
||||
```
|
||||
|
||||
Install bats without root:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
|
||||
/tmp/bats-core/install.sh ~/.local # then ensure ~/.local/bin is on PATH
|
||||
```
|
||||
|
||||
A fixture config is injected through `ZCONFIG`, so the suite never reads your real `zconfig.json` and passes on a machine that has never been configured. Tests that run the scripts stay on paths that exit before doing any work; only `zkill` runs with a real target, against deliberately unused ports.
|
||||
|
||||
Why a separate suite: the bash port reimplements the exclude lists and argument parsing, so it can drift from the PowerShell side independently. Both suites assert the same deploy-vs-backup rule — **deploys drop `.env*` and `uploads/`, backups keep them** — because that one has broken in production.
|
||||
|
||||
## 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.
|
||||
- **A leading dash on a project key is only tolerated by `zdeploy`.** PowerShell accepts `-myapp` anywhere; in bash every other script treats `-myapp` as an unknown option and exits 1. Tracked as a parity gap — use bare keys in the bash port.
|
||||
@ -1,51 +0,0 @@
|
||||
#!/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"
|
||||
z_track_start "$@"
|
||||
|
||||
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 all >> \$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
|
||||
151
bash/zbackup
151
bash/zbackup
@ -1,151 +0,0 @@
|
||||
#!/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 <project> [<project> ...]
|
||||
# zbackup all # every project in zconfig.json + this scripts folder
|
||||
# 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_track_start "$@"
|
||||
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="$(z_path "$(zq '.paths.backupsLocal')")"
|
||||
|
||||
# Require an explicit target: bare invocation shows usage instead of quietly
|
||||
# backing up everything - 'all' is explicit, matching zdeploy.
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup <project> [<project> ...] | all | scripts [--tag \"label\"]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' backs up every project plus this scripts folder; 'scripts' just this folder."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
include_scripts=0
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(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. Prints its own status line for
|
||||
# every outcome; returns 0 (dumped, $2 written) or 1 (nothing dumped).
|
||||
# A not-running database (connection refused) is a calm, expected skip; a real
|
||||
# failure (version mismatch, auth, missing db) prints the loud pg_dump error.
|
||||
local_pg_dump() { # <root> <out_path>
|
||||
local root="$1" out="$2" env_file db_url err_out rc
|
||||
env_file="$root/.env"; [ -f "$env_file" ] || env_file="$root/backend/.env"
|
||||
if [ ! -f "$env_file" ]; then dim " No local DATABASE_URL - source-only backup."; return 1; fi
|
||||
db_url="$(grep -m1 '^DATABASE_URL=' "$env_file" | cut -d= -f2- | tr -d '[:space:]')"
|
||||
if [ -z "$db_url" ]; then dim " No local DATABASE_URL - source-only backup."; return 1; fi
|
||||
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; }
|
||||
err_out="$(PGPASSWORD="$(z_urldecode "$pass")" pg_dump -h "$host" -p "$port" -U "$user" -d "$db" -F p -f "$out" 2>&1)"; rc=$?
|
||||
if [ "$rc" -eq 0 ] && [ -f "$out" ]; then
|
||||
ok " PostgreSQL dump: $(awk -v b="$(wc -c < "$out")" 'BEGIN{printf "%.1f", b/1024}') KB"
|
||||
return 0
|
||||
fi
|
||||
# Not reachable (usually just not running locally) is expected - stay calm.
|
||||
if printf '%s' "$err_out" | grep -qiE 'connection refused|could not connect|no route to host|could not translate host|timeout expired'; then
|
||||
dim " Local database not running at $host:$port - source-only backup."
|
||||
return 1
|
||||
fi
|
||||
# A real failure (version mismatch, auth, missing db) - show the reason.
|
||||
err " pg_dump failed ($host:$port/$db as $user):"
|
||||
printf '%s\n' "$err_out" | grep -v '^[[:space:]]*$' | sed 's/^/ /' >&2
|
||||
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_root "$key")"
|
||||
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"; fi
|
||||
|
||||
warn " [2/3] Archiving source..."
|
||||
local excl=()
|
||||
excl=(); while IFS= read -r _zl || [ -n "$_zl" ]; do excl+=("$_zl"); done < <(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="$(z_path "$(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"
|
||||
@ -1,56 +0,0 @@
|
||||
#!/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 <project> [<project> ...]
|
||||
# zbackup_and_sync all # backup everything + sync
|
||||
#
|
||||
# Cron example (see setup_backup_schedule):
|
||||
# 0 2 * * * /path/to/zscripts/bash/zbackup_and_sync all >> ~/zbackup.log 2>&1
|
||||
|
||||
set -uo pipefail
|
||||
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$_HERE/zhelpers.sh"
|
||||
z_track_start "$@"
|
||||
z_need_config
|
||||
|
||||
# Require an explicit target (matching zbackup/zdeploy); 'all' backs up everything.
|
||||
if [ $# -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup_and_sync <project> [<project> ...] | all"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' backs up every project plus the scripts folder, then syncs offsite."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
108
bash/zbackup_ec2
108
bash/zbackup_ec2
@ -1,108 +0,0 @@
|
||||
#!/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 <project> [<project> ...]
|
||||
# zbackup_ec2 all # every project with a remote.path
|
||||
#
|
||||
# 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_track_start "$@"
|
||||
z_need_config
|
||||
z_require ssh scp
|
||||
|
||||
projects=("$@")
|
||||
# Require an explicit target; 'all' pulls every project with a remote.path.
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zbackup_ec2 <project> [<project> ...] | all"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' pulls a server backup of every project with a remote.path."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done \
|
||||
< <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.remote.path != null) | .key' "$ZCONFIG")
|
||||
fi
|
||||
|
||||
EC2_BACKUP_ROOT="$(z_path "$(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"
|
||||
@ -1,87 +0,0 @@
|
||||
{
|
||||
"_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": ""
|
||||
},
|
||||
|
||||
"ztokens": {
|
||||
"_comment": "Optional. Where each run's token-usage stats (tokens.jsonl) are recorded. Omit to use ~/.ztokens/data.",
|
||||
"dataDir": "/home/youruser/.ztokens/data"
|
||||
},
|
||||
|
||||
"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",
|
||||
"_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port <dev> --reload).",
|
||||
"install": "-e .",
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
497
bash/zdeploy
497
bash/zdeploy
@ -1,497 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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="$(z_path "$(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
|
||||
edge_keys=(); while IFS= read -r _zl || [ -n "$_zl" ]; do edge_keys+=("$_zl"); done \
|
||||
< <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.kind == "edge") | .key' "$ZCONFIG")
|
||||
rest_keys=(); while IFS= read -r _zl || [ -n "$_zl" ]; do rest_keys+=("$_zl"); done \
|
||||
< <(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_root "$key")"
|
||||
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_root "$key")"
|
||||
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
|
||||
excl=(); while IFS= read -r _zl || [ -n "$_zl" ]; do excl+=("$_zl"); done < <(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_root "$key")"
|
||||
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
|
||||
excl=(); while IFS= read -r _zl || [ -n "$_zl" ]; do excl+=("$_zl"); done < <(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_root "$key")"
|
||||
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 ---"
|
||||
excl=(); while IFS= read -r _zl || [ -n "$_zl" ]; do excl+=("$_zl"); done < <(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_root "$key")"
|
||||
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_root "$key")"
|
||||
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
|
||||
100
bash/zec2
100
bash/zec2
@ -1,100 +0,0 @@
|
||||
#!/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 <project> [<project> ...]
|
||||
# zec2 all # every project with a "domain" in zconfig.json
|
||||
# zec2 viteapp --host 203.0.113.10
|
||||
|
||||
set -uo pipefail
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||
z_track_start "$@"
|
||||
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
|
||||
printf '\n'; warn "Usage: zec2 <project> [<project> ...] | all [--host <ip>]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' checks every project with a 'domain' configured."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(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"
|
||||
155
bash/zec2online
155
bash/zec2online
@ -1,155 +0,0 @@
|
||||
#!/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 <project> [<project> ...]
|
||||
# zec2online all # every project with a "domain" in zconfig.json
|
||||
#
|
||||
# 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_track_start "$@"
|
||||
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
|
||||
printf '\n'; warn "Usage: zec2online <project> [<project> ...] | all [--host <ip>]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' deep-checks every project with a 'domain' (and auto-starts any that are down)."
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
projects=(); while IFS= read -r _zl || [ -n "$_zl" ]; do projects+=("$_zl"); done < <(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"
|
||||
359
bash/zhelpers.sh
359
bash/zhelpers.sh
@ -1,359 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
# ---- local path translation -------------------------------------------------
|
||||
# A shared zconfig.json may hold Windows paths (e.g. "F:\evomedia.net\app") when
|
||||
# the same file is used from a Windows checkout and from WSL. Translate a
|
||||
# drive-letter path to the host's native form so local file ops work; leave
|
||||
# anything else (Unix paths, empty strings) untouched — so this is a no-op on
|
||||
# native Linux/macOS. Only LOCAL paths get run through this; server-side paths
|
||||
# (remote.path, composeDir, stackRoot, certsSource) stay verbatim.
|
||||
z_path() {
|
||||
local p="$1"
|
||||
case "$p" in
|
||||
[A-Za-z]:[\\/]*)
|
||||
if command -v wslpath >/dev/null 2>&1; then
|
||||
wslpath -u "$p" 2>/dev/null || printf '%s' "$p"
|
||||
else
|
||||
# Fallback when wslpath is absent: F:\a\b -> /mnt/f/a/b. Lowercase the
|
||||
# drive via tr (not ${x,,}) so this stays bash-3.2 clean for macOS.
|
||||
local drive rest
|
||||
drive="$(printf '%s' "${p%%:*}" | tr '[:upper:]' '[:lower:]')"
|
||||
rest="${p#*:}"; rest="${rest//\\//}"
|
||||
printf '/mnt/%s%s' "$drive" "$rest"
|
||||
fi
|
||||
;;
|
||||
*) printf '%s' "$p" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- 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"; }
|
||||
# A project's local root, translated to the host's native path form.
|
||||
zproj_root() { z_path "$(zproj "$1" .localRoot)"; }
|
||||
zproj_require() {
|
||||
# Keys starting with "_" are comments, not projects (zproj_keys already skips
|
||||
# them). Reject them here too, or `zkill _comment` would sail past this guard
|
||||
# on the mere existence of the key and then act on a string as if it were a
|
||||
# project - PowerShell's Get-ZProject has always refused them.
|
||||
case "$1" in
|
||||
_*) err "Unknown project key '$1'. Available: $(zproj_csv)"; exit 1 ;;
|
||||
esac
|
||||
if [ "$(jq -r --arg k "$1" '(.projects[$k] | type) == "object"' "$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() { z_path "$(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_root "$key")"
|
||||
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
|
||||
# deploys exclude user uploads + local .env secrets; backups keep both
|
||||
[ "$for_backup" -eq 1 ] || printf '%s\n' uploads .env .env.local .env.production ;;
|
||||
vite)
|
||||
printf '%s\n' node_modules dist
|
||||
[ "$for_backup" -eq 1 ] || printf '%s\n' .env .env.local .env.production ;;
|
||||
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"
|
||||
}
|
||||
|
||||
# ---- output tracking (ztokens) ----------------------------------------------
|
||||
# Mirrors the PowerShell Start-/Stop-ZTracking + tokens.jsonl feed: capture this
|
||||
# script's own output volume, print the "--- N lines / N chars / ~N tokens ---"
|
||||
# footer, and append one JSONL record per top-level run. Nested z-scripts (e.g.
|
||||
# zkill/zstart under zrestart) inherit the capture and do NOT double-count.
|
||||
# Call `z_track_start "$@"` once near the top; an EXIT trap covers every exit.
|
||||
z_track_start() {
|
||||
[ "${Z_TRACK_ACTIVE:-0}" = "1" ] && return # nested under another z-script
|
||||
command -v mktemp >/dev/null 2>&1 || return
|
||||
export Z_TRACK_ACTIVE=1
|
||||
Z_TRACK_SCRIPT="$(basename "$0")"
|
||||
Z_TRACK_ARGS="$*"
|
||||
Z_TRACK_FILE="$(mktemp "${TMPDIR:-/tmp}/ztrack.XXXXXX")" || { Z_TRACK_FILE=""; return; }
|
||||
Z_TRACK_FIFO="${Z_TRACK_FILE}.fifo"
|
||||
if ! mkfifo "$Z_TRACK_FIFO" 2>/dev/null; then rm -f "$Z_TRACK_FILE"; Z_TRACK_FILE=""; return; fi
|
||||
# tee copies the FIFO to the log file while still showing output on the terminal.
|
||||
tee "$Z_TRACK_FILE" < "$Z_TRACK_FIFO" &
|
||||
Z_TRACK_TEE=$!
|
||||
exec 3>&1 4>&2 >"$Z_TRACK_FIFO" 2>&1
|
||||
trap z_track_stop EXIT
|
||||
}
|
||||
|
||||
z_track_stop() {
|
||||
set +e # never let cleanup abort mid-way
|
||||
[ -n "${Z_TRACK_FILE:-}" ] || return
|
||||
exec 1>&3 2>&4 3>&- 4>&- # restore stdout/stderr, close the pipe
|
||||
[ -n "${Z_TRACK_TEE:-}" ] && wait "$Z_TRACK_TEE" 2>/dev/null
|
||||
rm -f "${Z_TRACK_FIFO:-}" 2>/dev/null
|
||||
|
||||
local esc plain lines chars tok
|
||||
esc=$'\033'
|
||||
plain="${Z_TRACK_FILE}.plain"
|
||||
sed "s/${esc}\[[0-9;]*m//g" "$Z_TRACK_FILE" > "$plain" 2>/dev/null || cp "$Z_TRACK_FILE" "$plain" 2>/dev/null
|
||||
lines="$(grep -cve '^[[:space:]]*$' "$plain" 2>/dev/null)"; [ -n "$lines" ] || lines=0
|
||||
chars="$(wc -m < "$plain" 2>/dev/null | tr -d ' ')"; [ -n "$chars" ] || chars=0
|
||||
tok="$(awk -v c="$chars" 'BEGIN{printf "%d", int(c/3.5 + 0.5)}')"
|
||||
|
||||
printf '\n%s--- %s lines / %s chars / ~%s tokens est. (Claude Code) ---%s\n' \
|
||||
"${C_GRAY:-}" "$(_z_commafy "$lines")" "$(_z_commafy "$chars")" "$(_z_commafy "$tok")" "${C_RESET:-}"
|
||||
|
||||
z_record "$lines" "$chars" "$tok"
|
||||
rm -f "$Z_TRACK_FILE" "$plain" 2>/dev/null
|
||||
Z_TRACK_FILE=""
|
||||
}
|
||||
|
||||
# Portable thousands separators for a non-negative integer string (avoids both
|
||||
# locale-dependent printf %'d and the GNU-only `sed :a;...;ta` idiom, which
|
||||
# errors on BSD/macOS sed).
|
||||
_z_commafy() {
|
||||
awk -v n="$1" 'BEGIN{
|
||||
s = n ""; out = ""
|
||||
while (length(s) > 3) { out = "," substr(s, length(s) - 2) out; s = substr(s, 1, length(s) - 3) }
|
||||
printf "%s%s", s, out
|
||||
}'
|
||||
}
|
||||
|
||||
# Append a run to the ztokens JSONL store. Data dir precedence: $ZTOKENS_DATA,
|
||||
# then config "ztokens.dataDir", then the sibling ../../ztokens/data if present
|
||||
# (dev layout), else ~/.ztokens/data. No-op without jq. Model tag is
|
||||
# $ZTOKENS_MODEL or "est. chars/3.5".
|
||||
z_record() { # <lines> <chars> <est>
|
||||
command -v jq >/dev/null 2>&1 || return
|
||||
local lines="$1" chars="$2" est="$3" dir model projects
|
||||
dir="${ZTOKENS_DATA:-}"
|
||||
[ -z "$dir" ] && [ -f "$ZCONFIG" ] && dir="$(z_path "$(jq -r '.ztokens.dataDir // empty' "$ZCONFIG" 2>/dev/null)")"
|
||||
if [ -z "$dir" ]; then
|
||||
if [ -d "$_ZDIR/../../ztokens/data" ]; then dir="$_ZDIR/../../ztokens/data"; else dir="$HOME/.ztokens/data"; fi
|
||||
fi
|
||||
mkdir -p "$dir" 2>/dev/null || return
|
||||
model="${ZTOKENS_MODEL:-est. chars/3.5}"
|
||||
projects=""
|
||||
[ -n "${Z_TRACK_ARGS:-}" ] && projects="$(printf '%s\n' $Z_TRACK_ARGS | grep -v '^-' | paste -sd',' - 2>/dev/null)"
|
||||
jq -cn \
|
||||
--arg ts "$(date +%Y-%m-%dT%H:%M:%S%z)" \
|
||||
--arg script "${Z_TRACK_SCRIPT%.*}" \
|
||||
--arg projects "$projects" \
|
||||
--argjson lines "${lines:-0}" --argjson chars "${chars:-0}" --argjson est "${est:-0}" \
|
||||
--arg model "$model" \
|
||||
'{ts:$ts,script:$script,projects:$projects,lines:$lines,chars:$chars,est:$est,model:$model}' \
|
||||
>> "$dir/tokens.jsonl" 2>/dev/null
|
||||
}
|
||||
77
bash/zkill
77
bash/zkill
@ -1,77 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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> ...] | all [--port N] [--kill-all]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " 'all' stops the dev server of every project that has one."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 'all' -> every project that actually has a local dev server (a ports.dev).
|
||||
if printf '%s\n' "${projects[@]}" | grep -qx 'all'; then
|
||||
dev_keys=(); while IFS= read -r _zl || [ -n "$_zl" ]; do dev_keys+=("$_zl"); done \
|
||||
< <(jq -r '.projects | to_entries[] | select((.key | startswith("_") | not) and .value.ports.dev != null) | .key' "$ZCONFIG")
|
||||
if [ "${#dev_keys[@]}" -eq 0 ]; then
|
||||
warn "No projects have a dev port to kill."
|
||||
exit 0
|
||||
fi
|
||||
projects=("${dev_keys[@]}")
|
||||
info "Killing dev servers for: $(printf '%s ' "${projects[@]}")"
|
||||
fi
|
||||
|
||||
# --kill-all (kill stray project processes beyond the dev-port listener) isn't
|
||||
# ported yet; announce it rather than silently accepting the flag and doing nothing.
|
||||
[ "$kill_all" -eq 1 ] && warn "Note: --kill-all isn't implemented in the bash port yet - killing dev-port listeners only."
|
||||
|
||||
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."
|
||||
80
bash/zrepair
80
bash/zrepair
@ -1,80 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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 $domain (via http://$HOST)..."
|
||||
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'
|
||||
@ -1,54 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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
|
||||
95
bash/zsetup
95
bash/zsetup
@ -1,95 +0,0 @@
|
||||
#!/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 — prepare a project for local dev: create its Python venv and install
|
||||
# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent -
|
||||
# safe to re-run to refresh deps. `zstart` only warns when a venv is missing;
|
||||
# this is the command that creates it.
|
||||
#
|
||||
# Usage:
|
||||
# zsetup <project> [<project> ...]
|
||||
# zsetup <project> --force # recreate the python venv from scratch
|
||||
#
|
||||
# Python install command (first match wins):
|
||||
# 1. the project's "install" config field (e.g. "-e backend", "-r reqs.txt")
|
||||
# 2. auto: pyproject.toml / setup.py -> "-e ." | requirements.txt -> "-r requirements.txt"
|
||||
|
||||
set -uo pipefail
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
|
||||
z_track_start "$@"
|
||||
z_need_config
|
||||
|
||||
projects=(); force=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) force=1; shift ;;
|
||||
-*) err "Unknown option: $1"; exit 1 ;;
|
||||
*) projects+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${#projects[@]}" -eq 0 ]; then
|
||||
printf '\n'; warn "Usage: zsetup <project> [<project> ...] [--force]"
|
||||
dim " Projects in zconfig.json: $(zproj_csv)"
|
||||
dim " Creates the python venv + installs deps (or runs npm install for vite/nextjs)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# pip install args for a python project (one per line): config .install, else
|
||||
# auto-detect a root pyproject.toml/setup.py or requirements.txt. Empty = unknown.
|
||||
py_install_args() { # <key> <root>
|
||||
local key="$1" root="$2" cfg
|
||||
cfg="$(zproj "$key" .install)"
|
||||
if [ -n "$cfg" ]; then printf '%s\n' $cfg; return 0; fi
|
||||
if [ -f "$root/pyproject.toml" ] || [ -f "$root/setup.py" ]; then printf '%s\n' -e .; return 0; fi
|
||||
if [ -f "$root/requirements.txt" ]; then printf '%s\n' -r requirements.txt; return 0; fi
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_python() { # <key> <root>
|
||||
local key="$1" root="$2" venv="$2/.venv" args=() a
|
||||
z_require python3
|
||||
while IFS= read -r a; do [ -n "$a" ] && args+=("$a"); done < <(py_install_args "$key" "$root")
|
||||
if [ "${#args[@]}" -eq 0 ]; then
|
||||
err " Can't determine dependencies for '$key' (no pyproject.toml/setup.py/requirements.txt at $root)."
|
||||
dim " Add an \"install\" field to the project, e.g. \"install\": \"-e backend\" or \"-r requirements.txt\"."
|
||||
return 1
|
||||
fi
|
||||
if [ "$force" -eq 1 ] && [ -d "$venv" ]; then dim " Removing existing venv (--force)..."; rm -rf "$venv"; fi
|
||||
if [ ! -x "$venv/bin/python" ]; then
|
||||
info " Creating venv: $venv"
|
||||
python3 -m venv "$venv" || { err " venv creation failed"; return 1; }
|
||||
else
|
||||
dim " venv exists: $venv (installing/updating deps)"
|
||||
fi
|
||||
info " pip install ${args[*]}"
|
||||
( cd "$root" && "$venv/bin/pip" install "${args[@]}" ) || { err " pip install failed"; return 1; }
|
||||
ok " $key ready - zstart $key will use $venv/bin/python."
|
||||
}
|
||||
|
||||
setup_node() { # <key> <root>
|
||||
local key="$1" root="$2"
|
||||
[ -f "$root/package.json" ] || { err " No package.json in $root"; return 1; }
|
||||
z_require npm
|
||||
info " npm install (in $root)..."
|
||||
( cd "$root" && npm install ) || { err " npm install failed"; return 1; }
|
||||
ok " $key node_modules ready."
|
||||
}
|
||||
|
||||
rc=0
|
||||
for key in "${projects[@]}"; do
|
||||
zproj_require "$key"
|
||||
kind="$(zproj "$key" .kind)"
|
||||
root="$(zproj_root "$key")"
|
||||
printf '\n'; info "=== zsetup ($(zproj "$key" .label)) ==="
|
||||
[ -d "$root" ] || { err " Project root not found: $root"; rc=1; continue; }
|
||||
case "$kind" in
|
||||
python) setup_python "$key" "$root" || rc=1 ;;
|
||||
vite|nextjs) setup_node "$key" "$root" || rc=1 ;;
|
||||
*) dim " kind '$kind' - nothing to set up locally." ;;
|
||||
esac
|
||||
done
|
||||
printf '\n'
|
||||
exit "$rc"
|
||||
107
bash/zsetup_mail
107
bash/zsetup_mail
@ -1,107 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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!"
|
||||
203
bash/zstart
203
bash/zstart
@ -1,203 +0,0 @@
|
||||
#!/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>, or uvicorn <startApp> for
|
||||
# ASGI/FastAPI apps; 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_track_start "$@"
|
||||
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"
|
||||
# Sever every fd tied to this shell before exec'ing the long-lived server:
|
||||
# stdin<-/dev/null, stdout/stderr->log, and CLOSE the ztokens tracking fds
|
||||
# (3/4 = saved stdout/stderr; 1/2 fed the capture FIFO). If the server keeps
|
||||
# any open, the parent's EXIT-trap footer (a `tee` on the FIFO) never gets
|
||||
# EOF and `zstart --detached` hangs instead of returning.
|
||||
( cd "$dir" && exec </dev/null >"$log" 2>&1 3>&- 4>&-; exec nohup "$@" ) &
|
||||
disown 2>/dev/null || true
|
||||
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_root "$key")"
|
||||
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
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast instead
|
||||
# of blocking the server start on an interactive "Username for ..." prompt.
|
||||
local out rc
|
||||
out="$( (cd "$root" && GIT_TERMINAL_PROMPT=0 git pull --ff-only 2>&1) )"; rc=$?
|
||||
dim " git pull: $(printf '%s\n' "$out" | tail -1)"
|
||||
[ "$rc" -eq 0 ] || warn " Auto-pull skipped (exit $rc) - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)"
|
||||
fi
|
||||
}
|
||||
|
||||
start_python() { # <key> <port>
|
||||
local key="$1" port="$2" root module app exe bv="" run_cmd=() what using_venv=1
|
||||
root="$(zproj_root "$key")"
|
||||
module="$(zproj "$key" .startModule)"
|
||||
app="$(zproj "$key" .startApp)"
|
||||
[ -d "$root" ] || { err "Project root not found: $root"; return 1; }
|
||||
[ -n "$module" ] || [ -n "$app" ] || { err "Project '$key' (kind=python) needs 'startModule' (python -m ...) or 'startApp' (uvicorn app:app) in zconfig.json."; return 1; }
|
||||
|
||||
# Prefer the project venv. The Scripts/python.exe branch is a *Windows* venv
|
||||
# layout — only runnable from a Windows-family shell (Git Bash/MSYS/Cygwin).
|
||||
# On WSL/Linux/macOS a Windows-built venv may sit on a mounted drive and look
|
||||
# executable but can't exec, so guard that branch to fall through to python3.
|
||||
if [ -x "$root/.venv/bin/python" ]; then exe="$root/.venv/bin/python"
|
||||
elif [ -x "$root/.venv/Scripts/python.exe" ] && [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then exe="$root/.venv/Scripts/python.exe"
|
||||
elif command -v python3 >/dev/null 2>&1; then exe="python3"; using_venv=0
|
||||
else exe="python"; using_venv=0; 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
|
||||
|
||||
# startApp (uvicorn ASGI target, e.g. app.main:app) takes precedence over
|
||||
# startModule (python -m ...). uvicorn is run via the venv python's -m so no
|
||||
# PATH juggling is needed, and it gets zstart's bind-host + dev port.
|
||||
if [ -n "$app" ]; then
|
||||
run_cmd=("$exe" -m uvicorn "$app" --host "$bind_host" --port "$port" --reload)
|
||||
what="uvicorn $app"
|
||||
else
|
||||
run_cmd=("$exe" -m "$module")
|
||||
what="python -m $module"
|
||||
fi
|
||||
|
||||
printf '\n'; info "=== zstart ($(zproj "$key" .label)) ==="
|
||||
# No project venv found - warn, since the system interpreter usually lacks the
|
||||
# app's deps (the failure would otherwise be a cryptic ModuleNotFoundError).
|
||||
if [ "$using_venv" -eq 0 ]; then
|
||||
warn "No project venv at $root/.venv - using system '$exe' (its deps may be missing)."
|
||||
dim " Create one: (cd \"$root\" && python3 -m venv .venv && .venv/bin/pip install -e .)"
|
||||
fi
|
||||
info "Starting $what 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" "${run_cmd[@]}"
|
||||
else
|
||||
( cd "$root" && exec "${run_cmd[@]}" )
|
||||
fi
|
||||
}
|
||||
|
||||
start_vite() { # <key> <port>
|
||||
local key="$1" port="$2" root
|
||||
root="$(zproj_root "$key")"
|
||||
[ -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_root "$key")"
|
||||
[ -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"
|
||||
@ -1,66 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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
|
||||
38
bash/zstop
38
bash/zstop
@ -1,38 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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."
|
||||
97
bash/zsync
97
bash/zsync
@ -1,97 +0,0 @@
|
||||
#!/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_track_start "$@"
|
||||
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 "$(z_path "$(zq '.paths.backupsLocal')")")"
|
||||
OFFSITE_ROOT="$(z_path "$(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_root "$key")"
|
||||
|
||||
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
|
||||
@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
# 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.
|
||||
#
|
||||
# args.bats - how the bash scripts read their target arguments.
|
||||
#
|
||||
# bats tests/bash
|
||||
#
|
||||
# Mirrors tests/ArgumentParsing.Tests.ps1. Same safety rule as the PowerShell
|
||||
# side: these run the real scripts, so they stay on code paths that exit BEFORE
|
||||
# doing any work (no args, unknown key). Only zkill runs with a valid target,
|
||||
# and only because the fixture's dev ports are deliberately unused and its
|
||||
# localRoots do not exist - it finds no listeners and kills nothing. zdeploy,
|
||||
# zbackup_ec2, zec2, zec2online, zrepair, zstop, zstart and zbackup are never
|
||||
# invoked with a real target; that needs a disposable server.
|
||||
|
||||
setup() {
|
||||
REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
|
||||
BASHDIR="$REPO/bash"
|
||||
export ZCONFIG="$BATS_TEST_TMPDIR/zconfig.json"
|
||||
cat > "$ZCONFIG" <<'JSON'
|
||||
{
|
||||
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/fixtures/test.pem", "stackRoot": "/home/testuser/stack" },
|
||||
"paths": { "temp": "/fixtures/temp", "backupsLocal": "/fixtures/backups", "backupsEc2": "/fixtures/backups-ec2", "scriptsRoot": "/fixtures/zscripts", "oneDriveBackups": "" },
|
||||
"projects": {
|
||||
"_note": "comment key - must never be treated as a project",
|
||||
"pyapp": {
|
||||
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
|
||||
"startModule": "pyapp.main", "ports": { "dev": 59990 },
|
||||
"remote": { "path": "/home/testuser/stack/pyapp" }
|
||||
},
|
||||
"viteapp": {
|
||||
"label": "Fixture Vite Site", "kind": "vite", "localRoot": "/fixtures/nonexistent-viteapp",
|
||||
"ports": { "dev": 59991 }, "remote": { "path": "/home/testuser/stack/viteapp" }
|
||||
},
|
||||
"edgeproxy": {
|
||||
"label": "Fixture Edge", "kind": "edge", "localRoot": "/fixtures/nonexistent-edge",
|
||||
"remote": { "path": "/home/testuser/stack/edge" }
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
# ---- no arguments -----------------------------------------------------------
|
||||
# Running bare must never mean "do it to everything".
|
||||
|
||||
@test "zkill with no args shows usage and exits 1" {
|
||||
run "$BASHDIR/zkill"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Usage"* ]]
|
||||
}
|
||||
|
||||
@test "zstart with no args shows usage and exits non-zero" {
|
||||
run "$BASHDIR/zstart"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Usage"* ]]
|
||||
}
|
||||
|
||||
@test "zdeploy with no args shows usage and exits non-zero" {
|
||||
run "$BASHDIR/zdeploy"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Usage"* ]]
|
||||
}
|
||||
|
||||
@test "zbackup with no args shows usage and exits non-zero" {
|
||||
run "$BASHDIR/zbackup"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Usage"* ]]
|
||||
}
|
||||
|
||||
@test "usage lists the configured project keys" {
|
||||
run "$BASHDIR/zkill"
|
||||
[[ "$output" == *"pyapp"* ]]
|
||||
[[ "$output" == *"viteapp"* ]]
|
||||
}
|
||||
|
||||
@test "usage never advertises the underscore comment key" {
|
||||
run "$BASHDIR/zkill"
|
||||
[[ "$output" != *"_note"* ]]
|
||||
}
|
||||
|
||||
# ---- unknown key ------------------------------------------------------------
|
||||
# Exiting 0 having selected nothing is the dangerous outcome.
|
||||
|
||||
@test "zkill rejects an unknown project key" {
|
||||
run "$BASHDIR/zkill" definitelynotaproject
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Unknown project key"* ]]
|
||||
}
|
||||
|
||||
@test "zstart rejects an unknown project key" {
|
||||
run "$BASHDIR/zstart" definitelynotaproject
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "zdeploy rejects an unknown project key" {
|
||||
run "$BASHDIR/zdeploy" definitelynotaproject
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "the unknown-key error names both the bad key and the valid ones" {
|
||||
run "$BASHDIR/zkill" definitelynotaproject
|
||||
[[ "$output" == *"definitelynotaproject"* ]]
|
||||
[[ "$output" == *"pyapp"* ]]
|
||||
}
|
||||
|
||||
@test "the underscore comment key is not addressable as a project" {
|
||||
run "$BASHDIR/zkill" _note
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
# The convention is that ANY "_"-prefixed key is a comment, not a project -
|
||||
# PowerShell's Get-ZProject refuses them outright. A string-valued comment is
|
||||
# also caught by the "is it an object?" check, so this uses an OBJECT-valued
|
||||
# underscore key (e.g. someone keeping a "_template" block to copy from) to
|
||||
# prove the prefix rule itself is enforced.
|
||||
@test "an object-valued underscore key is still not addressable" {
|
||||
cat > "$BATS_TEST_TMPDIR/objkey.json" <<'JSON'
|
||||
{
|
||||
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" },
|
||||
"paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" },
|
||||
"projects": {
|
||||
"_template": {
|
||||
"label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none",
|
||||
"ports": { "dev": 59993 }, "remote": { "path": "/s/t" }
|
||||
},
|
||||
"pyapp": {
|
||||
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
|
||||
"ports": { "dev": 59990 }, "remote": { "path": "/s/py" }
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
ZCONFIG="$BATS_TEST_TMPDIR/objkey.json" run "$BASHDIR/zkill" _template
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Unknown project key"* ]]
|
||||
}
|
||||
|
||||
@test "an object-valued underscore key is excluded from 'all'" {
|
||||
cat > "$BATS_TEST_TMPDIR/objkey2.json" <<'JSON'
|
||||
{
|
||||
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" },
|
||||
"paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" },
|
||||
"projects": {
|
||||
"_template": {
|
||||
"label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none",
|
||||
"ports": { "dev": 59993 }, "remote": { "path": "/s/t" }
|
||||
},
|
||||
"pyapp": {
|
||||
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
|
||||
"ports": { "dev": 59990 }, "remote": { "path": "/s/py" }
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
ZCONFIG="$BATS_TEST_TMPDIR/objkey2.json" run "$BASHDIR/zkill" all
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Fixture Python App"* ]]
|
||||
[[ "$output" != *"Template To Copy"* ]]
|
||||
}
|
||||
|
||||
# ---- leading dash: CURRENT bash behaviour ------------------------------------
|
||||
# NOTE: this is a known divergence from the PowerShell port, pinned here as it
|
||||
# actually behaves rather than as it ideally would. PowerShell tolerates a
|
||||
# leading dash on any project key (zdeploy -myapp == zdeploy myapp). In bash,
|
||||
# only zdeploy strips it ("${1#-}"); every other script treats -myapp as an
|
||||
# unknown option and exits 1. These tests document today's behaviour so a fix
|
||||
# is a deliberate change with a failing test to update, not a silent surprise.
|
||||
|
||||
@test "bash zdeploy strips a leading dash from a project key" {
|
||||
# Unknown key keeps this on the no-work path; the error must name the key
|
||||
# without its dash, proving the strip happened.
|
||||
run "$BASHDIR/zdeploy" -definitelynotaproject
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"definitelynotaproject"* ]]
|
||||
[[ "$output" != *"-definitelynotaproject"* ]]
|
||||
}
|
||||
|
||||
@test "bash zkill rejects a dashed key as an unknown option (diverges from PowerShell)" {
|
||||
run "$BASHDIR/zkill" -pyapp
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Unknown option"* ]]
|
||||
}
|
||||
|
||||
@test "bash zstart rejects a dashed key as an unknown option (diverges from PowerShell)" {
|
||||
run "$BASHDIR/zstart" -pyapp
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Unknown option"* ]]
|
||||
}
|
||||
|
||||
# ---- zkill target resolution (safe: fixture ports are unused) ----------------
|
||||
|
||||
@test "zkill accepts a bare project key" {
|
||||
run "$BASHDIR/zkill" pyapp
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Fixture Python App"* ]]
|
||||
[[ "$output" == *"59990"* ]]
|
||||
}
|
||||
|
||||
@test "zkill accepts several keys at once" {
|
||||
run "$BASHDIR/zkill" pyapp viteapp
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Fixture Python App"* ]]
|
||||
[[ "$output" == *"Fixture Vite Site"* ]]
|
||||
}
|
||||
|
||||
@test "zkill 'all' expands to every project with a dev port" {
|
||||
run "$BASHDIR/zkill" all
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Fixture Python App"* ]]
|
||||
[[ "$output" == *"Fixture Vite Site"* ]]
|
||||
}
|
||||
|
||||
@test "zkill 'all' skips projects with no dev port (edge/docker stacks)" {
|
||||
run "$BASHDIR/zkill" all
|
||||
[[ "$output" != *"Fixture Edge"* ]]
|
||||
}
|
||||
|
||||
@test "zkill --port overrides the configured dev port" {
|
||||
run "$BASHDIR/zkill" pyapp --port 59999
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"59999"* ]]
|
||||
[[ "$output" != *"59990"* ]]
|
||||
}
|
||||
|
||||
@test "zkill announces that --kill-all is not implemented rather than silently ignoring it" {
|
||||
run "$BASHDIR/zkill" pyapp --kill-all
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"kill-all"* ]]
|
||||
}
|
||||
@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
# 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.
|
||||
#
|
||||
# helpers.bats - coverage for the pure/config functions in bash/zhelpers.sh.
|
||||
#
|
||||
# bats tests/bash
|
||||
#
|
||||
# The bash port has its own implementation of the exclude lists and config
|
||||
# accessors, so it can drift from the PowerShell side independently. These
|
||||
# mirror tests/ZHelpers.Tests.ps1 assertion-for-assertion where the two are
|
||||
# meant to agree.
|
||||
#
|
||||
# A fixture config is injected through ZCONFIG (zhelpers.sh has always honored
|
||||
# it), so no real zconfig.json is read.
|
||||
|
||||
setup() {
|
||||
REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
|
||||
export ZCONFIG="$BATS_TEST_TMPDIR/zconfig.json"
|
||||
cat > "$ZCONFIG" <<'JSON'
|
||||
{
|
||||
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/fixtures/test.pem", "stackRoot": "/home/testuser/stack" },
|
||||
"paths": { "temp": "/fixtures/temp", "backupsLocal": "/fixtures/backups", "backupsEc2": "/fixtures/backups-ec2", "scriptsRoot": "/fixtures/zscripts", "oneDriveBackups": "" },
|
||||
"projects": {
|
||||
"_note": "comment key - must never be treated as a project",
|
||||
"pyapp": {
|
||||
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/pyapp",
|
||||
"startModule": "pyapp.main", "ports": { "dev": 59990 }, "domain": "pyapp.example.com",
|
||||
"remote": { "path": "/home/testuser/stack/pyapp", "composeDir": "/home/testuser/stack/pyapp/docker", "appService": "app" },
|
||||
"deploy": { "zipName": "PyAppDeploy.zip", "exclude": ["docs", "fixtures-extra"] }
|
||||
},
|
||||
"viteapp": {
|
||||
"label": "Fixture Vite Site", "kind": "vite", "localRoot": "/fixtures/viteapp",
|
||||
"ports": { "dev": 59991 }, "domain": "www.example.com",
|
||||
"remote": { "path": "/home/testuser/stack/viteapp" }
|
||||
},
|
||||
"nextapp": {
|
||||
"label": "Fixture Next App", "kind": "nextjs", "localRoot": "/fixtures/nextapp",
|
||||
"ports": { "dev": 59992 }, "remote": { "path": "/home/testuser/stack/nextapp" }
|
||||
},
|
||||
"edgeproxy": {
|
||||
"label": "Fixture Edge", "kind": "edge", "localRoot": "/fixtures/edge",
|
||||
"proxyContainer": "edge_proxy", "remote": { "path": "/home/testuser/stack/edge" }
|
||||
},
|
||||
"noports": {
|
||||
"label": "Fixture No Dev Port", "kind": "python", "localRoot": "/fixtures/noports",
|
||||
"remote": { "path": "/home/testuser/stack/noports" }
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
# shellcheck disable=SC1090
|
||||
source "$REPO/bash/zhelpers.sh"
|
||||
}
|
||||
|
||||
# Emit the exclude list for a project; $2=1 means "for backup".
|
||||
excludes() { z_archive_excludes "$1" "${2:-0}"; }
|
||||
|
||||
# Fixed-string, whole-line match. -F matters: without it grep treats the needle
|
||||
# as a regex, and '.env' would match 'venv' (leading '.' = any char) - which
|
||||
# silently turns every ".env is excluded" assertion into a false pass.
|
||||
has() { printf '%s\n' "$1" | grep -qxF "$2"; }
|
||||
|
||||
# ---- config accessors -------------------------------------------------------
|
||||
|
||||
@test "zproj_keys returns every project in config order" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys | paste -sd, -"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "pyapp,viteapp,nextapp,edgeproxy,noports" ]
|
||||
}
|
||||
|
||||
@test "zproj_keys omits underscore comment keys" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys"
|
||||
[ "$status" -eq 0 ]
|
||||
! has "$output" "_note"
|
||||
}
|
||||
|
||||
@test "zproj reads a project field" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zproj pyapp .label"
|
||||
[ "$output" = "Fixture Python App" ]
|
||||
}
|
||||
|
||||
@test "zproj returns empty for a missing field rather than the string null" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zproj viteapp .startModule"
|
||||
[ "$output" = "" ]
|
||||
}
|
||||
|
||||
@test "zec2_target composes user@ip" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zec2_target"
|
||||
[ "$output" = "testuser@203.0.113.10" ]
|
||||
}
|
||||
|
||||
@test "zremote_compose_dir prefers composeDir" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zremote_compose_dir pyapp"
|
||||
[ "$output" = "/home/testuser/stack/pyapp/docker" ]
|
||||
}
|
||||
|
||||
@test "zremote_compose_dir falls back to remote.path" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zremote_compose_dir viteapp"
|
||||
[ "$output" = "/home/testuser/stack/viteapp" ]
|
||||
}
|
||||
|
||||
@test "zedge_key finds the first edge-kind project" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zedge_key"
|
||||
[ "$output" = "edgeproxy" ]
|
||||
}
|
||||
|
||||
@test "zproj_keys_with_domain lists only projects that have a domain" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys_with_domain | paste -sd, -"
|
||||
[ "$output" = "pyapp,viteapp" ]
|
||||
}
|
||||
|
||||
# ---- z_path (Windows -> host path translation) ------------------------------
|
||||
# A shared zconfig.json may carry Windows paths when the same file is used from
|
||||
# a Windows checkout and from WSL.
|
||||
|
||||
@test "z_path translates a Windows drive path to /mnt form" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; z_path 'F:\\evomedia.net\\app'"
|
||||
[ "$output" = "/mnt/f/evomedia.net/app" ]
|
||||
}
|
||||
|
||||
@test "z_path lowercases the drive letter" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; z_path 'C:\\Users\\Someone'"
|
||||
[ "$output" = "/mnt/c/Users/Someone" ]
|
||||
}
|
||||
|
||||
@test "z_path leaves a unix path untouched (no-op on native Linux/macOS)" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; z_path '/home/kelly/project'"
|
||||
[ "$output" = "/home/kelly/project" ]
|
||||
}
|
||||
|
||||
@test "z_path leaves an empty string empty" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; z_path ''"
|
||||
[ "$output" = "" ]
|
||||
}
|
||||
|
||||
# ---- z_archive_excludes -----------------------------------------------------
|
||||
# Parity target: tests/ZHelpers.Tests.ps1 "Get-ArchiveExcludes".
|
||||
|
||||
@test "excludes: common junk applies to every kind" {
|
||||
run excludes pyapp
|
||||
for n in .git .idea .vscode .claude tmp nul .DS_Store backups; do
|
||||
has "$output" "$n" || { echo "missing common exclude: $n"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: python build/venv cruft" {
|
||||
run excludes pyapp
|
||||
for n in .venv venv __pycache__ .pytest_cache .nicegui archive dist build htmlcov; do
|
||||
has "$output" "$n" || { echo "missing python exclude: $n"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: vite drops node_modules and dist" {
|
||||
run excludes viteapp
|
||||
has "$output" "node_modules"
|
||||
has "$output" "dist"
|
||||
}
|
||||
|
||||
@test "excludes: nextjs drops .next and .vercel" {
|
||||
run excludes nextapp
|
||||
has "$output" ".next"
|
||||
has "$output" ".vercel"
|
||||
has "$output" "next-env.d.ts"
|
||||
}
|
||||
|
||||
# The regression this suite exists for: a dev .env shipped to production in a
|
||||
# deploy zip. Deploys drop secrets and uploads; backups must keep them or the
|
||||
# backup is not a restore point.
|
||||
@test "excludes: python DEPLOY drops .env, .env.local, .env.production and uploads" {
|
||||
run excludes pyapp 0
|
||||
for n in .env .env.local .env.production uploads; do
|
||||
has "$output" "$n" || { echo "deploy should exclude: $n"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: python BACKUP keeps .env, .env.local, .env.production and uploads" {
|
||||
run excludes pyapp 1
|
||||
for n in .env .env.local .env.production uploads; do
|
||||
if has "$output" "$n"; then echo "backup must NOT exclude: $n"; return 1; fi
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: python BACKUP still drops the virtualenv (bulk, not a secret)" {
|
||||
run excludes pyapp 1
|
||||
has "$output" ".venv"
|
||||
}
|
||||
|
||||
@test "excludes: vite DEPLOY drops .env files" {
|
||||
run excludes viteapp 0
|
||||
for n in .env .env.local .env.production; do
|
||||
has "$output" "$n" || { echo "deploy should exclude: $n"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: vite BACKUP keeps .env files" {
|
||||
run excludes viteapp 1
|
||||
for n in .env .env.local .env.production; do
|
||||
if has "$output" "$n"; then echo "backup must NOT exclude: $n"; return 1; fi
|
||||
done
|
||||
}
|
||||
|
||||
@test "excludes: vite BACKUP still drops node_modules" {
|
||||
run excludes viteapp 1
|
||||
has "$output" "node_modules"
|
||||
}
|
||||
|
||||
# nextjs has no backup gate on the PowerShell side either - both agree.
|
||||
@test "excludes: nextjs drops .env in a backup too (no gate, matches PowerShell)" {
|
||||
run excludes nextapp 1
|
||||
has "$output" ".env"
|
||||
}
|
||||
|
||||
@test "excludes: the project's own deploy.exclude entries are merged in" {
|
||||
run excludes pyapp
|
||||
has "$output" "docs"
|
||||
has "$output" "fixtures-extra"
|
||||
}
|
||||
|
||||
@test "excludes: a kind with no specific list still gets the common entries" {
|
||||
run excludes edgeproxy
|
||||
has "$output" ".git"
|
||||
}
|
||||
|
||||
# ---- json_build_label -------------------------------------------------------
|
||||
|
||||
@test "json_build_label formats v<productVersion>.<buildNumber>" {
|
||||
printf '{ "productVersion": "1.0", "buildNumber": 42 }' > "$BATS_TEST_TMPDIR/bv.json"
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; json_build_label '$BATS_TEST_TMPDIR/bv.json'"
|
||||
[ "$output" = "v1.0.42" ]
|
||||
}
|
||||
|
||||
@test "json_build_label reports unknown for a missing file" {
|
||||
run bash -c "source '$REPO/bash/zhelpers.sh'; json_build_label '$BATS_TEST_TMPDIR/nope.json'"
|
||||
[ "$output" = "unknown" ]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user