openvibers/openvibe.sites Commits
Sites: the legacy deploy fallback is deleted; deploy.sh only runs ovhost (#38)
Changes
@@@ -73,13 +73,13 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror- `openvibe-contracts` v0.126.0 — `scripts/sync-catalog.js` reads `openvibe-contracts/lib/products` (`catalog()`) to generate `sites.json`.- `openvibe-shared` v2.20.3 — `build.js` uses `legal` for the terms, privacy and dmca pages, `files` for the `BROWSER` list copied to `dist/_shared/`, and `app-icon` for the manifest and head tags; `test/run.js` uses `test-runner`.--- OpenVibe.Host — the deploy wrapper probes and calls `ovhost` (the `capabilities` probe, `deploy`, `plan`, `announce`).++- OpenVibe.Host — the deploy wrapper calls `ovhost deploy sites --restart`, or `ovhost plan sites --restart` with `DRY_RUN=1`.- OpenVibe.Network at `https://openvibe.network` — the frozen pages and shared browser files load its assets and API in the browser; `NET.networkUrl` in `build.js` is that base.## Capabilities- Sites checks and requires no OpenVibe capability, and holds no grant on another service: it serves static files and calls no service API.--- The deploy wrapper probes the host inventory with `ovhost capabilities sites` and refuses unless it reports `strategy=static-build`, `managed=yes` and a `deploy-api` of at least 1; otherwise it falls back to `deploy/scripts/deploy-legacy.sh`.++- The deploy wrapper requires `ovhost` to be installed and exits with an error if it is missing.## Acceptance@@@ -87,12 +87,12 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror- `node build.js --check` verifies the generated files are current; `test/build.test.js` runs it.- `test/build.test.js` proves the generated files are current, that `frozen.json` is exactly openvibe.tips and openvibe.vip, that every retired domain has left `dist/` and `deploy/nginx/`, that each frozen page says it is a placeholder with no pricing or unbacked-hosting copy, and (for any notice) the noindex, destination links, files and true-404 vhost.- `test/sync-catalog.test.js` proves `scripts/sync-catalog.js --check` passes (sites.json matches the Contracts catalog), that every vhost in `deploy/nginx/` has a `sites.json` entry with a home in the catalog, and that `dropNoRepo` stops a row carrying both `plannedRepo` and `noRepo`.--- `test/deploy-wrapper.test.js` drives `deploy/scripts/deploy.sh` against a fake ovhost and a temp checkout: it runs `ovhost deploy sites --restart`, `DRY_RUN=1` runs `ovhost plan sites --restart` instead, ovhost's exit code is the wrapper's, and it falls back to `deploy-legacy.sh` when the host does not deploy sites with strategy static-build.++- `test/deploy-wrapper.test.js` drives `deploy/scripts/deploy.sh` against a fake ovhost: it runs `ovhost deploy sites --restart`, `DRY_RUN=1` runs `ovhost plan sites --restart` instead, ovhost's exit code is the wrapper's, and a missing ovhost produces an error.## Security- Sites has no server process, token or environment file; it writes static files at build time, so there is no request input, auth surface or rate limiting to configure.--- The build is offline except for fetching the two pinned packages over HTTPS from codeload.github.com (`npm ci` in `deploy/scripts/deploy-legacy.sh`).++- The build is offline; deployment runs `npm ci` to fetch the two pinned packages over HTTPS from codeload.github.com.- Generated HTML is built from committed page snapshots and openvibe-shared templates, not user input; the generated 404 page escapes the values it interpolates (`esc` in `build.js`).- Every vhost sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin` and HSTS (`max-age=31536000`), denies dotfiles except `/.well-known` (where the host serves `security.txt`), and returns a real 404 (`try_files $uri $uri.html $uri/ =404`) instead of the front page.- `robots.txt` disallows `/auth/`; CI runs the shared secret scan (gitleaks) and dependency audit workflows.@@@ -101,7 +101,7 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror- Production is the checkout `/opt/openvibe.sites`. It is deployed by running `deploy/scripts/deploy.sh` on the host, a thin wrapper around `sudo ovhost deploy sites --restart` (OpenVibe.Host, strategy static-build, since the 2026-09-27 cutover).- ovhost pulls first (restoring the tracked `dist/` a previous build rewrote and fast-forwarding the checkout), then runs `npm ci` and `node build.js`, installs every `deploy/nginx/*.conf` into `/etc/nginx/sites-available/` and enables it behind `nginx -t` (a failure puts the previous vhosts back and does not reload nginx), and sends one `ovhost announce` release per `dist/<domain>/release.json`.--- Fallback: `deploy/scripts/deploy-legacy.sh` (the previous script) when ovhost is missing or too old, when the host inventory does not deploy sites with strategy static-build, or with `OVHOST_LEGACY=1`. `DRY_RUN=1` runs `ovhost plan sites --restart` instead.++- `DRY_RUN=1` runs `ovhost plan sites --restart` instead. If ovhost is missing, the wrapper exits with an error.- There is no systemd unit, no port and no environment file. nginx serves each domain from `root /opt/openvibe.sites/dist/<domain>` through `deploy/nginx/<domain>.conf`. A vhost removed from `deploy/nginx/` stays installed on the host until it is removed by hand.Report security issues through [SECURITY.md](SECURITY.md).ddeleted file mode 100755@@@ -1,36 +0,0 @@--#!/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,56 +1,21 @@#!/bin/bash--# 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:++# OpenVibe.Sites — deploy through OpenVibe.Host. 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++# DRY_RUN=1 /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost plan sites --restart#--# 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.++# --restart rebuilds and reinstalls even when the checkout is already at origin/main.set -euo pipefailSERVICE=sites--STRATEGY=static-build--REPO="${REPO:-/opt/openvibe.sites}"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 ! command -v "$OVHOST" >/dev/null 2>&1; then++ echo "[sites] ovhost not found ($OVHOST); install ovhost to deploy sites" >&2++ exit 1++fiif [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE" --restart; fiecho "[sites] ovhost deploy $SERVICE --restart"@@@ -1,63 +1,48 @@'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.++// Verify that deploy/scripts/deploy.sh delegates to ovhost with the Sites service and --restart.// node test/deploy-wrapper.test.jsconst assert = require('assert');const fs = require('fs');const os = require('os');const path = require('path');--const { spawnSync, execFileSync } = require('child_process');++const { spawnSync } = 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++++try {++ fs.writeFileSync(ovhost, `#!/usr/bin/env bashecho "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, OVHOST: ovhost, OVHOST_SUDO: '', ...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 };++ }--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']);++ 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");++ const invalidArgs = run(['--nope']);++ assert.strictEqual(invalidArgs.code, 1);++ assert.deepStrictEqual(invalidArgs.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);++ const missing = run([], { OVHOST: path.join(tmp, 'missing-ovhost') });++ assert.strictEqual(missing.code, 1);++ assert.match(missing.out, /ovhost not found/);++ assert.deepStrictEqual(missing.calls, []);++ assert.doesNotMatch(fs.readFileSync(WRAPPER, 'utf8'), /legacy/i);--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');++ console.log('deploy wrapper: all checks passed');++} finally {++ fs.rmSync(tmp, { recursive: true, force: true });++}
@@ -73,13 +73,13 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
@@ -73,13 +73,13 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
- `openvibe-contracts` v0.126.0 — `scripts/sync-catalog.js` reads `openvibe-contracts/lib/products` (`catalog()`) to generate `sites.json`.
- `openvibe-contracts` v0.126.0 — `scripts/sync-catalog.js` reads `openvibe-contracts/lib/products` (`catalog()`) to generate `sites.json`.
- `openvibe-shared` v2.20.3 — `build.js` uses `legal` for the terms, privacy and dmca pages, `files` for the `BROWSER` list copied to `dist/_shared/`, and `app-icon` for the manifest and head tags; `test/run.js` uses `test-runner`.
- `openvibe-shared` v2.20.3 — `build.js` uses `legal` for the terms, privacy and dmca pages, `files` for the `BROWSER` list copied to `dist/_shared/`, and `app-icon` for the manifest and head tags; `test/run.js` uses `test-runner`.
-- OpenVibe.Host — the deploy wrapper probes and calls `ovhost` (the `capabilities` probe, `deploy`, `plan`, `announce`).
+- OpenVibe.Host — the deploy wrapper calls `ovhost deploy sites --restart`, or `ovhost plan sites --restart` with `DRY_RUN=1`.
- OpenVibe.Network at `https://openvibe.network` — the frozen pages and shared browser files load its assets and API in the browser; `NET.networkUrl` in `build.js` is that base.
- OpenVibe.Network at `https://openvibe.network` — the frozen pages and shared browser files load its assets and API in the browser; `NET.networkUrl` in `build.js` is that base.
## Capabilities
## Capabilities
- Sites checks and requires no OpenVibe capability, and holds no grant on another service: it serves static files and calls no service API.
- Sites checks and requires no OpenVibe capability, and holds no grant on another service: it serves static files and calls no service API.
-- The deploy wrapper probes the host inventory with `ovhost capabilities sites` and refuses unless it reports `strategy=static-build`, `managed=yes` and a `deploy-api` of at least 1; otherwise it falls back to `deploy/scripts/deploy-legacy.sh`.
+- The deploy wrapper requires `ovhost` to be installed and exits with an error if it is missing.
## Acceptance
## Acceptance
@@ -87,12 +87,12 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
@@ -87,12 +87,12 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
- `node build.js --check` verifies the generated files are current; `test/build.test.js` runs it.
- `node build.js --check` verifies the generated files are current; `test/build.test.js` runs it.
- `test/build.test.js` proves the generated files are current, that `frozen.json` is exactly openvibe.tips and openvibe.vip, that every retired domain has left `dist/` and `deploy/nginx/`, that each frozen page says it is a placeholder with no pricing or unbacked-hosting copy, and (for any notice) the noindex, destination links, files and true-404 vhost.
- `test/build.test.js` proves the generated files are current, that `frozen.json` is exactly openvibe.tips and openvibe.vip, that every retired domain has left `dist/` and `deploy/nginx/`, that each frozen page says it is a placeholder with no pricing or unbacked-hosting copy, and (for any notice) the noindex, destination links, files and true-404 vhost.
- `test/sync-catalog.test.js` proves `scripts/sync-catalog.js --check` passes (sites.json matches the Contracts catalog), that every vhost in `deploy/nginx/` has a `sites.json` entry with a home in the catalog, and that `dropNoRepo` stops a row carrying both `plannedRepo` and `noRepo`.
- `test/sync-catalog.test.js` proves `scripts/sync-catalog.js --check` passes (sites.json matches the Contracts catalog), that every vhost in `deploy/nginx/` has a `sites.json` entry with a home in the catalog, and that `dropNoRepo` stops a row carrying both `plannedRepo` and `noRepo`.
-- `test/deploy-wrapper.test.js` drives `deploy/scripts/deploy.sh` against a fake ovhost and a temp checkout: it runs `ovhost deploy sites --restart`, `DRY_RUN=1` runs `ovhost plan sites --restart` instead, ovhost's exit code is the wrapper's, and it falls back to `deploy-legacy.sh` when the host does not deploy sites with strategy static-build.
+- `test/deploy-wrapper.test.js` drives `deploy/scripts/deploy.sh` against a fake ovhost: it runs `ovhost deploy sites --restart`, `DRY_RUN=1` runs `ovhost plan sites --restart` instead, ovhost's exit code is the wrapper's, and a missing ovhost produces an error.
## Security
## Security
- Sites has no server process, token or environment file; it writes static files at build time, so there is no request input, auth surface or rate limiting to configure.
- Sites has no server process, token or environment file; it writes static files at build time, so there is no request input, auth surface or rate limiting to configure.
-- The build is offline except for fetching the two pinned packages over HTTPS from codeload.github.com (`npm ci` in `deploy/scripts/deploy-legacy.sh`).
+- The build is offline; deployment runs `npm ci` to fetch the two pinned packages over HTTPS from codeload.github.com.
- Generated HTML is built from committed page snapshots and openvibe-shared templates, not user input; the generated 404 page escapes the values it interpolates (`esc` in `build.js`).
- Generated HTML is built from committed page snapshots and openvibe-shared templates, not user input; the generated 404 page escapes the values it interpolates (`esc` in `build.js`).
- Every vhost sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin` and HSTS (`max-age=31536000`), denies dotfiles except `/.well-known` (where the host serves `security.txt`), and returns a real 404 (`try_files $uri $uri.html $uri/ =404`) instead of the front page.
- Every vhost sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin` and HSTS (`max-age=31536000`), denies dotfiles except `/.well-known` (where the host serves `security.txt`), and returns a real 404 (`try_files $uri $uri.html $uri/ =404`) instead of the front page.
- `robots.txt` disallows `/auth/`; CI runs the shared secret scan (gitleaks) and dependency audit workflows.
- `robots.txt` disallows `/auth/`; CI runs the shared secret scan (gitleaks) and dependency audit workflows.
@@ -101,7 +101,7 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
@@ -101,7 +101,7 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
- Production is the checkout `/opt/openvibe.sites`. It is deployed by running `deploy/scripts/deploy.sh` on the host, a thin wrapper around `sudo ovhost deploy sites --restart` (OpenVibe.Host, strategy static-build, since the 2026-09-27 cutover).
- Production is the checkout `/opt/openvibe.sites`. It is deployed by running `deploy/scripts/deploy.sh` on the host, a thin wrapper around `sudo ovhost deploy sites --restart` (OpenVibe.Host, strategy static-build, since the 2026-09-27 cutover).
- ovhost pulls first (restoring the tracked `dist/` a previous build rewrote and fast-forwarding the checkout), then runs `npm ci` and `node build.js`, installs every `deploy/nginx/*.conf` into `/etc/nginx/sites-available/` and enables it behind `nginx -t` (a failure puts the previous vhosts back and does not reload nginx), and sends one `ovhost announce` release per `dist/<domain>/release.json`.
- ovhost pulls first (restoring the tracked `dist/` a previous build rewrote and fast-forwarding the checkout), then runs `npm ci` and `node build.js`, installs every `deploy/nginx/*.conf` into `/etc/nginx/sites-available/` and enables it behind `nginx -t` (a failure puts the previous vhosts back and does not reload nginx), and sends one `ovhost announce` release per `dist/<domain>/release.json`.
-- Fallback: `deploy/scripts/deploy-legacy.sh` (the previous script) when ovhost is missing or too old, when the host inventory does not deploy sites with strategy static-build, or with `OVHOST_LEGACY=1`. `DRY_RUN=1` runs `ovhost plan sites --restart` instead.
+- `DRY_RUN=1` runs `ovhost plan sites --restart` instead. If ovhost is missing, the wrapper exits with an error.
- There is no systemd unit, no port and no environment file. nginx serves each domain from `root /opt/openvibe.sites/dist/<domain>` through `deploy/nginx/<domain>.conf`. A vhost removed from `deploy/nginx/` stays installed on the host until it is removed by hand.
- There is no systemd unit, no port and no environment file. nginx serves each domain from `root /opt/openvibe.sites/dist/<domain>` through `deploy/nginx/<domain>.conf`. A vhost removed from `deploy/nginx/` stays installed on the host until it is removed by hand.
Report security issues through [SECURITY.md](SECURITY.md).
Report security issues through [SECURITY.md](SECURITY.md).
deleted file mode 100755
deleted file mode 100755
@@ -1,36 +0,0 @@
@@ -1,36 +0,0 @@
-#!/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,56 +1,21 @@
@@ -1,56 +1,21 @@
#!/bin/bash
#!/bin/bash
-# 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:
+# OpenVibe.Sites — deploy through OpenVibe.Host. Run on the host:
#
#
# sudo /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost deploy sites --restart
# 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
+# DRY_RUN=1 /opt/openvibe.sites/deploy/scripts/deploy.sh ovhost plan sites --restart
#
#
-# 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.
+# --restart rebuilds and reinstalls even when the checkout is already at origin/main.
set -euo pipefail
set -euo pipefail
SERVICE=sites
SERVICE=sites
-STRATEGY=static-build
-REPO="${REPO:-/opt/openvibe.sites}"
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
[ "$#" -eq 0 ] || { echo "Usage: $0 (DRY_RUN=1 for the plan)"; exit 1; }
[ "$#" -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 ! command -v "$OVHOST" >/dev/null 2>&1; then
+ echo "[sites] ovhost not found ($OVHOST); install ovhost to deploy sites" >&2
+ exit 1
+fi
if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE" --restart; fi
if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE" --restart; fi
echo "[sites] ovhost deploy $SERVICE --restart"
echo "[sites] ovhost deploy $SERVICE --restart"
@@ -1,63 +1,48 @@
@@ -1,63 +1,48 @@
'use strict';
'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.
+// Verify that deploy/scripts/deploy.sh delegates to ovhost with the Sites service and --restart.
// node test/deploy-wrapper.test.js
// node test/deploy-wrapper.test.js
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const os = require('os');
const os = require('os');
const path = require('path');
const path = require('path');
-const { spawnSync, execFileSync } = require('child_process');
+const { spawnSync } = require('child_process');
const WRAPPER = path.join(__dirname, '..', 'deploy', 'scripts', 'deploy.sh');
const WRAPPER = path.join(__dirname, '..', 'deploy', 'scripts', 'deploy.sh');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sites-deploy-wrapper-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sites-deploy-wrapper-'));
const log = path.join(tmp, 'calls.log');
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');
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
+
+try {
+ fs.writeFileSync(ovhost, `#!/usr/bin/env bash
echo "ovhost $*" >> "${log}"
echo "ovhost $*" >> "${log}"
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 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, OVHOST: ovhost, OVHOST_SUDO: '', ...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 };
+ }
-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']);
+ 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");
+ const invalidArgs = run(['--nope']);
+ assert.strictEqual(invalidArgs.code, 1);
+ assert.deepStrictEqual(invalidArgs.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);
+ const missing = run([], { OVHOST: path.join(tmp, 'missing-ovhost') });
+ assert.strictEqual(missing.code, 1);
+ assert.match(missing.out, /ovhost not found/);
+ assert.deepStrictEqual(missing.calls, []);
+ assert.doesNotMatch(fs.readFileSync(WRAPPER, 'utf8'), /legacy/i);
-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');
+ console.log('deploy wrapper: all checks passed');
+} finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+}