git reset --hard prints 'HEAD is now at …' to stdout, which polluted read -r previous_sha target_sha < <(git_fetch_and_checkout) and wrote invalid values into .evo-deploy/state.json. Redirect git stdout to stderr inside git_fetch_and_checkout and rollback; validate SHAs before write_state and rollback. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
5.6 KiB
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
- Git checkout at
~/Projects/evOo-local(or setEVO_DEPLOY_DIR). .envwith a workingDATABASE_URL(Postgres reachable forpnpm db:migrate).- A restart path the script can use (pick one):
- systemd (recommended for unattended deploy): user unit, e.g.
evoo-local.service, that startspnpm devor your stack wrapper. Set repo variableEVO_SYSTEMD_UNIT. - tmux: session
evoorunningpnpm dev(matchesscripts/start-tmux.shon Mac; on Evo setEVO_TMUX_SESSIONif different). - compose:
infra/compose/docker-compose.ymlwithapi/web/worker(dev stack). - script: executable hook at
EVO_RESTART_SCRIPT.
- systemd (recommended for unattended deploy): user unit, e.g.
- 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
- Merge this PR (or copy
ops/evo-deploy/and.github/workflows/evo-deploy.ymltomain). - In GitHub → Settings → Secrets and variables → Actions → Variables for
cynarAI/evOo-local:EVO_DEPLOY_ENABLED=true- Optional:
EVO_DEPLOY_DIR(default~/Projects/evOo-localon the runner user) - Optional:
EVO_RESTART_MODE(auto,systemd,tmux,compose,script,none) - Optional:
EVO_SYSTEMD_UNIT,EVO_TMUX_SESSION, health URLs, timeouts
- Confirm the runner user can read/write
EVO_DEPLOY_DIRand restart services (systemctl --useror 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):
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
Regression: SHA capture vs. git stdout
deploy.sh captures previousSha and targetSha via process substitution (read … < <(git_fetch_and_checkout)). Git commands such as reset --hard print progress to stdout (HEAD is now at …), which previously polluted that capture and produced invalid .evo-deploy/state.json (e.g. deployedSha: "is now at 84bc45e …", previousSha: "HEAD"). Rollback then became a no-op.
Fix: all git invocations inside git_fetch_and_checkout (and rollback) redirect stdout to stderr; only the final printf '%s %s\n' emits SHAs on stdout. write_state and rollback reject values that are not 7–40 hex characters.
- Before
git fetch, the script recordspreviousSha(currentHEAD). - After pull, migrate, and restart, it waits up to
EVO_HEALTH_TIMEOUT_SEC(default 120s) for:EVO_API_HEALTH_URL(defaulthttp://127.0.0.1:3100/health)EVO_WEB_HEALTH_URL(defaulthttp://127.0.0.1:5173/)
- On failure:
git reset --hardtopreviousSha, restart services again, exit non-zero. - Last outcome is written to
.evo-deploy/state.jsonunder the checkout.
Manual rollback to a known good SHA:
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:
- Preferred: rely on the
evo-deployworkflow (onceEVO_DEPLOY_ENABLED=true). No SSH step required. - Fallback (manual): SSH to Evo and run:
cd ~/Projects/evOo-local && bash ops/evo-deploy/deploy.sh - Verify: API
curl -fsS http://127.0.0.1:3100/health, webcurl -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.