openvibers/openrestream Commits

OpenRestream: the legacy deploy fallback is deleted; worker generations move to deploy/scripts/workers.sh (#35)

OP OpenVibers committed 10 Oct 2026 · 68f3c91744 · 7 files, +98 −177

* OpenRestream: the legacy deploy fallback is deleted; worker generations move to deploy/scripts/workers.sh

Plan T1 (the legacy deploy path). `deploy/scripts/deploy-legacy.sh`, the pre-ovhost deploy script, is deleted, along with the wrapper's fallback to it (`DEPLOY_LEGACY`, `OVHOST_LEGACY`, the "ovhost missing or too old" probe). `deploy.sh` runs `ovhost deploy|rollback|plan openre` and exits 1 without ovhost.

**Worker generations keep their tool.** ovhost never starts, stops or restarts a transport worker, so `deploy.sh workers|status|prune` always ran the legacy script. Those three commands move unchanged into `deploy/scripts/workers.sh`, and `deploy.sh` dispatches to it. `deploy.sh workers [<sha>]` still starts a new generation of all four workers and disables (never stops) the old ones so they drain. The cutover runbook and preflight guidance that name `deploy.sh workers` stay valid.

`test/deploy-wrapper.test.js` covers the ovhost calls, the missing-ovhost failure, the absence of the legacy path, and the dispatch to workers.sh. `ov test`: green.

**Note for the slot cutover (plan T4):** `scripts/migrate-from-live.js` and `cutover-preflight.js` still read a read-only SQLite snapshot of Live (`--live-db`). Live has been on PostgreSQL since 2026-10-08, so they need to read Live's PostgreSQL before the cutover can run. That is tracked separately.

* docs: workers/status/prune run deploy/scripts/workers.sh

Changes

ddiff --git a/.env.example b/.env.example
iindex a9f3a7a..3b8685a 100644
---- a/.env.example
++++ b/.env.example
@@@ -51,7 +51,7 @@ OPENRE_RTMP_PORT=1936
#OPENRE_RTMP_EXTRA_PORTS=1935
# Public listener address (default 0.0.0.0). 127.0.0.1 keeps ingest host-local (production until the
# cutover, docs/cutover.md step 5). A new value takes effect with the next worker generation
--# (deploy.sh workers), never by restarting a worker.
++# (deploy.sh workers, which runs deploy/scripts/workers.sh), never by restarting a worker.
#OPENRE_RTMP_BIND=0.0.0.0
# What encoders are told: rtmp://<host>[:<port>]/live
#OPENRE_RTMP_PUBLIC_HOST=ingest.openre.stream
ddiff --git a/README.md b/README.md
iindex 9783e15..40446a4 100644
---- a/README.md
++++ b/README.md
@@@ -185,11 +185,11 @@ sudo deploy/scripts/deploy.sh release origin/main # ovhost deploy openre --pre
sudo deploy/scripts/deploy.sh api [<sha>] # ovhost deploy openre [--to <sha>]: restarts openre-api + coordinator ONLY, rolls back if not ready
sudo deploy/scripts/deploy.sh rollback [<sha>] # ovhost rollback openre
sudo deploy/scripts/deploy.sh plan # ovhost plan openre (or DRY_RUN=1)
--sudo deploy/scripts/deploy.sh workers # deploy-legacy.sh: starts a new worker generation; old ones drain
--sudo deploy/scripts/deploy.sh status # deploy-legacy.sh
++sudo deploy/scripts/deploy.sh workers # workers.sh: starts a new worker generation; old ones drain
++sudo deploy/scripts/deploy.sh status # workers.sh
```
--- ovhost refuses an API restart while an ingest session is open (`--wait-idle` holds it, `--force` goes ahead), records every attempt (`ovhost releases openre`), prunes releases beyond five but never one a worker generation runs from, and announces the release. When ovhost is missing, too old or does not deploy OpenRestream with `release-layout`, the wrapper runs [deploy/scripts/deploy-legacy.sh](deploy/scripts/deploy-legacy.sh), the previous script, unchanged (`OVHOST_LEGACY=1` forces it). `workers`, `status` and `prune` always run it.
++- ovhost refuses an API restart while an ingest session is open (`--wait-idle` holds it, `--force` goes ahead), records every attempt (`ovhost releases openre`), prunes releases beyond five but never one a worker generation runs from, and announces the release. Without ovhost, `deploy.sh` exits 1 (there is no fallback); `workers`, `status` and `prune` run [workers.sh](deploy/scripts/workers.sh), because ovhost never starts, stops or restarts a transport worker.
- An **API deploy** never restarts a worker unit (no unit depends on another). Viewers of `openre.stream/play/…` reconnect; encoders, restreams and recordings do not notice.
- A **worker deploy** starts `openre-rtmp-ingest@<sha>`, `openre-restream-worker@<sha>` and `openre-jsmpeg@<sha>`; older instances are disabled (not stopped) and exit on their own when drained. Never `systemctl restart` a worker instance during a broadcast; `systemctl stop` starts a drain and waits up to 30 min (`TimeoutStopSec`), then kills.
ddiff --git a/STATUS.json b/STATUS.json
iindex 52b5737..f42a28d 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -29,7 +29,7 @@
"capabilitiesRegistered": true,
"livePatch": "applied in Live f0ca18b and deployed; Live wired on 2026-09-23 (OPENRE_URL and the openre.session.* subscription, docs/cutover.md phase A); every Live slot keeps ingest_authority=live until its per-slot cutover (phase B), so no broadcaster uses OpenRestream yet",
"deployed": true,
-- "deployment": "Deployed with `sudo ovhost deploy openre` (OpenVibe.Host strategy release-layout since the 2026-09-27 cutover; deploy/scripts/deploy.sh is the wrapper and keeps workers/status/prune): API + coordinator on 127.0.0.1:4500, RTMP ingest public on 0.0.0.0:1936 (ingest.openre.stream, DNS-only; reachable from outside since 2026-09-23), Live wired (OPENRE_URL, openre.session.* subscription), preflight all PASS, restore drill passed. No slot switched yet: every slot is still ingested by Live until the per-slot cutover (docs/cutover.md phase B).",
++ "deployment": "Deployed with `sudo ovhost deploy openre` (OpenVibe.Host strategy release-layout since the 2026-09-27 cutover; deploy/scripts/deploy.sh wraps deploy, rollback and plan, and hands workers/status/prune to deploy/scripts/workers.sh): API + coordinator on 127.0.0.1:4500, RTMP ingest public on 0.0.0.0:1936 (ingest.openre.stream, DNS-only; reachable from outside since 2026-09-23), Live wired (OPENRE_URL, openre.session.* subscription), preflight all PASS, restore drill passed. No slot switched yet: every slot is still ingested by Live until the per-slot cutover (docs/cutover.md phase B).",
"features": [
"stream definitions owned by a Network subject, with typed references",
"ingest keys hashed at rest, shown once, rotation with grace",
ddiff --git a/deploy/scripts/deploy-legacy.sh b/deploy/scripts/deploy-legacy.sh
ddeleted file mode 100755
iindex 8a7afaf..0000000
---- a/deploy/scripts/deploy-legacy.sh
++++ /dev/null
@@@ -1,110 +0,0 @@
--#!/usr/bin/env bash
--# ═══════════════════════════════════════════════════════════════
--# OpenRestream deploy (run on the host as root; the lead runs it, never CI).
--#
--# deploy/scripts/deploy.sh release [<git-ref>] check out <ref> (default origin/main) into
--# /opt/openre.stream/releases/<sha>, npm ci
--# deploy/scripts/deploy.sh api [<sha>] point `current` at a release, restart
--# openre-api + openre-session-coordinator.
--# NEVER touches a transport worker.
--# deploy/scripts/deploy.sh workers [<sha>] start a new generation of both workers from a
--# release; older generations drain and exit on
--# their own (sessions are not interrupted)
--# deploy/scripts/deploy.sh status units, generations, open sessions
--# deploy/scripts/deploy.sh prune remove releases no unit uses (keeps 5)
--#
--# Layout: /opt/openre.stream/releases/<sha>/ (full checkout + node_modules), `current` symlink
--# for the API and coordinator, one openre-rtmp-ingest@<sha> / openre-restream-worker@<sha>
--# instance per worker generation. Env: /etc/openvibe/openre.env. Store: ov_openre on PostgreSQL (ADR-035).
--# ═══════════════════════════════════════════════════════════════
--set -euo pipefail
--
--ROOT=/opt/openre.stream
--REPO=https://github.com/OpenVibers/OpenRestream.git
--API=http://127.0.0.1:4500
--NODE_BIN=${NODE_BIN:-node}
--
--say() { printf '[openre-deploy] %s\n' "$*" >&2; }
--die() { say "ERROR: $*"; exit 1; }
--
--latest_release() { ls -1t "$ROOT/releases" 2>/dev/null | head -1; }
--
--cmd_release() {
-- local ref=${1:-origin/main}
-- mkdir -p "$ROOT/releases" "$ROOT/repo"
-- if [ ! -d "$ROOT/repo/.git" ]; then git clone --quiet "$REPO" "$ROOT/repo"; fi
-- git -C "$ROOT/repo" fetch --quiet --tags origin
-- local sha
-- sha=$(git -C "$ROOT/repo" rev-parse --short=12 "$ref")
-- local dir="$ROOT/releases/$sha"
-- if [ -d "$dir" ]; then say "release $sha already exists"; echo "$sha"; return; fi
-- git -C "$ROOT/repo" worktree add --detach --quiet "$dir" "$sha"
-- (cd "$dir" && npm ci --omit=dev --no-audit --no-fund --quiet)
-- chown -R ubuntu:ubuntu "$dir"
-- say "release $sha ready at $dir"
-- echo "$sha"
--}
--
--wait_ready() {
-- for _ in $(seq 1 60); do
-- if curl -sf --max-time 2 "$API/api/ready" >/dev/null; then return 0; fi
-- sleep 1
-- done
-- return 1
--}
--
--cmd_api() {
-- local sha=${1:-$(latest_release)}
-- [ -n "$sha" ] && [ -d "$ROOT/releases/$sha" ] || die "no release $sha"
-- local prev
-- prev=$(readlink "$ROOT/current" 2>/dev/null || true)
-- ln -sfn "$ROOT/releases/$sha" "$ROOT/current.new" && mv -Tf "$ROOT/current.new" "$ROOT/current"
-- say "current -> $sha (was ${prev:-none}); restarting openre-api and openre-session-coordinator only"
-- systemctl restart openre-api.service
-- systemctl restart openre-session-coordinator.service
-- if ! wait_ready; then
-- say "openre-api not ready after 60 s; rolling back to ${prev:-nothing}"
-- if [ -n "$prev" ]; then
-- ln -sfn "$prev" "$ROOT/current.new" && mv -Tf "$ROOT/current.new" "$ROOT/current"
-- systemctl restart openre-api.service openre-session-coordinator.service
-- fi
-- exit 2
-- fi
-- say "openre-api ready on $sha"
--}
--
--cmd_workers() {
-- local sha=${1:-$(latest_release)}
-- [ -n "$sha" ] && [ -d "$ROOT/releases/$sha" ] || die "no release $sha"
-- say "starting worker generation from release $sha (older generations drain by themselves)"
-- systemctl enable --now "openre-rtmp-ingest@$sha.service" "openre-restream-worker@$sha.service" "openre-jsmpeg@$sha.service" "openre-webrtc@$sha.service"
-- # Disable (not stop) older instances so a reboot does not bring them back; they exit on their own.
-- for unit in $(systemctl list-units --plain --no-legend 'openre-rtmp-ingest@*' 'openre-restream-worker@*' 'openre-jsmpeg@*' 'openre-webrtc@*' | awk '{print $1}'); do
-- case "$unit" in *"@$sha.service") ;; *) systemctl disable "$unit" >/dev/null 2>&1 || true; say "draining: $unit";; esac
-- done
--}
--
--cmd_status() {
-- systemctl --no-pager --plain list-units 'openre-*' || true
-- curl -s --max-time 3 "$API/api/ready" | "$NODE_BIN" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const r=JSON.parse(s);console.log(JSON.stringify({status:r.status,workers:r.workers,coordinator:r.coordinator,events:r.events},null,2))}catch{console.log(s)}})' || true
--}
--
--cmd_prune() {
-- local keep used
-- used=$( (readlink "$ROOT/current"; systemctl list-units --plain --no-legend 'openre-*@*' | awk '{print $1}' | sed -E 's/.*@(.*)\.service/\1/') | xargs -n1 basename 2>/dev/null | sort -u)
-- keep=$(ls -1t "$ROOT/releases" | head -5)
-- for r in $(ls -1 "$ROOT/releases"); do
-- if echo "$used $keep" | grep -qw "$r"; then continue; fi
-- say "removing release $r"
-- git -C "$ROOT/repo" worktree remove --force "$ROOT/releases/$r"
-- done
--}
--
--case "${1:-}" in
-- release) shift; cmd_release "$@" ;;
-- api) shift; cmd_api "$@" ;;
-- workers) shift; cmd_workers "$@" ;;
-- status) cmd_status ;;
-- prune) cmd_prune ;;
-- *) sed -n '2,20p' "$0"; exit 1 ;;
--esac
ddiff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh
iindex ba3650a..3beb9ad 100755
---- a/deploy/scripts/deploy.sh
++++ b/deploy/scripts/deploy.sh
@@@ -12,23 +12,14 @@
# openre-session-coordinator ONLY, roll back if not ready
# deploy/scripts/deploy.sh rollback [<sha>] ovhost rollback openre [--to <sha>]
# deploy/scripts/deploy.sh plan (or DRY_RUN=1) ovhost plan openre
--# deploy/scripts/deploy.sh workers|status|prune deploy-legacy.sh, unchanged: ovhost never starts, stops or
--# restarts a worker unit (it prunes releases itself, never
--# one a worker generation runs from)
++# deploy/scripts/deploy.sh workers|status|prune deploy/scripts/workers.sh: ovhost never starts, stops or restarts
++# a transport worker, so worker generations are rolled there
# --wait-idle / --force after deploy, api or rollback are passed on (ingest sessions refuse an API restart).
--#
--# Fallback: deploy-legacy.sh (the previous script, unchanged) with the same arguments when ovhost is missing
--# or too old (no `capabilities`, deploy-api < 1), or the host inventory does not deploy openre with strategy
--# release-layout; OVHOST_LEGACY=1 forces it. There `deploy` is `release` then `api`, and `rollback <sha>` is
--# `api <sha>`.
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
SERVICE=openre
--STRATEGY=release-layout
ROOT="${OPENRE_ROOT:-/opt/openre.stream}"
--HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
--LEGACY="${DEPLOY_LEGACY:-$HERE/deploy-legacy.sh}"
OVHOST="${OVHOST:-/usr/local/bin/ovhost}"
if [ "${OVHOST_SUDO-auto}" = auto ]; then if [ "$(id -u)" -eq 0 ]; then SUDO=(); else SUDO=(sudo); fi; elif [ -n "${OVHOST_SUDO}" ]; then SUDO=("$OVHOST_SUDO"); else SUDO=(); fi
@@@ -47,37 +38,16 @@ for a in "$@"; do
esac
done
--legacy() {
-- say "$1 — running deploy-legacy.sh (the previous deploy script) instead"
-- case "$SUB" in
-- deploy) bash "$LEGACY" release ${REF:+"$REF"} >/dev/null; exec bash "$LEGACY" api ;;
-- rollback) [ -n "$REF" ] || { say "✗ deploy-legacy.sh needs the release: rollback <sha>"; exit 1; }; exec bash "$LEGACY" api "$REF" ;;
-- plan) say "✗ deploy-legacy.sh has no plan; see deploy-legacy.sh status"; exit 1 ;;
-- *) exec bash "$LEGACY" "$SUB" ${REF:+"$REF"} ;;
-- esac
--}
--
--REASON=""
--probe() {
-- if [ "${OVHOST_LEGACY:-0}" = 1 ]; then REASON="OVHOST_LEGACY=1"; return 1; fi
-- if ! command -v "$OVHOST" >/dev/null 2>&1; then REASON="ovhost not found ($OVHOST)"; return 1; fi
-- local caps api
-- if ! caps=$("${SUDO[@]}" "$OVHOST" capabilities "$SERVICE" 2>/dev/null); then REASON="this ovhost has no 'capabilities' (too old) or no inventory entry for $SERVICE"; return 1; fi
-- api=$(printf '%s\n' "$caps" | sed -n 's/^deploy-api=//p')
-- case "$api" in ''|*[!0-9]*) REASON="this ovhost reports no deploy-api (too old)"; return 1 ;; esac
-- if [ "$api" -lt 1 ]; then REASON="this ovhost's deploy-api is $api, 1 is needed"; return 1; fi
-- if ! printf '%s\n' "$caps" | grep -qx "strategy=$STRATEGY"; then REASON="the host inventory does not deploy $SERVICE with strategy $STRATEGY ($(printf '%s\n' "$caps" | sed -n 's/^strategy=//p'))"; return 1; fi
-- if ! printf '%s\n' "$caps" | grep -qx "managed=yes"; then REASON="ovhost does not manage $SERVICE"; return 1; fi
-- return 0
--}
--
case "$SUB" in
-- workers|status|prune) exec bash "$LEGACY" "$SUB" ${REF:+"$REF"} ;;
deploy|release|api|rollback|plan) ;;
-- *) sed -n '2,24p' "$0"; exit 1 ;;
++ workers|status|prune) exec bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/workers.sh" "$SUB" ${REF:+"$REF"} ;;
++ *) say "unknown command: $SUB (expected deploy, release, api, rollback, plan, workers, status or prune)"; exit 1 ;;
esac
--probe || legacy "$REASON"
++if ! command -v "$OVHOST" >/dev/null 2>&1; then
++ say "ERROR: ovhost not found ($OVHOST); install ovhost before deploying"
++ exit 1
++fi
TO=()
[ -n "$REF" ] && TO=(--to "$REF")
ddiff --git a/deploy/scripts/workers.sh b/deploy/scripts/workers.sh
nnew file mode 100755
iindex 0000000..1c200e5
---- /dev/null
++++ b/deploy/scripts/workers.sh
@@@ -0,0 +1,63 @@
++#!/usr/bin/env bash
++# ═══════════════════════════════════════════════════════════════
++# OpenRestream transport workers (run on the host as root; `deploy/scripts/deploy.sh workers|status|prune` calls this).
++# `ovhost deploy openre` makes releases and restarts the API and coordinator; it never starts, stops or restarts a
++# transport worker, because an ingest or restream session must not be cut. Worker generations are rolled here:
++#
++# workers.sh workers [<sha>] start a new generation of every worker from a release (default: the newest); older
++# generations are disabled (not stopped) and drain and exit on their own
++# workers.sh status units, generations, open sessions
++# workers.sh prune remove releases nothing uses (keeps the newest 5, `current` and every worker's)
++#
++# Layout: /opt/openre.stream/releases/<sha>/, `current` for the API and coordinator, one openre-rtmp-ingest@<sha> /
++# openre-restream-worker@<sha> / openre-jsmpeg@<sha> / openre-webrtc@<sha> instance per worker generation.
++# ═══════════════════════════════════════════════════════════════
++set -euo pipefail
++
++ROOT=${OPENRE_ROOT:-/opt/openre.stream}
++API=${OPENRE_API:-http://127.0.0.1:4500}
++NODE_BIN=${NODE_BIN:-node}
++UNITS=(openre-rtmp-ingest openre-restream-worker openre-jsmpeg openre-webrtc)
++
++say() { printf '[openre-workers] %s\n' "$*" >&2; }
++die() { say "ERROR: $*"; exit 1; }
++
++latest_release() { ls -1t "$ROOT/releases" 2>/dev/null | head -1; }
++
++cmd_workers() {
++ local sha=${1:-$(latest_release)}
++ [ -n "$sha" ] && [ -d "$ROOT/releases/$sha" ] || die "no release $sha"
++ say "starting worker generation from release $sha (older generations drain by themselves)"
++ local u units=()
++ for u in "${UNITS[@]}"; do units+=("$u@$sha.service"); done
++ systemctl enable --now "${units[@]}"
++ # Disable (not stop) older instances so a reboot does not bring them back; they exit on their own.
++ local patterns=()
++ for u in "${UNITS[@]}"; do patterns+=("$u@*"); done
++ for unit in $(systemctl list-units --plain --no-legend "${patterns[@]}" | awk '{print $1}'); do
++ case "$unit" in *"@$sha.service") ;; *) systemctl disable "$unit" >/dev/null 2>&1 || true; say "draining: $unit";; esac
++ done
++}
++
++cmd_status() {
++ systemctl --no-pager --plain list-units 'openre-*' || true
++ curl -s --max-time 3 "$API/api/ready" | "$NODE_BIN" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const r=JSON.parse(s);console.log(JSON.stringify({status:r.status,workers:r.workers,coordinator:r.coordinator,events:r.events},null,2))}catch{console.log(s)}})' || true
++}
++
++cmd_prune() {
++ local keep used r
++ used=$( (readlink "$ROOT/current"; systemctl list-units --plain --no-legend 'openre-*@*' | awk '{print $1}' | sed -E 's/.*@(.*)\.service/\1/') | xargs -n1 basename 2>/dev/null | sort -u)
++ keep=$(ls -1t "$ROOT/releases" | head -5)
++ for r in $(ls -1 "$ROOT/releases"); do
++ if echo "$used $keep" | grep -qw "$r"; then continue; fi
++ say "removing release $r"
++ git -C "$ROOT/repo" worktree remove --force "$ROOT/releases/$r"
++ done
++}
++
++case "${1:-}" in
++ workers) shift; cmd_workers "$@" ;;
++ status) cmd_status ;;
++ prune) cmd_prune ;;
++ *) sed -n '2,14p' "$0"; exit 1 ;;
++esac
ddiff --git a/test/deploy-wrapper.test.js b/test/deploy-wrapper.test.js
iindex fd33a3b..a695900 100644
---- a/test/deploy-wrapper.test.js
++++ b/test/deploy-wrapper.test.js
@@@ -1,12 +1,11 @@
'use strict';
/**
* deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy openre` (OpenVibe.Host, strategy
-- * release-layout; roadmap WS-N task 11) with deploy-legacy.sh (the previous script, unchanged) as its
-- * fallback. A fake ovhost records what the wrapper asks for; a fake legacy script records the fallback.
++ * release-layout; roadmap WS-N task 11). A fake ovhost records what the wrapper asks for.
* - deploy → ovhost deploy openre; release [<ref>] → --prepare-only, and the sha12 alone on stdout;
* api [<sha>] → deploy --to <sha>; rollback → ovhost rollback; plan / DRY_RUN=1 → ovhost plan;
-- * - workers, status and prune always run the legacy script (ovhost never touches a worker unit);
-- * - the fallback: deploy is release then api, rollback <sha> is api <sha>.
++ * - a missing ovhost fails with a clear error;
++ * - workers|status|prune go to deploy/scripts/workers.sh, never to ovhost.
*/
const assert = require('assert');
const fs = require('fs');
@@@ -26,21 +25,13 @@ const sha12 = sh(`git -C "${root}/repo" rev-parse --short=12 HEAD`).trim();
const ovhost = path.join(tmp, 'ovhost');
fs.writeFileSync(ovhost, `#!/usr/bin/env bash
--if [ "$1" = capabilities ]; then
-- [ -n "$FAKE_OLD" ] && exit 1
-- printf '%b\\n' "\${FAKE_CAPS:-ovhost=0.3.0\\ndeploy-api=1\\nservice=openre\\nstrategy=release-layout\\nmanaged=yes\\nlayout=release}"
-- exit 0
--fi
echo "ovhost $*" >> "${log}"
echo "[ovhost] progress output"
exit "\${FAKE_EXIT:-0}"
`, { mode: 0o755 });
--const legacy = path.join(tmp, 'legacy.sh');
--fs.writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy $*" >> "${log}"\n[ "$1" = release ] && echo "${sha12}"\nexit 0\n`, { mode: 0o755 });
--
function run(args = [], env = {}) {
fs.rmSync(log, { force: true });
-- const r = spawnSync('bash', [WRAPPER, ...args], { env: { PATH: process.env.PATH, HOME: tmp, OVHOST: ovhost, OVHOST_SUDO: '', DEPLOY_LEGACY: legacy, OPENRE_ROOT: root, ...env }, encoding: 'utf8' });
++ const r = spawnSync('bash', [WRAPPER, ...args], { env: { PATH: process.env.PATH, HOME: tmp, OVHOST: ovhost, OVHOST_SUDO: '', OPENRE_ROOT: root, ...env }, encoding: 'utf8' });
let calls = [];
try { calls = fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean); } catch { /* none */ }
return { code: r.status, stdout: r.stdout, out: r.stdout + r.stderr, calls };
@@@ -59,20 +50,27 @@ assert.deepStrictEqual(run(['rollback', sha12]).calls, [`ovhost rollback openre
assert.deepStrictEqual(run(['plan']).calls, ['ovhost plan openre']);
assert.deepStrictEqual(run([], { DRY_RUN: '1' }).calls, ['ovhost plan openre']);
assert.strictEqual(run(['api'], { FAKE_EXIT: '5' }).code, 5, "ovhost's exit code is the wrapper's (5: ingest sessions)");
--for (const sub of ['workers', 'status', 'prune']) assert.deepStrictEqual(run([sub]).calls, [`legacy ${sub}`], `${sub}: always the legacy script`);
--assert.deepStrictEqual(run(['workers', sha12]).calls, [`legacy workers ${sha12}`]);
++// Worker generations are rolled by deploy/scripts/workers.sh (ovhost never touches a transport worker): the
++// wrapper hands workers|status|prune to it and never calls ovhost for them.
++r = run(['workers', 'nosuch']);
++assert.strictEqual(r.code, 1);
++assert.deepStrictEqual(r.calls, [], 'workers is not an ovhost command');
++assert.match(r.out, /\[openre-workers\] ERROR: no release nosuch/, 'workers.sh answered');
++assert.match(fs.readFileSync(WRAPPER, 'utf8'), /workers\|status\|prune\) exec bash .*workers\.sh/);
++const workersSource = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'scripts', 'workers.sh'), 'utf8');
++assert.match(workersSource, /systemctl enable --now/, 'workers.sh starts the new generation');
++assert.match(workersSource, /systemctl disable "\$unit"/, 'and disables (never stops) older ones');
assert.strictEqual(run(['nope']).code, 1);
assert.strictEqual(run(['api', '--bogus']).code, 1);
--// Fallback.
--r = run([], { FAKE_OLD: '1' });
--assert.deepStrictEqual(r.calls, ['legacy release', 'legacy api'], 'deploy is release then api');
--assert.match(r.out, /too old/);
--assert.deepStrictEqual(run(['release', 'origin/main'], { OVHOST_LEGACY: '1' }).calls, ['legacy release origin/main']);
--assert.deepStrictEqual(run(['api', sha12], { OVHOST: path.join(tmp, 'missing') }).calls, [`legacy api ${sha12}`]);
--assert.deepStrictEqual(run(['rollback', sha12], { FAKE_CAPS: 'deploy-api=1\\nstrategy=none\\nmanaged=no' }).calls, [`legacy api ${sha12}`]);
--assert.strictEqual(run(['rollback'], { OVHOST_LEGACY: '1' }).code, 1, 'the legacy script needs the release to go back to');
--assert.match(fs.readFileSync(WRAPPER, 'utf8'), /^set -euo pipefail$/m);
++r = run(['api', sha12], { OVHOST: path.join(tmp, 'missing') });
++assert.strictEqual(r.code, 1);
++assert.deepStrictEqual(r.calls, []);
++assert.match(r.out, /ovhost not found/);
++assert.deepStrictEqual(run([], { FAKE_EXIT: '7' }).calls, ['ovhost deploy openre']);
++const wrapperSource = fs.readFileSync(WRAPPER, 'utf8');
++assert.match(wrapperSource, /^set -euo pipefail$/m);
++assert.doesNotMatch(wrapperSource, /deploy-legacy|DEPLOY_LEGACY|OVHOST_LEGACY|capabilities/);
fs.rmSync(tmp, { recursive: true, force: true });
console.log('deploy wrapper: all checks passed');