fix(evo-deploy): validate restart config before mutating checkout (#167)
Move restart-mode prerequisites (systemd unit default, tmux, compose file, script hook, EVO_SYSTEMD_SCOPE) ahead of git fetch/install/build/migrate. Explicit systemd with no EVO_SYSTEMD_UNIT falls back to EVO_SYSTEMD_AUTO_UNIT. die() after checkout triggers rollback; rollback marks in-progress to avoid loops. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
eb9c8847a4
commit
9062717eee
@ -111,7 +111,8 @@ bash ops/evo-deploy/deploy.sh
|
||||
# Pin a SHA
|
||||
EVO_DEPLOY_REF=abc1234 bash ops/evo-deploy/deploy.sh
|
||||
|
||||
# Force systemd restart
|
||||
# Force systemd restart (unit optional — defaults to evoo-local.service)
|
||||
EVO_RESTART_MODE=systemd bash ops/evo-deploy/deploy.sh
|
||||
EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service bash ops/evo-deploy/deploy.sh
|
||||
```
|
||||
|
||||
@ -119,6 +120,8 @@ GitHub: **Actions → evo-deploy → Run workflow** (optional `ref`, `dry_run`,
|
||||
|
||||
## Deploy and rollback behavior
|
||||
|
||||
Restart mode and its prerequisites (systemd unit name, tmux, compose file, restart script, valid `EVO_SYSTEMD_SCOPE`) are validated **before** `git fetch`, install, build, migrate, or web activation. A bad configuration exits immediately without changing the checkout or database.
|
||||
|
||||
1. Before `git fetch`, the script records `previousSha` (current `HEAD`), then resets the checkout to the target.
|
||||
2. `pnpm install --frozen-lockfile`, then the web build into `.evo-deploy/web-dist`. Its output goes to `.evo-deploy/web-build.log` and is printed only when the build fails.
|
||||
3. `pnpm db:generate && pnpm db:migrate`.
|
||||
@ -162,7 +165,7 @@ 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_REF` | `main` | Branch name or commit SHA |
|
||||
| `EVO_RESTART_MODE` | `auto` | `systemd` / `tmux` / `compose` / `script` / `none` |
|
||||
| `EVO_SYSTEMD_UNIT` | _(empty)_ | Required for explicit `systemd` mode; auto-detect sets this when a unit is found |
|
||||
| `EVO_SYSTEMD_UNIT` | _(empty)_ | Systemd unit to restart; when `EVO_RESTART_MODE=systemd` and unset, defaults to `EVO_SYSTEMD_AUTO_UNIT` (same as auto mode). Auto mode sets this when a unit is found |
|
||||
| `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 |
|
||||
|
||||
@ -30,7 +30,14 @@ 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; }
|
||||
die() {
|
||||
log "ERROR: $*"
|
||||
if [[ "${EVO_IN_ROLLBACK:-}" != true && "${EVO_CHECKOUT_DONE:-}" == true && -n "${EVO_PREVIOUS_SHA:-}" ]]; then
|
||||
EVO_IN_ROLLBACK=true
|
||||
rollback "$EVO_PREVIOUS_SHA"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
is_valid_git_sha() {
|
||||
[[ "$1" =~ ^[0-9a-fA-F]{7,40}$ ]]
|
||||
@ -182,13 +189,11 @@ systemctl_cmd() {
|
||||
}
|
||||
|
||||
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"
|
||||
@ -198,7 +203,6 @@ restart_tmux() {
|
||||
|
||||
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")
|
||||
@ -206,8 +210,6 @@ restart_compose() {
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
@ -230,6 +232,54 @@ restart_services() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Fail before git fetch/checkout or install when restart settings are unusable.
|
||||
# Must not run inside command substitution — it sets EVO_SYSTEMD_UNIT and related env.
|
||||
validate_restart_config() {
|
||||
case "$EVO_RESTART_MODE" in
|
||||
auto | systemd | tmux | compose | script | none) ;;
|
||||
*)
|
||||
die "invalid EVO_RESTART_MODE: $EVO_RESTART_MODE (expected auto, systemd, tmux, compose, script, or none)"
|
||||
;;
|
||||
esac
|
||||
case "$EVO_SYSTEMD_SCOPE" in
|
||||
user | system) ;;
|
||||
*)
|
||||
die "invalid EVO_SYSTEMD_SCOPE: $EVO_SYSTEMD_SCOPE (expected user or system)"
|
||||
;;
|
||||
esac
|
||||
|
||||
prepare_auto_restart_mode
|
||||
if [[ "$EVO_RESTART_MODE" == "systemd" && -z "$EVO_SYSTEMD_UNIT" ]]; then
|
||||
EVO_SYSTEMD_UNIT="$EVO_SYSTEMD_AUTO_UNIT"
|
||||
log "EVO_SYSTEMD_UNIT unset — using $EVO_SYSTEMD_UNIT (same default as auto mode)"
|
||||
fi
|
||||
|
||||
local mode
|
||||
mode="$(resolve_restart_mode)"
|
||||
case "$mode" in
|
||||
systemd)
|
||||
[[ -n "$EVO_SYSTEMD_UNIT" ]] || die "EVO_RESTART_MODE=systemd requires EVO_SYSTEMD_UNIT or EVO_SYSTEMD_AUTO_UNIT"
|
||||
;;
|
||||
tmux)
|
||||
command -v tmux >/dev/null 2>&1 || die "tmux not found; set EVO_RESTART_MODE=systemd or install tmux"
|
||||
;;
|
||||
compose)
|
||||
[[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE" ]] ||
|
||||
die "compose file not found: $EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE"
|
||||
;;
|
||||
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"
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
die "unsupported restart mode: $mode"
|
||||
;;
|
||||
esac
|
||||
EVO_RESOLVED_RESTART_MODE="$mode"
|
||||
}
|
||||
|
||||
wait_for_url() {
|
||||
local url="$1"
|
||||
local label="$2"
|
||||
@ -368,6 +418,7 @@ activate_web_build() {
|
||||
# Restores the previous commit with its dependencies, client and web build, restarts and
|
||||
# health-checks it. Every step is attempted so the service comes back even if one of them fails.
|
||||
rollback() {
|
||||
EVO_IN_ROLLBACK=true
|
||||
local previous_sha="$1" status="rolled-back"
|
||||
is_valid_git_sha "$previous_sha" || die "rollback aborted: invalid previous_sha: $previous_sha"
|
||||
log "ROLLBACK: resetting to $previous_sha"
|
||||
@ -393,9 +444,8 @@ main() {
|
||||
parse_args "$@"
|
||||
validate_deploy_dir
|
||||
|
||||
prepare_auto_restart_mode
|
||||
local resolved_mode
|
||||
resolved_mode="$(resolve_restart_mode)"
|
||||
validate_restart_config
|
||||
local resolved_mode="$EVO_RESOLVED_RESTART_MODE"
|
||||
log "deploy dir: $EVO_DEPLOY_DIR"
|
||||
log "target ref: $EVO_DEPLOY_REF"
|
||||
log "restart mode: $resolved_mode"
|
||||
@ -403,6 +453,8 @@ main() {
|
||||
|
||||
local previous_sha target_sha
|
||||
read -r previous_sha target_sha < <(git_fetch_and_checkout)
|
||||
EVO_CHECKOUT_DONE=true
|
||||
EVO_PREVIOUS_SHA="$previous_sha"
|
||||
is_valid_git_sha "$previous_sha" || die "invalid previous_sha captured from checkout: $previous_sha"
|
||||
is_valid_git_sha "$target_sha" || die "invalid target_sha captured from checkout: $target_sha"
|
||||
|
||||
|
||||
@ -236,6 +236,71 @@ describe("evo deploy", () => {
|
||||
expect(existsSync(path.join(f.checkout, ".evo-deploy/state.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects script mode without EVO_RESTART_SCRIPT before mutating the checkout", () => {
|
||||
const f = deployFixture({});
|
||||
const root = path.dirname(f.checkout);
|
||||
const bin = path.join(root, "bin");
|
||||
const home = path.join(root, "home");
|
||||
const result = spawnSync("/bin/bash", [deployScript], {
|
||||
env: {
|
||||
HOME: home,
|
||||
PATH: searchPath(bin),
|
||||
GIT_CONFIG_NOSYSTEM: "1",
|
||||
EVO_DEPLOY_DIR: f.checkout,
|
||||
EVO_DEPLOY_REF: f.b,
|
||||
EVO_RESTART_MODE: "script",
|
||||
EVO_HEALTH_TIMEOUT_SEC: "1",
|
||||
},
|
||||
encoding: "utf8",
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("EVO_RESTART_MODE=script requires EVO_RESTART_SCRIPT");
|
||||
expect(f.commands()).toEqual([]);
|
||||
expect(f.head()).toBe(f.a);
|
||||
expect(existsSync(path.join(f.checkout, ".evo-deploy/state.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("systemd mode without EVO_SYSTEMD_UNIT uses the auto unit name before deploy", () => {
|
||||
const f = deployFixture({});
|
||||
const root = path.dirname(f.checkout);
|
||||
const bin = path.join(root, "bin");
|
||||
const home = path.join(root, "home");
|
||||
write(
|
||||
path.join(bin, "systemctl"),
|
||||
`#!/bin/bash
|
||||
case "$*" in
|
||||
"--user restart evoo-local.service") exit 0 ;;
|
||||
esac
|
||||
exit 1
|
||||
`,
|
||||
0o755,
|
||||
);
|
||||
write(path.join(home, ".bash_profile"), `export PATH="${bin}:$PATH"\n`);
|
||||
const result = spawnSync("/bin/bash", [deployScript], {
|
||||
env: {
|
||||
HOME: home,
|
||||
PATH: searchPath(bin),
|
||||
COMMAND_LOG: path.join(root, "commands.log"),
|
||||
GIT_CONFIG_NOSYSTEM: "1",
|
||||
EVO_DEPLOY_DIR: f.checkout,
|
||||
EVO_DEPLOY_REF: f.b,
|
||||
EVO_RESTART_MODE: "systemd",
|
||||
EVO_SYSTEMD_SCOPE: "user",
|
||||
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("EVO_SYSTEMD_UNIT unset — using evoo-local.service");
|
||||
expect(result.stderr).toContain("restart mode: systemd");
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user