feat(ops): auto-deploy evOo-local on Evo after main advances (#85)
ops/evo-deploy: deploy.sh with health+rollback, evo-deploy.yml on self-hosted evoo-ci (gated by EVO_DEPLOY_ENABLED), README.
This commit is contained in:
parent
5cd1bc906f
commit
f2a8052f4d
83
.github/workflows/evo-deploy.yml
vendored
Normal file
83
.github/workflows/evo-deploy.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
name: evo-deploy
|
||||||
|
|
||||||
|
# Deploy evOo-local on the Evo host when main advances, or on manual dispatch.
|
||||||
|
# Runs ON the self-hosted Evo runner (no SSH, no deploy secrets in the workflow).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: Git ref or SHA to deploy (default main tip)
|
||||||
|
required: false
|
||||||
|
default: main
|
||||||
|
dry_run:
|
||||||
|
description: Dry run (no git/service changes)
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
skip_migrate:
|
||||||
|
description: Skip database migrations
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: evo-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: vars.EVO_DEPLOY_ENABLED == 'true'
|
||||||
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout deploy scripts
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.sha }}
|
||||||
|
|
||||||
|
- name: Deploy evOo-local on Evo
|
||||||
|
env:
|
||||||
|
EVO_DEPLOY_DIR: ${{ vars.EVO_DEPLOY_DIR }}
|
||||||
|
EVO_DEPLOY_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.sha }}
|
||||||
|
EVO_DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||||
|
EVO_MIGRATE: ${{ github.event.inputs.skip_migrate == true && 'false' || 'true' }}
|
||||||
|
EVO_RESTART_MODE: ${{ vars.EVO_RESTART_MODE || 'auto' }}
|
||||||
|
EVO_SYSTEMD_UNIT: ${{ vars.EVO_SYSTEMD_UNIT || '' }}
|
||||||
|
EVO_SYSTEMD_SCOPE: ${{ vars.EVO_SYSTEMD_SCOPE || 'user' }}
|
||||||
|
EVO_TMUX_SESSION: ${{ vars.EVO_TMUX_SESSION || 'evoo' }}
|
||||||
|
EVO_API_HEALTH_URL: ${{ vars.EVO_API_HEALTH_URL || 'http://127.0.0.1:3100/health' }}
|
||||||
|
EVO_WEB_HEALTH_URL: ${{ vars.EVO_WEB_HEALTH_URL || 'http://127.0.0.1:5173/' }}
|
||||||
|
EVO_HEALTH_TIMEOUT_SEC: ${{ vars.EVO_HEALTH_TIMEOUT_SEC || '120' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
EVO_DEPLOY_DIR="${EVO_DEPLOY_DIR:-${HOME}/Projects/evOo-local}"
|
||||||
|
export EVO_DEPLOY_DIR
|
||||||
|
test -d "$EVO_DEPLOY_DIR" || { echo "EVO_DEPLOY_DIR missing: $EVO_DEPLOY_DIR" >&2; exit 1; }
|
||||||
|
args=()
|
||||||
|
[[ "$EVO_DRY_RUN" == "true" ]] && args+=(--dry-run)
|
||||||
|
[[ "$EVO_MIGRATE" == "false" ]] && args+=(--skip-migrate)
|
||||||
|
bash "${GITHUB_WORKSPACE}/ops/evo-deploy/deploy.sh" "${args[@]}"
|
||||||
|
|
||||||
|
- name: Deploy summary
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
EVO_DEPLOY_DIR: ${{ vars.EVO_DEPLOY_DIR }}
|
||||||
|
run: |
|
||||||
|
EVO_DEPLOY_DIR="${EVO_DEPLOY_DIR:-${HOME}/Projects/evOo-local}"
|
||||||
|
EVO_STATE_FILE="$EVO_DEPLOY_DIR/.evo-deploy/state.json"
|
||||||
|
{
|
||||||
|
echo "### Evo deploy"
|
||||||
|
echo
|
||||||
|
echo "- Ref: \`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.sha }}\`"
|
||||||
|
echo "- Dir: \`$EVO_DEPLOY_DIR\`"
|
||||||
|
if [[ -f "$EVO_STATE_FILE" ]]; then
|
||||||
|
echo "- State:"
|
||||||
|
echo '```json'
|
||||||
|
cat "$EVO_STATE_FILE"
|
||||||
|
echo '```'
|
||||||
|
fi
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
103
ops/evo-deploy/README.md
Normal file
103
ops/evo-deploy/README.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# Evo auto-deploy (evOo-local)
|
||||||
|
|
||||||
|
Idempotent deploy helper for the **source checkout** on the Evo host (`~/Projects/evOo-local`, user `cynar`). Pulls `main` (or a pinned SHA), runs migrations, restarts the existing stack, health-checks API `:3100` and web `:5173`, and rolls back on failure.
|
||||||
|
|
||||||
|
This is separate from production Docker deploys under `/srv/rakazo` (see `docs/self-host.md` and `.github/workflows/ci.yml` `deploy-production`).
|
||||||
|
|
||||||
|
## Prerequisites on Evo
|
||||||
|
|
||||||
|
1. Git checkout at `~/Projects/evOo-local` (or set `EVO_DEPLOY_DIR`).
|
||||||
|
2. `.env` with a working `DATABASE_URL` (Postgres reachable for `pnpm db:migrate`).
|
||||||
|
3. A restart path the script can use (pick one):
|
||||||
|
- **systemd (recommended for unattended deploy):** user unit, e.g. `evoo-local.service`, that starts `pnpm dev` or your stack wrapper. Set repo variable `EVO_SYSTEMD_UNIT`.
|
||||||
|
- **tmux:** session `evoo` running `pnpm dev` (matches `scripts/start-tmux.sh` on Mac; on Evo set `EVO_TMUX_SESSION` if different).
|
||||||
|
- **compose:** `infra/compose/docker-compose.yml` with `api` / `web` / `worker` (dev stack).
|
||||||
|
- **script:** executable hook at `EVO_RESTART_SCRIPT`.
|
||||||
|
4. Self-hosted GitHub Actions runner on Evo with labels `[self-hosted, evoo-ci, Linux]` (already used by CI).
|
||||||
|
|
||||||
|
No SSH keys or deploy secrets are stored in this repo. The workflow runs **on** the Evo runner against the local checkout.
|
||||||
|
|
||||||
|
## Enable the workflow
|
||||||
|
|
||||||
|
1. Merge this PR (or copy `ops/evo-deploy/` and `.github/workflows/evo-deploy.yml` to `main`).
|
||||||
|
2. In GitHub → **Settings → Secrets and variables → Actions → Variables** for `cynarAI/evOo-local`:
|
||||||
|
- `EVO_DEPLOY_ENABLED` = `true`
|
||||||
|
- Optional: `EVO_DEPLOY_DIR` (default `~/Projects/evOo-local` on the runner user)
|
||||||
|
- Optional: `EVO_RESTART_MODE` (`auto`, `systemd`, `tmux`, `compose`, `script`, `none`)
|
||||||
|
- Optional: `EVO_SYSTEMD_UNIT`, `EVO_TMUX_SESSION`, health URLs, timeouts
|
||||||
|
3. Confirm the runner user can read/write `EVO_DEPLOY_DIR` and restart services (`systemctl --user` or tmux).
|
||||||
|
|
||||||
|
Until `EVO_DEPLOY_ENABLED` is `true`, pushes to `main` do not deploy (workflow job is skipped).
|
||||||
|
|
||||||
|
## Manual / dry-run
|
||||||
|
|
||||||
|
On Evo (or any machine with the checkout and stack):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/evOo-local
|
||||||
|
|
||||||
|
# Show what would happen (no git reset, migrate, restart, or health wait)
|
||||||
|
bash ops/evo-deploy/deploy.sh --dry-run
|
||||||
|
|
||||||
|
# Deploy main tip
|
||||||
|
bash ops/evo-deploy/deploy.sh
|
||||||
|
|
||||||
|
# Pin a SHA
|
||||||
|
EVO_DEPLOY_REF=abc1234 bash ops/evo-deploy/deploy.sh
|
||||||
|
|
||||||
|
# Force systemd restart
|
||||||
|
EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service bash ops/evo-deploy/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub: **Actions → evo-deploy → Run workflow** (optional `ref`, `dry_run`, `skip_migrate`).
|
||||||
|
|
||||||
|
## Rollback behavior
|
||||||
|
|
||||||
|
1. Before `git fetch`, the script records `previousSha` (current `HEAD`).
|
||||||
|
2. After pull, migrate, and restart, it waits up to `EVO_HEALTH_TIMEOUT_SEC` (default 120s) for:
|
||||||
|
- `EVO_API_HEALTH_URL` (default `http://127.0.0.1:3100/health`)
|
||||||
|
- `EVO_WEB_HEALTH_URL` (default `http://127.0.0.1:5173/`)
|
||||||
|
3. On failure: `git reset --hard` to `previousSha`, restart services again, exit non-zero.
|
||||||
|
4. Last outcome is written to `.evo-deploy/state.json` under the checkout.
|
||||||
|
|
||||||
|
Manual rollback to a known good SHA:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/evOo-local
|
||||||
|
git reset --hard <good-sha>
|
||||||
|
EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service bash ops/evo-deploy/deploy.sh --skip-migrate --ref HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
(or restart your unit/tmux session by hand, then verify health URLs).
|
||||||
|
|
||||||
|
## Autopilot / Grok Bot after merge
|
||||||
|
|
||||||
|
After a PR merges to `main`:
|
||||||
|
|
||||||
|
1. **Preferred:** rely on the `evo-deploy` workflow (once `EVO_DEPLOY_ENABLED=true`). No SSH step required.
|
||||||
|
2. **Fallback (manual):** SSH to Evo and run:
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/evOo-local && bash ops/evo-deploy/deploy.sh
|
||||||
|
```
|
||||||
|
3. **Verify:** API `curl -fsS http://127.0.0.1:3100/health`, web `curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5173/`.
|
||||||
|
|
||||||
|
Do not commit host passwords, tokens, or SSH private keys. Configure restart targets via repo variables or env on the runner host only.
|
||||||
|
|
||||||
|
## Environment reference
|
||||||
|
|
||||||
|
| Variable | Default | Purpose |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| `EVO_DEPLOY_DIR` | `$HOME/Projects/evOo-local` | Git checkout root |
|
||||||
|
| `EVO_DEPLOY_REF` | `main` | Branch name or commit SHA |
|
||||||
|
| `EVO_RESTART_MODE` | `auto` | `systemd` / `tmux` / `compose` / `script` / `none` |
|
||||||
|
| `EVO_SYSTEMD_UNIT` | _(empty)_ | Required for `systemd` mode |
|
||||||
|
| `EVO_SYSTEMD_SCOPE` | `user` | `user` or `system` |
|
||||||
|
| `EVO_TMUX_SESSION` | `evoo` | tmux session name |
|
||||||
|
| `EVO_TMUX_COMMAND` | `pnpm dev` | Command started in new tmux session |
|
||||||
|
| `EVO_MIGRATE` | `true` | Run `pnpm db:generate` + `pnpm db:migrate` |
|
||||||
|
| `EVO_INSTALL` | `true` | Run `pnpm install --frozen-lockfile` after checkout |
|
||||||
|
| `EVO_API_HEALTH_URL` | `http://127.0.0.1:3100/health` | API health probe |
|
||||||
|
| `EVO_WEB_HEALTH_URL` | `http://127.0.0.1:5173/` | Web health probe |
|
||||||
|
| `EVO_HEALTH_TIMEOUT_SEC` | `120` | Per-check timeout (seconds) |
|
||||||
|
|
||||||
|
If `EVO_RESTART_MODE=auto` cannot detect systemd, tmux, compose, or a restart script, deploy fails with a clear error — set `EVO_RESTART_MODE` and the matching unit/session explicitly.
|
||||||
310
ops/evo-deploy/deploy.sh
Executable file
310
ops/evo-deploy/deploy.sh
Executable file
@ -0,0 +1,310 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# evOo-local — idempotent deploy helper for the Evo host checkout.
|
||||||
|
# Pull (or pin) a revision, migrate, restart services, health-check, rollback on failure.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# --- defaults (override via env; see ops/evo-deploy/README.md) ---
|
||||||
|
EVO_DEPLOY_DIR="${EVO_DEPLOY_DIR:-${HOME}/Projects/evOo-local}"
|
||||||
|
EVO_DEPLOY_REF="${EVO_DEPLOY_REF:-main}"
|
||||||
|
EVO_GIT_REMOTE="${EVO_GIT_REMOTE:-origin}"
|
||||||
|
EVO_RESTART_MODE="${EVO_RESTART_MODE:-auto}" # auto | systemd | tmux | compose | script | none
|
||||||
|
EVO_SYSTEMD_UNIT="${EVO_SYSTEMD_UNIT:-}"
|
||||||
|
EVO_SYSTEMD_SCOPE="${EVO_SYSTEMD_SCOPE:-user}" # user | system
|
||||||
|
EVO_TMUX_SESSION="${EVO_TMUX_SESSION:-evoo}"
|
||||||
|
EVO_TMUX_COMMAND="${EVO_TMUX_COMMAND:-pnpm dev}"
|
||||||
|
EVO_COMPOSE_FILE="${EVO_COMPOSE_FILE:-infra/compose/docker-compose.yml}"
|
||||||
|
EVO_COMPOSE_ENV_FILE="${EVO_COMPOSE_ENV_FILE:-.env}"
|
||||||
|
EVO_COMPOSE_SERVICES="${EVO_COMPOSE_SERVICES:-api web worker}"
|
||||||
|
EVO_RESTART_SCRIPT="${EVO_RESTART_SCRIPT:-}"
|
||||||
|
EVO_API_HEALTH_URL="${EVO_API_HEALTH_URL:-http://127.0.0.1:3100/health}"
|
||||||
|
EVO_WEB_HEALTH_URL="${EVO_WEB_HEALTH_URL:-http://127.0.0.1:5173/}"
|
||||||
|
EVO_HEALTH_TIMEOUT_SEC="${EVO_HEALTH_TIMEOUT_SEC:-120}"
|
||||||
|
EVO_MIGRATE="${EVO_MIGRATE:-true}"
|
||||||
|
EVO_INSTALL="${EVO_INSTALL:-true}"
|
||||||
|
EVO_DRY_RUN="${EVO_DRY_RUN:-false}"
|
||||||
|
EVO_STATE_FILE="${EVO_STATE_FILE:-${EVO_DEPLOY_DIR}/.evo-deploy/state.json}"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
log() { printf '[evo-deploy] %s\n' "$*" >&2; }
|
||||||
|
die() { log "ERROR: $*"; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: deploy.sh [--dry-run] [--ref REF] [--skip-migrate] [--skip-install]
|
||||||
|
|
||||||
|
Environment: EVO_DEPLOY_DIR, EVO_DEPLOY_REF, EVO_RESTART_MODE, EVO_SYSTEMD_UNIT,
|
||||||
|
EVO_TMUX_SESSION, EVO_API_HEALTH_URL, EVO_WEB_HEALTH_URL, EVO_HEALTH_TIMEOUT_SEC, …
|
||||||
|
See ops/evo-deploy/README.md.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
EVO_DRY_RUN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ref)
|
||||||
|
[[ $# -ge 2 ]] || die "--ref requires a value"
|
||||||
|
EVO_DEPLOY_REF="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-migrate)
|
||||||
|
EVO_MIGRATE=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-install)
|
||||||
|
EVO_INSTALL=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ "$EVO_DRY_RUN" == "true" ]]; then
|
||||||
|
DRY_RUN=true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "[dry-run] $*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_shell() {
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "[dry-run] $*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
bash -lc "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_deploy_dir() {
|
||||||
|
[[ -d "$EVO_DEPLOY_DIR" ]] || die "EVO_DEPLOY_DIR does not exist: $EVO_DEPLOY_DIR"
|
||||||
|
[[ -d "$EVO_DEPLOY_DIR/.git" ]] || die "EVO_DEPLOY_DIR is not a git checkout: $EVO_DEPLOY_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_restart_mode() {
|
||||||
|
if [[ "$EVO_RESTART_MODE" != "auto" ]]; then
|
||||||
|
printf '%s\n' "$EVO_RESTART_MODE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -n "$EVO_SYSTEMD_UNIT" ]]; then
|
||||||
|
printf 'systemd\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -n "$EVO_RESTART_SCRIPT" && -x "$EVO_RESTART_SCRIPT" ]]; then
|
||||||
|
printf 'script\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$EVO_TMUX_SESSION" 2>/dev/null; then
|
||||||
|
printf 'tmux\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE" ]]; then
|
||||||
|
printf 'compose\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
printf 'none\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
systemctl_cmd() {
|
||||||
|
if [[ "$EVO_SYSTEMD_SCOPE" == "system" ]]; then
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then
|
||||||
|
systemctl "$@"
|
||||||
|
else
|
||||||
|
sudo systemctl "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
systemctl --user "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_systemd() {
|
||||||
|
[[ -n "$EVO_SYSTEMD_UNIT" ]] || die "EVO_RESTART_MODE=systemd requires EVO_SYSTEMD_UNIT"
|
||||||
|
log "restarting systemd unit: $EVO_SYSTEMD_UNIT ($EVO_SYSTEMD_SCOPE scope)"
|
||||||
|
run systemctl_cmd restart "$EVO_SYSTEMD_UNIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_tmux() {
|
||||||
|
command -v tmux >/dev/null 2>&1 || die "tmux not found; set EVO_RESTART_MODE=systemd or install tmux"
|
||||||
|
log "restarting tmux session '$EVO_TMUX_SESSION' in $EVO_DEPLOY_DIR"
|
||||||
|
if [[ "$DRY_RUN" != true ]] && tmux has-session -t "$EVO_TMUX_SESSION" 2>/dev/null; then
|
||||||
|
tmux kill-session -t "$EVO_TMUX_SESSION"
|
||||||
|
fi
|
||||||
|
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && tmux new-session -d -s $(printf '%q' "$EVO_TMUX_SESSION") -c $(printf '%q' "$EVO_DEPLOY_DIR") $(printf '%q' "$EVO_TMUX_COMMAND")"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_compose() {
|
||||||
|
local compose_path="$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE"
|
||||||
|
[[ -f "$compose_path" ]] || die "compose file not found: $compose_path"
|
||||||
|
log "restarting compose services ($EVO_COMPOSE_SERVICES)"
|
||||||
|
local env_args=()
|
||||||
|
[[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_ENV_FILE" ]] && env_args=(--env-file "$EVO_DEPLOY_DIR/$EVO_COMPOSE_ENV_FILE")
|
||||||
|
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && docker compose ${env_args[*]} -f $(printf '%q' "$EVO_COMPOSE_FILE") up -d --wait $EVO_COMPOSE_SERVICES"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_script() {
|
||||||
|
[[ -n "$EVO_RESTART_SCRIPT" ]] || die "EVO_RESTART_MODE=script requires EVO_RESTART_SCRIPT"
|
||||||
|
[[ -x "$EVO_RESTART_SCRIPT" ]] || die "EVO_RESTART_SCRIPT is not executable: $EVO_RESTART_SCRIPT"
|
||||||
|
log "running restart script: $EVO_RESTART_SCRIPT"
|
||||||
|
run env EVO_DEPLOY_DIR="$EVO_DEPLOY_DIR" "$EVO_RESTART_SCRIPT"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_services() {
|
||||||
|
local mode
|
||||||
|
mode="$(resolve_restart_mode)"
|
||||||
|
case "$mode" in
|
||||||
|
systemd) restart_systemd ;;
|
||||||
|
tmux) restart_tmux ;;
|
||||||
|
compose) restart_compose ;;
|
||||||
|
script) restart_script ;;
|
||||||
|
none)
|
||||||
|
log "EVO_RESTART_MODE=none — skipping service restart (health check only)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "unsupported restart mode: $mode"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_url() {
|
||||||
|
local url="$1"
|
||||||
|
local label="$2"
|
||||||
|
local timeout="$EVO_HEALTH_TIMEOUT_SEC"
|
||||||
|
local start now last=""
|
||||||
|
start="$(date +%s)"
|
||||||
|
log "health-check $label: $url (timeout ${timeout}s)"
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "[dry-run] would wait for $label at $url"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while true; do
|
||||||
|
now="$(date +%s)"
|
||||||
|
if (( now - start >= timeout )); then
|
||||||
|
log "ERROR: $label health check timed out after ${timeout}s (last: ${last:-unknown})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if curl -fsS --max-time 5 "$url" >/dev/null 2>&1; then
|
||||||
|
log "$label healthy"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
last="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null || echo unreachable)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
health_check() {
|
||||||
|
wait_for_url "$EVO_API_HEALTH_URL" "API"
|
||||||
|
wait_for_url "$EVO_WEB_HEALTH_URL" "web"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_state() {
|
||||||
|
local previous_sha="$1" target_sha="$2" status="$3"
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "[dry-run] would write state to $EVO_STATE_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$EVO_STATE_FILE")"
|
||||||
|
cat >"$EVO_STATE_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"previousSha": "$previous_sha",
|
||||||
|
"deployedSha": "$target_sha",
|
||||||
|
"ref": "$EVO_DEPLOY_REF",
|
||||||
|
"status": "$status",
|
||||||
|
"at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
git_fetch_and_checkout() {
|
||||||
|
local previous_sha target_sha
|
||||||
|
previous_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse HEAD)"
|
||||||
|
log "previous HEAD: $previous_sha"
|
||||||
|
log "fetching $EVO_GIT_REMOTE…"
|
||||||
|
run git -C "$EVO_DEPLOY_DIR" fetch "$EVO_GIT_REMOTE" --prune
|
||||||
|
if git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||||
|
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_DEPLOY_REF}^{commit}")"
|
||||||
|
elif git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
|
||||||
|
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}")"
|
||||||
|
else
|
||||||
|
die "cannot resolve ref: $EVO_DEPLOY_REF"
|
||||||
|
fi
|
||||||
|
log "checking out $target_sha ($EVO_DEPLOY_REF)"
|
||||||
|
run git -C "$EVO_DEPLOY_DIR" reset --hard "$target_sha"
|
||||||
|
printf '%s %s\n' "$previous_sha" "$target_sha"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_migrate() {
|
||||||
|
[[ "$EVO_MIGRATE" == "true" ]] || { log "skipping migrate (EVO_MIGRATE=false)"; return 0; }
|
||||||
|
if [[ ! -f "$EVO_DEPLOY_DIR/package.json" ]]; then
|
||||||
|
die "package.json missing in $EVO_DEPLOY_DIR"
|
||||||
|
fi
|
||||||
|
if ! grep -q '"db:migrate"' "$EVO_DEPLOY_DIR/package.json"; then
|
||||||
|
log "no db:migrate script in package.json — skipping migrations"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "running pnpm db:generate && pnpm db:migrate"
|
||||||
|
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm db:generate && pnpm db:migrate"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_install() {
|
||||||
|
[[ "$EVO_INSTALL" == "true" ]] || { log "skipping install (EVO_INSTALL=false)"; return 0; }
|
||||||
|
log "running pnpm install --frozen-lockfile"
|
||||||
|
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm install --frozen-lockfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback() {
|
||||||
|
local previous_sha="$1"
|
||||||
|
log "ROLLBACK: resetting to $previous_sha and restarting services"
|
||||||
|
run git -C "$EVO_DEPLOY_DIR" reset --hard "$previous_sha"
|
||||||
|
restart_services
|
||||||
|
write_state "$previous_sha" "$previous_sha" "rolled-back"
|
||||||
|
die "deploy failed; rolled back to $previous_sha"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
validate_deploy_dir
|
||||||
|
|
||||||
|
local resolved_mode
|
||||||
|
resolved_mode="$(resolve_restart_mode)"
|
||||||
|
log "deploy dir: $EVO_DEPLOY_DIR"
|
||||||
|
log "target ref: $EVO_DEPLOY_REF"
|
||||||
|
log "restart mode: $resolved_mode"
|
||||||
|
[[ "$DRY_RUN" == true ]] && log "dry-run enabled — no mutating commands"
|
||||||
|
|
||||||
|
local previous_sha target_sha
|
||||||
|
read -r previous_sha target_sha < <(git_fetch_and_checkout)
|
||||||
|
|
||||||
|
if [[ "$previous_sha" == "$target_sha" && "$DRY_RUN" != true ]]; then
|
||||||
|
log "already at $target_sha — continuing with migrate/restart/health"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_install
|
||||||
|
run_migrate
|
||||||
|
restart_services
|
||||||
|
|
||||||
|
if ! health_check; then
|
||||||
|
rollback "$previous_sha"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_state "$previous_sha" "$target_sha" "success"
|
||||||
|
log "deploy succeeded: $previous_sha -> $target_sha"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
x
Reference in New Issue
Block a user