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 (#34)
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
c20e82e209
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.
|
# PowerShell/cmd are happiest with CRLF on Windows.
|
||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
*.cmd 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
|
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
||||||
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
||||||
testing — reintroducing each historical bug turns the suite red.
|
testing — reintroducing each historical bug turns the suite red.
|
||||||
- **`ZCONFIG` environment variable (PowerShell)** — overrides the path to
|
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
||||||
`zconfig.json`, matching the bash port, which has always honored it. Closes a
|
a run can target an alternate config. Also gives the test suite a seam for
|
||||||
parity gap and gives the test suite a seam for injecting a fixture config.
|
injecting a fixture.
|
||||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
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
|
- **`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
|
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`;
|
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
|
the one-shot "stop everything I've got running locally".
|
||||||
PowerShell and bash versions.
|
|
||||||
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
||||||
not published through the edge proxy can declare
|
not published through the edge proxy can declare
|
||||||
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
`"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.
|
**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
|
## 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.
|
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
|
### Config reference
|
||||||
|
|
||||||
|
|||||||
@ -34,9 +34,8 @@ $script:JunkDirNames = @(
|
|||||||
$script:ZConfigCache = $null
|
$script:ZConfigCache = $null
|
||||||
|
|
||||||
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
||||||
# ZCONFIG environment variable, matching the bash port (zhelpers.sh does the
|
# ZCONFIG environment variable. Useful for pointing a run at an alternate
|
||||||
# same). Useful for pointing a run at an alternate config, and it is the seam
|
# config, and it is the seam the test suite uses to inject a fixture.
|
||||||
# the test suite uses to inject a fixture.
|
|
||||||
function Get-ZConfigPath {
|
function Get-ZConfigPath {
|
||||||
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
||||||
return (Join-Path $PSScriptRoot "zconfig.json")
|
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