openvibers/openvibe.host Commits

Developer path against production, daily (WS-N task 1): openvibe-devpath.timer runs OpenVibe.Examples' developer path as an unprivileged user with a dedicated test account from a root-only env file, writes step outcomes to /var/lib/openvibe-devpath/last.json and metrics for OpenVibeDeveloperPathFailed/Missed

OP OpenVibers committed 25 Sept 2026 · 7f602faf74 · 5 files, +108 −0


Changes

ddiff --git a/deploy/prometheus/openvibe-rules.yml b/deploy/prometheus/openvibe-rules.yml
iindex c60a3e8..11e0b9a 100644
---- a/deploy/prometheus/openvibe-rules.yml
++++ b/deploy/prometheus/openvibe-rules.yml
@@@ -51,3 +51,16 @@ groups:
labels: { severity: ticket }
annotations:
summary: "{{ $labels.service }}: main has had commits production does not run for more than a day"
++ # The developer path against production, daily (openvibe-devpath.timer; WS-N task 1).
++ - alert: OpenVibeDeveloperPathFailed
++ expr: openvibe_devpath_last_run_ok == 0
++ for: 5m
++ labels: { severity: ticket }
++ annotations:
++ summary: "The daily developer path failed ({{ with query \"openvibe_devpath_steps_failed\" }}{{ . | first | value }}{{ end }} step(s)): a developer cannot go from an account to Media and Events"
++ - alert: OpenVibeDeveloperPathMissed
++ expr: time() - openvibe_devpath_last_success_timestamp_seconds > 30 * 3600
++ for: 30m
++ labels: { severity: ticket }
++ annotations:
++ summary: "No passing developer-path run for more than 30 hours"
ddiff --git a/deploy/systemd/openvibe-devpath.service b/deploy/systemd/openvibe-devpath.service
nnew file mode 100644
iindex 0000000..8710a14
---- /dev/null
++++ b/deploy/systemd/openvibe-devpath.service
@@@ -0,0 +1,23 @@
++[Unit]
++Description=OpenVibe developer path against production (roadmap WS-N task 1): a test account's sandbox project, Media and Events through the SDK, credential rotation and revocation, cleanup
++Documentation=https://github.com/OpenVibers/OpenVibe.Host/blob/main/docs/developer-path.md
++After=network-online.target
++Wants=network-online.target
++
++[Service]
++Type=oneshot
++# The account's name and password come from a root-only env file that systemd reads before dropping to
++# the unprivileged user; the script never prints them (masked output, and --result holds step names only).
++User=ubuntu
++EnvironmentFile=/etc/openvibe/devpath.env
++Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
++WorkingDirectory=/usr/local/lib/openvibe-examples
++StateDirectory=openvibe-devpath
++StateDirectoryMode=0755
++ExecStart=/usr/bin/env node scripts/developer-path.js --result /var/lib/openvibe-devpath/last.json
++# As root: the outcome for Prometheus (node_exporter's textfile collector), whatever the exit code.
++ExecStopPost=+/usr/local/lib/openvibe-host/scripts/devpath-metrics.sh
++TimeoutStartSec=15min
++NoNewPrivileges=true
++PrivateTmp=true
++ProtectSystem=full
ddiff --git a/deploy/systemd/openvibe-devpath.timer b/deploy/systemd/openvibe-devpath.timer
nnew file mode 100644
iindex 0000000..a94ecd2
---- /dev/null
++++ b/deploy/systemd/openvibe-devpath.timer
@@@ -0,0 +1,10 @@
++[Unit]
++Description=Run the OpenVibe developer path against production daily (WS-N task 1)
++
++[Timer]
++OnCalendar=*-*-* 05:10:00 UTC
++RandomizedDelaySec=15min
++Persistent=true
++
++[Install]
++WantedBy=timers.target
ddiff --git a/docs/developer-path.md b/docs/developer-path.md
nnew file mode 100644
iindex 0000000..1d203a8
---- /dev/null
++++ b/docs/developer-path.md
@@@ -0,0 +1,28 @@
++# The developer path against production, daily (roadmap WS-N task 1)
++
++`openvibe-devpath.timer` runs OpenVibe.Examples' `scripts/developer-path.js` once a day (05:10 UTC, up to
++15 minutes later) against production, as an ordinary developer would: sign in with a dedicated test
++account, create a sandbox project and app, get grants, upload and read a file through Media, publish and
++pull an event through Events, rotate and revoke the credential, archive the project.
++
++| Piece | Where |
++|---|---|
++| Checkout | `/usr/local/lib/openvibe-examples` (root-owned; `git pull` + `npm ci` to update) |
++| Account | `/etc/openvibe/devpath.env` (root, 0600): `OV_E2E_USERNAME`, `OV_E2E_PASSWORD` — never printed |
++| Units | `deploy/systemd/openvibe-devpath.service` + `.timer` (runs as `ubuntu`) |
++| Result | `/var/lib/openvibe-devpath/last.json`: step names and ok only (shown on openvibe.network/status) |
++| Metrics | `scripts/devpath-metrics.sh` → node_exporter textfile `openvibe_devpath.prom` |
++| Alerts | `OpenVibeDeveloperPathFailed`, `OpenVibeDeveloperPathMissed` (30 h) in `deploy/prometheus/openvibe-rules.yml` |
++
++Install (once, as root):
++
++```sh
++git clone https://github.com/OpenVibers/OpenVibe.Examples /usr/local/lib/openvibe-examples && (cd /usr/local/lib/openvibe-examples && npm ci --no-audit --no-fund)
++umask 077; printf 'OV_E2E_USERNAME=ovprobe_devpath\nOV_E2E_PASSWORD=%s\n' "$(openssl rand -hex 24)" > /etc/openvibe/devpath.env
++install -m 644 deploy/systemd/openvibe-devpath.{service,timer} /etc/systemd/system/ && systemctl daemon-reload
++# first run registers the account:
++set -a; . /etc/openvibe/devpath.env; set +a; runuser -u ubuntu -p -- node /usr/local/lib/openvibe-examples/scripts/developer-path.js --register
++systemctl enable --now openvibe-devpath.timer
++```
++
++Run now: `systemctl start openvibe-devpath.service`; read: `journalctl -u openvibe-devpath` (masked).
ddiff --git a/scripts/devpath-metrics.sh b/scripts/devpath-metrics.sh
nnew file mode 100755
iindex 0000000..61a50ee
---- /dev/null
++++ b/scripts/devpath-metrics.sh
@@@ -0,0 +1,34 @@
++#!/usr/bin/env bash
++# The developer path's outcome for Prometheus (openvibe-devpath.service ExecStopPost, as root; WS-N task 1):
++# node_exporter's textfile collector reads /var/lib/prometheus/node-exporter/openvibe_devpath.prom.
++# openvibe_devpath_last_run_timestamp_seconds, openvibe_devpath_last_run_ok,
++# openvibe_devpath_last_success_timestamp_seconds (kept from the previous file when this run failed),
++# openvibe_devpath_steps_failed
++# A run that refused to start or wrote no result (exit != 0, no fresh last.json) counts as failed.
++set -euo pipefail
++RESULT=${DEVPATH_RESULT:-/var/lib/openvibe-devpath/last.json}
++DIR=${TEXTFILE_DIR:-/var/lib/prometheus/node-exporter}
++[ -d "$DIR" ] || exit 0
++OUT="$DIR/openvibe_devpath.prom"
++node - "$RESULT" "$OUT" "${EXIT_STATUS:-}" <<'JS'
++const fs = require('fs');
++const [file, out, exitStatus] = process.argv.slice(2);
++const now = Math.floor(Date.now() / 1000);
++let rec = null;
++try { rec = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { rec = null; }
++const fresh = rec && Date.parse(rec.finished_at) / 1000 > now - 3600;
++const ok = !!(fresh && rec.ok && (exitStatus === '' || exitStatus === '0'));
++let lastSuccess = null;
++try { const m = /^openvibe_devpath_last_success_timestamp_seconds (\d+)$/m.exec(fs.readFileSync(out, 'utf8')); if (m) lastSuccess = Number(m[1]); } catch { /* first run */ }
++if (ok) lastSuccess = Math.floor(Date.parse(rec.finished_at) / 1000);
++const failed = fresh ? rec.steps.filter((s) => !s.ok && !s.skipped).length : 1;
++const lines = [
++ '# HELP openvibe_devpath_last_run_timestamp_seconds When the last developer-path run ended.',
++ '# TYPE openvibe_devpath_last_run_timestamp_seconds gauge', `openvibe_devpath_last_run_timestamp_seconds ${fresh ? Math.floor(Date.parse(rec.finished_at) / 1000) : now}`,
++ '# HELP openvibe_devpath_last_run_ok 1 when every step passed.', '# TYPE openvibe_devpath_last_run_ok gauge', `openvibe_devpath_last_run_ok ${ok ? 1 : 0}`,
++ '# HELP openvibe_devpath_steps_failed Steps that failed in the last run.', '# TYPE openvibe_devpath_steps_failed gauge', `openvibe_devpath_steps_failed ${failed}`,
++];
++if (lastSuccess) lines.push('# HELP openvibe_devpath_last_success_timestamp_seconds When the last fully passing run ended.', '# TYPE openvibe_devpath_last_success_timestamp_seconds gauge', `openvibe_devpath_last_success_timestamp_seconds ${lastSuccess}`);
++fs.writeFileSync(`${out}.tmp`, lines.join('\n') + '\n', { mode: 0o644 });
++fs.renameSync(`${out}.tmp`, out);
++JS