fix(evo-deploy): prefer systemd over compose in auto restart mode (#130)

In auto restart mode, prefer an active evoo-local user unit over compose so a missing POSTGRES_PASSWORD does not roll the deploy back.
This commit is contained in:
cynarAI 2026-10-04 15:34:40 +02:00 committed by GitHub
parent f395ed9a23
commit e928600c98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 4 deletions

View File

@ -24,7 +24,8 @@ No SSH keys or deploy secrets are stored in this repo. The workflow runs **on**
- `EVO_DEPLOY_ENABLED` = `true` - `EVO_DEPLOY_ENABLED` = `true`
- Optional: `EVO_DEPLOY_DIR` (default `$HOME/Projects/evOo-local` of the runner user) - Optional: `EVO_DEPLOY_DIR` (default `$HOME/Projects/evOo-local` of the runner user)
- Optional: `EVO_RESTART_MODE` (`auto`, `systemd`, `tmux`, `compose`, `script`, `none`) - Optional: `EVO_RESTART_MODE` (`auto`, `systemd`, `tmux`, `compose`, `script`, `none`)
- Optional: `EVO_SYSTEMD_UNIT`, `EVO_TMUX_SESSION`, health URLs, timeouts - **Evo (recommended):** pin `EVO_RESTART_MODE=systemd`, `EVO_SYSTEMD_UNIT=evoo-local.service`, and `EVO_SYSTEMD_SCOPE=user` as repo variables (or the same values in the runner host env). Auto-detect prefers systemd when that unit is active or enabled, but pinning avoids ambiguity if `infra/compose/docker-compose.yml` is present and Compose env vars are not configured for production-like deploys.
- Optional: `EVO_TMUX_SESSION`, health URLs, timeouts
3. Confirm the runner user can read/write `EVO_DEPLOY_DIR` and restart services (`systemctl --user` or tmux). 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). Until `EVO_DEPLOY_ENABLED` is `true`, pushes to `main` do not deploy (workflow job is skipped).
@ -161,8 +162,9 @@ Do not commit host passwords, tokens, or SSH private keys. Configure restart tar
| `EVO_DEPLOY_DIR` | `$HOME/Projects/evOo-local` | Git checkout root | | `EVO_DEPLOY_DIR` | `$HOME/Projects/evOo-local` | Git checkout root |
| `EVO_DEPLOY_REF` | `main` | Branch name or commit SHA | | `EVO_DEPLOY_REF` | `main` | Branch name or commit SHA |
| `EVO_RESTART_MODE` | `auto` | `systemd` / `tmux` / `compose` / `script` / `none` | | `EVO_RESTART_MODE` | `auto` | `systemd` / `tmux` / `compose` / `script` / `none` |
| `EVO_SYSTEMD_UNIT` | _(empty)_ | Required for `systemd` mode | | `EVO_SYSTEMD_UNIT` | _(empty)_ | Required for explicit `systemd` mode; auto-detect sets this when a unit is found |
| `EVO_SYSTEMD_SCOPE` | `user` | `user` or `system` | | `EVO_SYSTEMD_SCOPE` | `user` | `user` or `system` (auto-detect picks the scope where the unit is active/enabled) |
| `EVO_SYSTEMD_AUTO_UNIT` | `evoo-local.service` | Unit name probed when `EVO_RESTART_MODE=auto` and `EVO_SYSTEMD_UNIT` is unset |
| `EVO_TMUX_SESSION` | `evoo` | tmux session name | | `EVO_TMUX_SESSION` | `evoo` | tmux session name |
| `EVO_TMUX_COMMAND` | `pnpm dev` | Command started in new tmux session | | `EVO_TMUX_COMMAND` | `pnpm dev` | Command started in new tmux session |
| `EVO_MIGRATE` | `true` | Run `pnpm db:generate` + `pnpm db:migrate` (`--skip-migrate`) | | `EVO_MIGRATE` | `true` | Run `pnpm db:generate` + `pnpm db:migrate` (`--skip-migrate`) |
@ -178,4 +180,12 @@ Do not commit host passwords, tokens, or SSH private keys. Configure restart tar
|----------|---------|---------| |----------|---------|---------|
| `EVO_WEB_MODE` | `dev` | `dev` (Vite dev server) or `preview` (production build) | | `EVO_WEB_MODE` | `dev` | `dev` (Vite dev server) or `preview` (production build) |
If `EVO_RESTART_MODE=auto` cannot detect systemd, tmux, compose, or a restart script, it falls back to `none` and only health-checks — set `EVO_RESTART_MODE` and the matching unit/session explicitly. When `EVO_RESTART_MODE=auto`, detection order is:
1. **systemd** — `EVO_SYSTEMD_UNIT` is set, or `EVO_SYSTEMD_AUTO_UNIT` (default `evoo-local.service`) is active, enabled, or loaded under `systemctl --user` then `systemctl` (system scope).
2. **script** — `EVO_RESTART_SCRIPT` exists and is executable.
3. **tmux** — session `EVO_TMUX_SESSION` exists.
4. **compose** — `EVO_COMPOSE_FILE` exists under the checkout.
5. **none** — skip restart; health-check only.
If auto mode picks the wrong path (for example Compose on Evo because the compose file exists but the stack runs under systemd), pin `EVO_RESTART_MODE=systemd` and `EVO_SYSTEMD_UNIT=evoo-local.service` in repo variables or host env.

