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:
parent
f395ed9a23
commit
e928600c98
@ -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.
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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. */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user