openvibers/openrestream Commits
OpenRestream: the legacy deploy fallback is deleted; worker generations move to deploy/scripts/workers.sh (#35)
Changes
@@@ -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@@@ -185,11 +185,11 @@ sudo deploy/scripts/deploy.sh release origin/main # ovhost deploy openre --presudo deploy/scripts/deploy.sh api [<sha>] # ovhost deploy openre [--to <sha>]: restarts openre-api + coordinator ONLY, rolls back if not readysudo deploy/scripts/deploy.sh rollback [<sha>] # ovhost rollback openresudo 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.@@@ -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",ddeleted file mode 100755@@@ -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@@@ -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 pipefailSERVICE=openre--STRATEGY=release-layoutROOT="${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 "$@"; doesacdone--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++fiTO=()[ -n "$REF" ] && TO=(--to "$REF")nnew file mode 100755@@@ -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@@@ -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--fiecho "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 openreassert.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');
@@ -51,7 +51,7 @@ OPENRE_RTMP_PORT=1936
@@ -51,7 +51,7 @@ OPENRE_RTMP_PORT=1936
#OPENRE_RTMP_EXTRA_PORTS=1935
#OPENRE_RTMP_EXTRA_PORTS=1935
# Public listener address (default 0.0.0.0). 127.0.0.1 keeps ingest host-local (production until the
# 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
# 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
#OPENRE_RTMP_BIND=0.0.0.0
# What encoders are told: rtmp://<host>[:<port>]/live
# What encoders are told: rtmp://<host>[:<port>]/live
#OPENRE_RTMP_PUBLIC_HOST=ingest.openre.stream
#OPENRE_RTMP_PUBLIC_HOST=ingest.openre.stream
@@ -185,11 +185,11 @@ sudo deploy/scripts/deploy.sh release origin/main # ovhost deploy openre --pre
@@ -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 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 rollback [<sha>] # ovhost rollback openre
sudo deploy/scripts/deploy.sh plan # ovhost plan openre (or DRY_RUN=1)
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.
- 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.
- 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.
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"capabilitiesRegistered": true,
"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",
"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,
"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": [
"features": [
"stream definitions owned by a Network subject, with typed references",
"stream definitions owned by a Network subject, with typed references",
"ingest keys hashed at rest, shown once, rotation with grace",
"ingest keys hashed at rest, shown once, rotation with grace",
deleted file mode 100755
deleted file mode 100755
@@ -1,110 +0,0 @@
@@ -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
@@ -12,23 +12,14 @@
@@ -12,23 +12,14 @@
# openre-session-coordinator ONLY, roll back if not ready
# openre-session-coordinator ONLY, roll back if not ready
# deploy/scripts/deploy.sh rollback [<sha>] ovhost rollback openre [--to <sha>]
# 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 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).
# --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
set -euo pipefail
SERVICE=openre
SERVICE=openre
-STRATEGY=release-layout
ROOT="${OPENRE_ROOT:-/opt/openre.stream}"
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}"
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
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
@@ -47,37 +38,16 @@ for a in "$@"; do
esac
esac
done
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
case "$SUB" in
- workers|status|prune) exec bash "$LEGACY" "$SUB" ${REF:+"$REF"} ;;
deploy|release|api|rollback|plan) ;;
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
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=()
TO=()
[ -n "$REF" ] && TO=(--to "$REF")
[ -n "$REF" ] && TO=(--to "$REF")
new file mode 100755
new file mode 100755
@@ -0,0 +1,63 @@
@@ -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
@@ -1,12 +1,11 @@
@@ -1,12 +1,11 @@
'use strict';
'use strict';
/**
/**
* deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy openre` (OpenVibe.Host, strategy
* 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;
* - 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;
* 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 assert = require('assert');
const fs = require('fs');
const fs = require('fs');
@@ -26,21 +25,13 @@ const sha12 = sh(`git -C "${root}/repo" rev-parse --short=12 HEAD`).trim();
@@ -26,21 +25,13 @@ const sha12 = sh(`git -C "${root}/repo" rev-parse --short=12 HEAD`).trim();
const ovhost = path.join(tmp, 'ovhost');
const ovhost = path.join(tmp, 'ovhost');
fs.writeFileSync(ovhost, `#!/usr/bin/env bash
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 $*" >> "${log}"
echo "[ovhost] progress output"
echo "[ovhost] progress output"
exit "\${FAKE_EXIT:-0}"
exit "\${FAKE_EXIT:-0}"
`, { mode: 0o755 });
`, { 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 = {}) {
function run(args = [], env = {}) {
fs.rmSync(log, { force: true });
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 = [];
let calls = [];
try { calls = fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean); } catch { /* none */ }
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 };
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
@@ -59,20 +50,27 @@ assert.deepStrictEqual(run(['rollback', sha12]).calls, [`ovhost rollback openre
assert.deepStrictEqual(run(['plan']).calls, ['ovhost plan openre']);
assert.deepStrictEqual(run(['plan']).calls, ['ovhost plan openre']);
assert.deepStrictEqual(run([], { DRY_RUN: '1' }).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)");
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(['nope']).code, 1);
assert.strictEqual(run(['api', '--bogus']).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 });
fs.rmSync(tmp, { recursive: true, force: true });
console.log('deploy wrapper: all checks passed');
console.log('deploy wrapper: all checks passed');