View File

@ -11,6 +11,7 @@ EVO_GIT_REMOTE="${EVO_GIT_REMOTE:-origin}"
EVO_RESTART_MODE="${EVO_RESTART_MODE:-auto}" # auto | systemd | tmux | compose | script | none EVO_RESTART_MODE="${EVO_RESTART_MODE:-auto}" # auto | systemd | tmux | compose | script | none
EVO_SYSTEMD_UNIT="${EVO_SYSTEMD_UNIT:-}" EVO_SYSTEMD_UNIT="${EVO_SYSTEMD_UNIT:-}"
EVO_SYSTEMD_SCOPE="${EVO_SYSTEMD_SCOPE:-user}" # user | system EVO_SYSTEMD_SCOPE="${EVO_SYSTEMD_SCOPE:-user}" # user | system
EVO_SYSTEMD_AUTO_UNIT="${EVO_SYSTEMD_AUTO_UNIT:-evoo-local.service}"
EVO_TMUX_SESSION="${EVO_TMUX_SESSION:-evoo}" EVO_TMUX_SESSION="${EVO_TMUX_SESSION:-evoo}"
EVO_TMUX_COMMAND="${EVO_TMUX_COMMAND:-pnpm dev}" EVO_TMUX_COMMAND="${EVO_TMUX_COMMAND:-pnpm dev}"
EVO_COMPOSE_FILE="${EVO_COMPOSE_FILE:-infra/compose/docker-compose.yml}" EVO_COMPOSE_FILE="${EVO_COMPOSE_FILE:-infra/compose/docker-compose.yml}"
@ -105,6 +106,45 @@ validate_deploy_dir() {
[[ -d "$EVO_DEPLOY_DIR/.git" ]] || die "EVO_DEPLOY_DIR is not a git checkout: $EVO_DEPLOY_DIR" [[ -d "$EVO_DEPLOY_DIR/.git" ]] || die "EVO_DEPLOY_DIR is not a git checkout: $EVO_DEPLOY_DIR"
} }
systemd_unit_usable() {
local unit="$1"
local scope="$2"
command -v systemctl >/dev/null 2>&1 || return 1
local -a ctl=(systemctl)
if [[ "$scope" == "user" ]]; then
ctl=(systemctl --user)
fi
if "${ctl[@]}" is-active --quiet "$unit" 2>/dev/null; then
return 0
fi
if "${ctl[@]}" is-enabled --quiet "$unit" 2>/dev/null; then
return 0
fi
local load_state
load_state="$("${ctl[@]}" show -p LoadState --value "$unit" 2>/dev/null || true)"
[[ "$load_state" == "loaded" ]]
}
# When EVO_RESTART_MODE=auto and EVO_SYSTEMD_UNIT is unset, pick a unit that is active or installed.
auto_detect_systemd() {
local unit="${EVO_SYSTEMD_UNIT:-$EVO_SYSTEMD_AUTO_UNIT}"
local scope
for scope in user system; do
if systemd_unit_usable "$unit" "$scope"; then
EVO_SYSTEMD_UNIT="$unit"
EVO_SYSTEMD_SCOPE="$scope"
return 0
fi
done
return 1
}
# Side effects must run outside $(resolve_restart_mode) — command substitution uses a subshell.
prepare_auto_restart_mode() {
[[ "$EVO_RESTART_MODE" == "auto" && -z "$EVO_SYSTEMD_UNIT" ]] || return 0
auto_detect_systemd || true
}
resolve_restart_mode() { resolve_restart_mode() {
if [[ "$EVO_RESTART_MODE" != "auto" ]]; then if [[ "$EVO_RESTART_MODE" != "auto" ]]; then
printf '%s\n' "$EVO_RESTART_MODE" printf '%s\n' "$EVO_RESTART_MODE"
@ -173,6 +213,7 @@ restart_script() {
} }
restart_services() { restart_services() {
prepare_auto_restart_mode
local mode local mode
mode="$(resolve_restart_mode)" mode="$(resolve_restart_mode)"
case "$mode" in case "$mode" in
@ -349,6 +390,7 @@ main() {
parse_args "$@" parse_args "$@"
validate_deploy_dir validate_deploy_dir
prepare_auto_restart_mode
local resolved_mode local resolved_mode
resolved_mode="$(resolve_restart_mode)" resolved_mode="$(resolve_restart_mode)"
log "deploy dir: $EVO_DEPLOY_DIR" log "deploy dir: $EVO_DEPLOY_DIR"

View File

@ -235,6 +235,58 @@ describe("evo deploy", () => {
expect(f.served()).toBe("built A"); expect(f.served()).toBe("built A");
expect(existsSync(path.join(f.checkout, ".evo-deploy/state.json"))).toBe(false); expect(existsSync(path.join(f.checkout, ".evo-deploy/state.json"))).toBe(false);
}); });
it("auto mode prefers systemd over compose when the default unit is active", () => {
const root = temporaryRoot("evo-deploy-autodetect-");
const checkout = path.join(root, "checkout");
const bin = path.join(root, "bin");
const home = path.join(root, "home");
const env = {
HOME: home,
PATH: searchPath(bin),
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
GIT_CONFIG_NOSYSTEM: "1",
};
write(
path.join(bin, "systemctl"),
`#!/bin/bash
case "$*" in
"--user is-active --quiet evoo-local.service") exit 0 ;;
"--user is-enabled --quiet evoo-local.service") exit 1 ;;
"--user show -p LoadState --value evoo-local.service") echo loaded; exit 0 ;;
esac
exit 1
`,
0o755,
);
write(path.join(home, ".bash_profile"), `export PATH="${bin}:$PATH"\n`);
mkdirSync(checkout, { recursive: true });
execFileSync("git", ["init", "-q", "-b", "main"], { cwd: checkout, env });
write(path.join(checkout, "package.json"), "{}");
execFileSync("git", ["add", "-A"], { cwd: checkout, env });
execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: checkout, env });
execFileSync("git", ["remote", "add", "origin", checkout], { cwd: checkout, env });
write(path.join(checkout, "infra/compose/docker-compose.yml"), "services: {}\n");
const result = spawnSync("/bin/bash", [deployScript, "--dry-run"], {
env: {
...env,
EVO_DEPLOY_DIR: checkout,
EVO_RESTART_MODE: "auto",
EVO_HEALTH_TIMEOUT_SEC: "1",
},
encoding: "utf8",
timeout: 30_000,
});
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
expect(result.stderr).toContain("restart mode: systemd");
expect(result.stderr).not.toContain("restart mode: compose");
});
}); });
/** A checkout layout around a copy of start-stack.sh, with the real tsx and vite installs. */ /** A checkout layout around a copy of start-stack.sh, with the real tsx and vite installs. */