openvibers/openvibe.sites Commits
deploy/scripts/deploy.sh hands over to ovhost deploy sites --restart (OpenVibe.Host strategy static-build; roadmap WS-N task 11). The old script is kept, unchanged, as deploy/scripts/deploy-legacy.sh. ovhost pulls first as the checkout owner (a stale checkout never installs old vhosts), restores the tracked dist/ the last build rewrote, runs npm ci and node build.js, installs every deploy/nginx/*.conf behind nginx -t (a failed test restores the previous vhosts, the checkout and dist/, and never reloads nginx) and sends one release notification per dist/<domain>/release.json; --restart keeps the old behaviour of rebuilding every run (people pulled before running the script). A vhost that left deploy/nginx/ is named, never removed. The wrapper asks ovhost capabilities sites first; when ovhost is missing, too old or the host inventory does not deploy sites with static-build (OVHOST_LEGACY=1 forces it) it restores dist/, pulls as the checkout owner and runs the pulled deploy-legacy.sh. test/deploy-wrapper.test.js (in npm test). README.
Changes
@@@ -36,13 +36,19 @@ Every domain also gets a `404.html`: its vhost answers any path that is not a fi`dist/<domain>/` with status 404 and that page (`try_files … =404` + `error_page 404 /404.html`),never the front page.--Deploy (host): the repo lives at `/opt/openvibe.sites`; `deploy/scripts/deploy.sh` runs `npm ci`, rebuilds, installs--the vhosts into `/etc/nginx/sites-available`, enables them and reloads nginx. Each domain has a--Let's Encrypt wildcard certificate (`certbot --dns-cloudflare`, see the host's renewal configs).--Then, for each placeholder, it runs `ovhost announce <service> --release <id> --origin https://<domain>`--with the service and release id from that placeholder's `release.json`. OpenVibe.Host publishes each release once, as--`host.release.published`, and open tabs check `/release.json` within seconds (WS-P task 9). This is best--effort: it is skipped without an `ovhost` that has `announce`, stops at the first failure, and never fails the deploy.++Deploy (host): the repo lives at `/opt/openvibe.sites`. `sudo deploy/scripts/deploy.sh` runs++`ovhost deploy sites --restart` (OpenVibe.Host, strategy `static-build`; `DRY_RUN=1` for the plan). ovhost++pulls first, as the checkout owner, so a stale checkout never installs old vhosts: it restores the tracked++`dist/` the last build rewrote, fast-forwards, runs `npm ci` and `node build.js`, installs and enables every++`deploy/nginx/*.conf` behind `nginx -t` (a failing test puts the previous vhost files back, never reloads++nginx, and restores the checkout and `dist/`), then reloads nginx. Each domain has a Let's Encrypt wildcard++certificate (`certbot --dns-cloudflare`, see the host's renewal configs). Then, for each placeholder, it++publishes `host.release.published` with the service and release id from that placeholder's `release.json`++and `https://<domain>` as the origin, once per release, so open tabs check `/release.json` within seconds++(WS-P task 9); this is best effort, stops at the first failure and never fails the deploy. A vhost that left++`deploy/nginx/` stays installed and is named in the output: remove it by hand once its domain is served++elsewhere. When ovhost is missing, too old or does not deploy Sites with that strategy, the wrapper pulls++and runs `deploy/scripts/deploy-legacy.sh`, the previous script, unchanged (`OVHOST_LEGACY=1` forces it).When a domain becomes a real app, delete it from `sites.json`, remove its vhost, and point thedomain's nginx block at the new service — nothing else references it.nnew file mode 100755@@@ -0,0 +1,36 @@++#!/bin/bash++# OpenVibe.Sites — pull, rebuild, install vhosts, reload nginx, announce each placeholder's release. Run on the host:++# sudo -u ubuntu git -C /opt/openvibe.sites pull --ff-only && /opt/openvibe.sites/deploy/scripts/deploy.sh++set -euo pipefail++REPO="${REPO:-/opt/openvibe.sites}"++cd "$REPO"++# openvibe-shared is the pinned OpenVibe.Shared release (package.json); needs outbound HTTPS to codeload.github.com.++npm ci --omit=dev --no-audit --no-fund++node build.js++changed=0++for conf in deploy/nginx/*.conf; do++ name=$(basename "$conf")++ if ! cmp -s "$conf" "/etc/nginx/sites-available/$name"; then sudo install -m 644 "$conf" "/etc/nginx/sites-available/$name"; changed=1; fi++ [ -L "/etc/nginx/sites-enabled/$name" ] || { sudo ln -s "/etc/nginx/sites-available/$name" "/etc/nginx/sites-enabled/$name"; changed=1; }++done++if [ "$changed" = 1 ]; then sudo nginx -t && sudo systemctl reload nginx && echo "[sites] nginx reloaded"; else echo "[sites] vhosts unchanged"; fi++echo "[sites] $(ls dist | wc -l) sites in place"++# Release notifications (roadmap WS-P task 9): one `ovhost announce <service> --release <id> --origin++# https://<domain>` per placeholder, from its dist/<domain>/release.json. OpenVibe.Host publishes++# host.release.published and open tabs check /release.json now instead of at their next poll; ovhost sends a++# release once, so an unchanged placeholder sends nothing. Best effort: skipped without an ovhost whose++# --help has `announce <service>`, 20 s at most each, stops at the first failure (the rest would fail the++# same way), and never fails the deploy (OpenVibe.Host docs/release-notifications.md).++OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)++if [ -n "$OVHOST_BIN" ]; then++ case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in++ *"announce <service>"*)++ SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"++ node -e 'const fs = require("fs"); for (const d of fs.readdirSync("dist").sort()) { try { const m = JSON.parse(fs.readFileSync(`dist/${d}/release.json`, "utf8")); if (/^[a-z][a-z0-9-]{1,39}$/.test(m.service) && /^[0-9a-f]{7,40}$/.test(m.release) && /^[a-z0-9.-]+$/.test(d)) console.log(`${m.service} ${m.release} ${d}`); } catch { /* no release.json */ } }' |++ while read -r svc rel domain; do++ timeout 20 $SUDO "$OVHOST_BIN" announce "$svc" --release "$rel" --origin "https://$domain" </dev/null 2>&1 | sed 's/^/[sites] /' \++ || { echo "[sites] release notification not sent for $domain; the rest are skipped (the deploy stands)"; break; }++ done ;;++ *) echo "[sites] release notification skipped: this ovhost has no announce" ;;++ esac++fi@@@ -1,36 +1,57 @@#!/bin/bash--# OpenVibe.Sites — pull, rebuild, install vhosts, reload nginx, announce each placeholder's release. Run on the host:--# sudo -u ubuntu git -C /opt/openvibe.sites pull --ff-only && /opt/openvibe.sites/deploy/scripts/deploy.sh++# OpenVibe.Sites — deploy: a thin wrapper around `ovhost deploy sites` (OpenVibe.Host, strategy++# static-build; roadmap WS-N task 11; OpenVibe.Host docs/deploy-strategies.md). Run on the host:++#++# sudo /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost deploy sites --restart++# DRY_RUN=1 /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost plan sites++#++# ovhost pulls FIRST (as the checkout owner), so a stale checkout never installs old vhosts: the tracked++# dist/ a previous build rewrote is restored from git, the checkout fast-forwards, then npm ci, node build.js,++# every deploy/nginx/*.conf installed and enabled behind `nginx -t` (a failing test puts the previous vhost++# files back and nginx is NOT reloaded; the checkout and dist/ go back too), then one release notification++# per dist/<domain>/release.json. --restart makes it rebuild and reinstall even when the checkout is already++# at origin/main (the old script did that every run, and people pulled before running it).++#++# Fallback: deploy-legacy.sh (the previous script, unchanged) when ovhost is missing or too old (no++# `capabilities`, deploy-api < 1) or the host inventory does not deploy sites with strategy static-build;++# OVHOST_LEGACY=1 forces it. The fallback keeps the old rule "pull before deploying": it restores dist/, pulls++# as the checkout owner, then runs the freshly pulled deploy-legacy.sh.set -euo pipefail++++SERVICE=sites++STRATEGY=static-buildREPO="${REPO:-/opt/openvibe.sites}"--cd "$REPO"--# openvibe-shared is the pinned OpenVibe.Shared release (package.json); needs outbound HTTPS to codeload.github.com.--npm ci --omit=dev --no-audit --no-fund--node build.js--changed=0--for conf in deploy/nginx/*.conf; do-- name=$(basename "$conf")-- if ! cmp -s "$conf" "/etc/nginx/sites-available/$name"; then sudo install -m 644 "$conf" "/etc/nginx/sites-available/$name"; changed=1; fi-- [ -L "/etc/nginx/sites-enabled/$name" ] || { sudo ln -s "/etc/nginx/sites-available/$name" "/etc/nginx/sites-enabled/$name"; changed=1; }--done--if [ "$changed" = 1 ]; then sudo nginx -t && sudo systemctl reload nginx && echo "[sites] nginx reloaded"; else echo "[sites] vhosts unchanged"; fi--echo "[sites] $(ls dist | wc -l) sites in place"--# Release notifications (roadmap WS-P task 9): one `ovhost announce <service> --release <id> --origin--# https://<domain>` per placeholder, from its dist/<domain>/release.json. OpenVibe.Host publishes--# host.release.published and open tabs check /release.json now instead of at their next poll; ovhost sends a--# release once, so an unchanged placeholder sends nothing. Best effort: skipped without an ovhost whose--# --help has `announce <service>`, 20 s at most each, stops at the first failure (the rest would fail the--# same way), and never fails the deploy (OpenVibe.Host docs/release-notifications.md).--OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)--if [ -n "$OVHOST_BIN" ]; then-- case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in-- *"announce <service>"*)-- SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"-- node -e 'const fs = require("fs"); for (const d of fs.readdirSync("dist").sort()) { try { const m = JSON.parse(fs.readFileSync(`dist/${d}/release.json`, "utf8")); if (/^[a-z][a-z0-9-]{1,39}$/.test(m.service) && /^[0-9a-f]{7,40}$/.test(m.release) && /^[a-z0-9.-]+$/.test(d)) console.log(`${m.service} ${m.release} ${d}`); } catch { /* no release.json */ } }' |-- while read -r svc rel domain; do-- timeout 20 $SUDO "$OVHOST_BIN" announce "$svc" --release "$rel" --origin "https://$domain" </dev/null 2>&1 | sed 's/^/[sites] /' \-- || { echo "[sites] release notification not sent for $domain; the rest are skipped (the deploy stands)"; break; }-- done ;;-- *) echo "[sites] release notification skipped: this ovhost has no announce" ;;-- esac--fi++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++[ "$#" -eq 0 ] || { echo "Usage: $0 (DRY_RUN=1 for the plan)"; exit 1; }++++legacy() {++ echo "[sites] $1 — running deploy-legacy.sh (the previous deploy script) instead"++ [ "${DRY_RUN:-0}" = 1 ] && { echo "[sites] ✗ deploy-legacy.sh has no DRY_RUN; nothing was done" >&2; exit 1; }++ local owner++ owner=$(stat -c %U "$REPO")++ as_owner() { if [ "$(id -un)" = "$owner" ]; then "$@"; else sudo -u "$owner" "$@"; fi; }++ as_owner git -C "$REPO" checkout -- dist/++ as_owner git -C "$REPO" pull --ff-only++ exec bash "${DEPLOY_LEGACY:-$REPO/deploy/scripts/deploy-legacy.sh}"++}++++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++}++++probe || legacy "$REASON"++++if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE" --restart; fi++echo "[sites] ovhost deploy $SERVICE --restart"++exec "${SUDO[@]}" "$OVHOST" deploy "$SERVICE" --restart@@@ -4,7 +4,7 @@"description": "Placeholder and hub pages for the planned OpenVibe domains (static build)","scripts": {"build": "node build.js",-- "test": "node test/build.test.js"++ "test": "node test/build.test.js && node test/deploy-wrapper.test.js"},"dependencies": {"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"nnew file mode 100644@@@ -0,0 +1,63 @@++'use strict';++// deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy sites --restart` (OpenVibe.Host,++// strategy static-build; roadmap WS-N task 11) with deploy-legacy.sh (the previous script, unchanged) as++// its fallback, which still pulls first. A fake ovhost records what the wrapper asks for; the fallback++// runs against a temp checkout with a real git origin.++// node test/deploy-wrapper.test.js++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { spawnSync, execFileSync } = require('child_process');++++const WRAPPER = path.join(__dirname, '..', 'deploy', 'scripts', 'deploy.sh');++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sites-deploy-wrapper-'));++const log = path.join(tmp, 'calls.log');++const sh = (cmd) => execFileSync('bash', ['-c', cmd], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });++++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=sites\\nstrategy=static-build\\nmanaged=yes\\nlayout=git}"++ exit 0++fi++echo "ovhost $*" >> "${log}"++exit "\${FAKE_EXIT:-0}"++`, { mode: 0o755 });++const legacy = path.join(tmp, 'legacy.sh');++fs.writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy at $(git -C "$REPO" log -1 --format=%s) dist=$(cat "$REPO/dist/x/index.html")" >> "${log}"\n`, { mode: 0o755 });++++// A checkout with an origin that is one commit ahead, and a dist/ the last build left dirty.++const origin = path.join(tmp, 'origin.git');++const work = path.join(tmp, 'work');++const repo = path.join(tmp, 'repo');++sh(`git init -q --bare -b main "${origin}" && git init -q -b main "${work}" && cd "${work}" && git config user.email t@t && git config user.name t && mkdir -p dist/x && echo v1 > dist/x/index.html && git add -A && git commit -qm v1 && git remote add origin "${origin}" && git push -q origin main`);++sh(`git clone -q "${origin}" "${repo}" && cd "${work}" && echo v2 > dist/x/index.html && git commit -qam v2 && git push -q origin main`);++fs.writeFileSync(path.join(repo, 'dist', 'x', 'index.html'), 'v1 rebuilt on the host\n');++++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, REPO: repo, ...env }, encoding: 'utf8' });++ let calls = [];++ try { calls = fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean); } catch { /* none */ }++ return { code: r.status, out: r.stdout + r.stderr, calls };++}++++assert.deepStrictEqual(run().calls, ['ovhost deploy sites --restart'], 'rebuild and reinstall every run, as before');++assert.deepStrictEqual(run([], { DRY_RUN: '1' }).calls, ['ovhost plan sites --restart']);++assert.strictEqual(run([], { FAKE_EXIT: '2' }).code, 2, "ovhost's exit code is the wrapper's (2: nginx -t failed, nothing reloaded)");++assert.strictEqual(run(['--nope']).code, 1);++++let r = run([], { FAKE_CAPS: 'deploy-api=1\\nstrategy=git-checkout\\nmanaged=yes' });++assert.match(r.out, /does not deploy sites with strategy static-build \(git-checkout\)/);++assert.deepStrictEqual(r.calls, ['legacy at v2 dist=v2'], 'the fallback restores dist/ and pulls before the legacy script runs');++r = run([], { OVHOST_LEGACY: '1', DRY_RUN: '1' });++assert.strictEqual(r.code, 1, 'the legacy script has no DRY_RUN: nothing runs');++r = run([], { FAKE_OLD: '1' });++assert.deepStrictEqual(r.calls, ['legacy at v2 dist=v2']);++assert.match(fs.readFileSync(WRAPPER, 'utf8'), /^set -euo pipefail$/m);++assert.strictEqual(fs.readFileSync(path.join(__dirname, '..', 'deploy', 'scripts', 'deploy-legacy.sh'), 'utf8').includes('sudo nginx -t && sudo systemctl reload nginx'), true, 'deploy-legacy.sh is the previous script: nginx -t gates the reload');++++fs.rmSync(tmp, { recursive: true, force: true });++console.log('deploy wrapper: all checks passed');
@@ -36,13 +36,19 @@ Every domain also gets a `404.html`: its vhost answers any path that is not a fi
@@ -36,13 +36,19 @@ Every domain also gets a `404.html`: its vhost answers any path that is not a fi
`dist/<domain>/` with status 404 and that page (`try_files … =404` + `error_page 404 /404.html`),
`dist/<domain>/` with status 404 and that page (`try_files … =404` + `error_page 404 /404.html`),
never the front page.
never the front page.
-Deploy (host): the repo lives at `/opt/openvibe.sites`; `deploy/scripts/deploy.sh` runs `npm ci`, rebuilds, installs
-the vhosts into `/etc/nginx/sites-available`, enables them and reloads nginx. Each domain has a
-Let's Encrypt wildcard certificate (`certbot --dns-cloudflare`, see the host's renewal configs).
-Then, for each placeholder, it runs `ovhost announce <service> --release <id> --origin https://<domain>`
-with the service and release id from that placeholder's `release.json`. OpenVibe.Host publishes each release once, as
-`host.release.published`, and open tabs check `/release.json` within seconds (WS-P task 9). This is best
-effort: it is skipped without an `ovhost` that has `announce`, stops at the first failure, and never fails the deploy.
+Deploy (host): the repo lives at `/opt/openvibe.sites`. `sudo deploy/scripts/deploy.sh` runs
+`ovhost deploy sites --restart` (OpenVibe.Host, strategy `static-build`; `DRY_RUN=1` for the plan). ovhost
+pulls first, as the checkout owner, so a stale checkout never installs old vhosts: it restores the tracked
+`dist/` the last build rewrote, fast-forwards, runs `npm ci` and `node build.js`, installs and enables every
+`deploy/nginx/*.conf` behind `nginx -t` (a failing test puts the previous vhost files back, never reloads
+nginx, and restores the checkout and `dist/`), then reloads nginx. Each domain has a Let's Encrypt wildcard
+certificate (`certbot --dns-cloudflare`, see the host's renewal configs). Then, for each placeholder, it
+publishes `host.release.published` with the service and release id from that placeholder's `release.json`
+and `https://<domain>` as the origin, once per release, so open tabs check `/release.json` within seconds
+(WS-P task 9); this is best effort, stops at the first failure and never fails the deploy. A vhost that left
+`deploy/nginx/` stays installed and is named in the output: remove it by hand once its domain is served
+elsewhere. When ovhost is missing, too old or does not deploy Sites with that strategy, the wrapper pulls
+and runs `deploy/scripts/deploy-legacy.sh`, the previous script, unchanged (`OVHOST_LEGACY=1` forces it).
When a domain becomes a real app, delete it from `sites.json`, remove its vhost, and point the
When a domain becomes a real app, delete it from `sites.json`, remove its vhost, and point the
domain's nginx block at the new service — nothing else references it.
domain's nginx block at the new service — nothing else references it.
new file mode 100755
new file mode 100755
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+#!/bin/bash
+# OpenVibe.Sites — pull, rebuild, install vhosts, reload nginx, announce each placeholder's release. Run on the host:
+# sudo -u ubuntu git -C /opt/openvibe.sites pull --ff-only && /opt/openvibe.sites/deploy/scripts/deploy.sh
+set -euo pipefail
+REPO="${REPO:-/opt/openvibe.sites}"
+cd "$REPO"
+# openvibe-shared is the pinned OpenVibe.Shared release (package.json); needs outbound HTTPS to codeload.github.com.
+npm ci --omit=dev --no-audit --no-fund
+node build.js
+changed=0
+for conf in deploy/nginx/*.conf; do
+ name=$(basename "$conf")
+ if ! cmp -s "$conf" "/etc/nginx/sites-available/$name"; then sudo install -m 644 "$conf" "/etc/nginx/sites-available/$name"; changed=1; fi
+ [ -L "/etc/nginx/sites-enabled/$name" ] || { sudo ln -s "/etc/nginx/sites-available/$name" "/etc/nginx/sites-enabled/$name"; changed=1; }
+done
+if [ "$changed" = 1 ]; then sudo nginx -t && sudo systemctl reload nginx && echo "[sites] nginx reloaded"; else echo "[sites] vhosts unchanged"; fi
+echo "[sites] $(ls dist | wc -l) sites in place"
+# Release notifications (roadmap WS-P task 9): one `ovhost announce <service> --release <id> --origin
+# https://<domain>` per placeholder, from its dist/<domain>/release.json. OpenVibe.Host publishes
+# host.release.published and open tabs check /release.json now instead of at their next poll; ovhost sends a
+# release once, so an unchanged placeholder sends nothing. Best effort: skipped without an ovhost whose
+# --help has `announce <service>`, 20 s at most each, stops at the first failure (the rest would fail the
+# same way), and never fails the deploy (OpenVibe.Host docs/release-notifications.md).
+OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)
+if [ -n "$OVHOST_BIN" ]; then
+ case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in
+ *"announce <service>"*)
+ SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"
+ node -e 'const fs = require("fs"); for (const d of fs.readdirSync("dist").sort()) { try { const m = JSON.parse(fs.readFileSync(`dist/${d}/release.json`, "utf8")); if (/^[a-z][a-z0-9-]{1,39}$/.test(m.service) && /^[0-9a-f]{7,40}$/.test(m.release) && /^[a-z0-9.-]+$/.test(d)) console.log(`${m.service} ${m.release} ${d}`); } catch { /* no release.json */ } }' |
+ while read -r svc rel domain; do
+ timeout 20 $SUDO "$OVHOST_BIN" announce "$svc" --release "$rel" --origin "https://$domain" </dev/null 2>&1 | sed 's/^/[sites] /' \
+ || { echo "[sites] release notification not sent for $domain; the rest are skipped (the deploy stands)"; break; }
+ done ;;
+ *) echo "[sites] release notification skipped: this ovhost has no announce" ;;
+ esac
+fi
@@ -1,36 +1,57 @@
@@ -1,36 +1,57 @@
#!/bin/bash
#!/bin/bash
-# OpenVibe.Sites — pull, rebuild, install vhosts, reload nginx, announce each placeholder's release. Run on the host:
-# sudo -u ubuntu git -C /opt/openvibe.sites pull --ff-only && /opt/openvibe.sites/deploy/scripts/deploy.sh
+# OpenVibe.Sites — deploy: a thin wrapper around `ovhost deploy sites` (OpenVibe.Host, strategy
+# static-build; roadmap WS-N task 11; OpenVibe.Host docs/deploy-strategies.md). Run on the host:
+#
+# sudo /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost deploy sites --restart
+# DRY_RUN=1 /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost plan sites
+#
+# ovhost pulls FIRST (as the checkout owner), so a stale checkout never installs old vhosts: the tracked
+# dist/ a previous build rewrote is restored from git, the checkout fast-forwards, then npm ci, node build.js,
+# every deploy/nginx/*.conf installed and enabled behind `nginx -t` (a failing test puts the previous vhost
+# files back and nginx is NOT reloaded; the checkout and dist/ go back too), then one release notification
+# per dist/<domain>/release.json. --restart makes it rebuild and reinstall even when the checkout is already
+# at origin/main (the old script did that every run, and people pulled before running it).
+#
+# Fallback: deploy-legacy.sh (the previous script, unchanged) when ovhost is missing or too old (no
+# `capabilities`, deploy-api < 1) or the host inventory does not deploy sites with strategy static-build;
+# OVHOST_LEGACY=1 forces it. The fallback keeps the old rule "pull before deploying": it restores dist/, pulls
+# as the checkout owner, then runs the freshly pulled deploy-legacy.sh.
set -euo pipefail
set -euo pipefail
+
+SERVICE=sites
+STRATEGY=static-build
REPO="${REPO:-/opt/openvibe.sites}"
REPO="${REPO:-/opt/openvibe.sites}"
-cd "$REPO"
-# openvibe-shared is the pinned OpenVibe.Shared release (package.json); needs outbound HTTPS to codeload.github.com.
-npm ci --omit=dev --no-audit --no-fund
-node build.js
-changed=0
-for conf in deploy/nginx/*.conf; do
- name=$(basename "$conf")
- if ! cmp -s "$conf" "/etc/nginx/sites-available/$name"; then sudo install -m 644 "$conf" "/etc/nginx/sites-available/$name"; changed=1; fi
- [ -L "/etc/nginx/sites-enabled/$name" ] || { sudo ln -s "/etc/nginx/sites-available/$name" "/etc/nginx/sites-enabled/$name"; changed=1; }
-done
-if [ "$changed" = 1 ]; then sudo nginx -t && sudo systemctl reload nginx && echo "[sites] nginx reloaded"; else echo "[sites] vhosts unchanged"; fi
-echo "[sites] $(ls dist | wc -l) sites in place"
-# Release notifications (roadmap WS-P task 9): one `ovhost announce <service> --release <id> --origin
-# https://<domain>` per placeholder, from its dist/<domain>/release.json. OpenVibe.Host publishes
-# host.release.published and open tabs check /release.json now instead of at their next poll; ovhost sends a
-# release once, so an unchanged placeholder sends nothing. Best effort: skipped without an ovhost whose
-# --help has `announce <service>`, 20 s at most each, stops at the first failure (the rest would fail the
-# same way), and never fails the deploy (OpenVibe.Host docs/release-notifications.md).
-OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)
-if [ -n "$OVHOST_BIN" ]; then
- case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in
- *"announce <service>"*)
- SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"
- node -e 'const fs = require("fs"); for (const d of fs.readdirSync("dist").sort()) { try { const m = JSON.parse(fs.readFileSync(`dist/${d}/release.json`, "utf8")); if (/^[a-z][a-z0-9-]{1,39}$/.test(m.service) && /^[0-9a-f]{7,40}$/.test(m.release) && /^[a-z0-9.-]+$/.test(d)) console.log(`${m.service} ${m.release} ${d}`); } catch { /* no release.json */ } }' |
- while read -r svc rel domain; do
- timeout 20 $SUDO "$OVHOST_BIN" announce "$svc" --release "$rel" --origin "https://$domain" </dev/null 2>&1 | sed 's/^/[sites] /' \
- || { echo "[sites] release notification not sent for $domain; the rest are skipped (the deploy stands)"; break; }
- done ;;
- *) echo "[sites] release notification skipped: this ovhost has no announce" ;;
- esac
-fi
+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
+[ "$#" -eq 0 ] || { echo "Usage: $0 (DRY_RUN=1 for the plan)"; exit 1; }
+
+legacy() {
+ echo "[sites] $1 — running deploy-legacy.sh (the previous deploy script) instead"
+ [ "${DRY_RUN:-0}" = 1 ] && { echo "[sites] ✗ deploy-legacy.sh has no DRY_RUN; nothing was done" >&2; exit 1; }
+ local owner
+ owner=$(stat -c %U "$REPO")
+ as_owner() { if [ "$(id -un)" = "$owner" ]; then "$@"; else sudo -u "$owner" "$@"; fi; }
+ as_owner git -C "$REPO" checkout -- dist/
+ as_owner git -C "$REPO" pull --ff-only
+ exec bash "${DEPLOY_LEGACY:-$REPO/deploy/scripts/deploy-legacy.sh}"
+}
+
+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
+}
+
+probe || legacy "$REASON"
+
+if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE" --restart; fi
+echo "[sites] ovhost deploy $SERVICE --restart"
+exec "${SUDO[@]}" "$OVHOST" deploy "$SERVICE" --restart
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"description": "Placeholder and hub pages for the planned OpenVibe domains (static build)",
"description": "Placeholder and hub pages for the planned OpenVibe domains (static build)",
"scripts": {
"scripts": {
"build": "node build.js",
"build": "node build.js",
- "test": "node test/build.test.js"
+ "test": "node test/build.test.js && node test/deploy-wrapper.test.js"
},
},
"dependencies": {
"dependencies": {
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
new file mode 100644
new file mode 100644
@@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
+'use strict';
+// deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy sites --restart` (OpenVibe.Host,
+// strategy static-build; roadmap WS-N task 11) with deploy-legacy.sh (the previous script, unchanged) as
+// its fallback, which still pulls first. A fake ovhost records what the wrapper asks for; the fallback
+// runs against a temp checkout with a real git origin.
+// node test/deploy-wrapper.test.js
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawnSync, execFileSync } = require('child_process');
+
+const WRAPPER = path.join(__dirname, '..', 'deploy', 'scripts', 'deploy.sh');
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sites-deploy-wrapper-'));
+const log = path.join(tmp, 'calls.log');
+const sh = (cmd) => execFileSync('bash', ['-c', cmd], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
+
+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=sites\\nstrategy=static-build\\nmanaged=yes\\nlayout=git}"
+ exit 0
+fi
+echo "ovhost $*" >> "${log}"
+exit "\${FAKE_EXIT:-0}"
+`, { mode: 0o755 });
+const legacy = path.join(tmp, 'legacy.sh');
+fs.writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy at $(git -C "$REPO" log -1 --format=%s) dist=$(cat "$REPO/dist/x/index.html")" >> "${log}"\n`, { mode: 0o755 });
+
+// A checkout with an origin that is one commit ahead, and a dist/ the last build left dirty.
+const origin = path.join(tmp, 'origin.git');
+const work = path.join(tmp, 'work');
+const repo = path.join(tmp, 'repo');
+sh(`git init -q --bare -b main "${origin}" && git init -q -b main "${work}" && cd "${work}" && git config user.email t@t && git config user.name t && mkdir -p dist/x && echo v1 > dist/x/index.html && git add -A && git commit -qm v1 && git remote add origin "${origin}" && git push -q origin main`);
+sh(`git clone -q "${origin}" "${repo}" && cd "${work}" && echo v2 > dist/x/index.html && git commit -qam v2 && git push -q origin main`);
+fs.writeFileSync(path.join(repo, 'dist', 'x', 'index.html'), 'v1 rebuilt on the host\n');
+
+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, REPO: repo, ...env }, encoding: 'utf8' });
+ let calls = [];
+ try { calls = fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean); } catch { /* none */ }
+ return { code: r.status, out: r.stdout + r.stderr, calls };
+}
+
+assert.deepStrictEqual(run().calls, ['ovhost deploy sites --restart'], 'rebuild and reinstall every run, as before');
+assert.deepStrictEqual(run([], { DRY_RUN: '1' }).calls, ['ovhost plan sites --restart']);
+assert.strictEqual(run([], { FAKE_EXIT: '2' }).code, 2, "ovhost's exit code is the wrapper's (2: nginx -t failed, nothing reloaded)");
+assert.strictEqual(run(['--nope']).code, 1);
+
+let r = run([], { FAKE_CAPS: 'deploy-api=1\\nstrategy=git-checkout\\nmanaged=yes' });
+assert.match(r.out, /does not deploy sites with strategy static-build \(git-checkout\)/);
+assert.deepStrictEqual(r.calls, ['legacy at v2 dist=v2'], 'the fallback restores dist/ and pulls before the legacy script runs');
+r = run([], { OVHOST_LEGACY: '1', DRY_RUN: '1' });
+assert.strictEqual(r.code, 1, 'the legacy script has no DRY_RUN: nothing runs');
+r = run([], { FAKE_OLD: '1' });
+assert.deepStrictEqual(r.calls, ['legacy at v2 dist=v2']);
+assert.match(fs.readFileSync(WRAPPER, 'utf8'), /^set -euo pipefail$/m);
+assert.strictEqual(fs.readFileSync(path.join(__dirname, '..', 'deploy', 'scripts', 'deploy-legacy.sh'), 'utf8').includes('sudo nginx -t && sudo systemctl reload nginx'), true, 'deploy-legacy.sh is the previous script: nginx -t gates the reload');
+
+fs.rmSync(tmp, { recursive: true, force: true });
+console.log('deploy wrapper: all checks passed');