openvibers/openvibe.shared Commits

Shared 1.24.0: no fake green in readiness (roadmap WS-Q task 7). ready.js knew only ok and fail, so a check whose feature was not configured had to claim ok (Network's Discord bot, Media's events outbox and News's source pull did, as production's /api/ready shows) or fail and turn the service yellow for a setting. A check now returns skip(reason, detail?) (exported) or { skipped: reason }: it reports status skipped with the reason, never ok, and is listed in the body's new skipped array. A skipped optional check leaves a service whose other checks passed ready; a skipped required check is also degraded (yellow, still HTTP 200: nothing failed); and a service is ready only when at least one check really passed, so one with no checks or every check skipped is degraded (before: ready). test/ready.test.js covers each case; README and CHANGELOG describe it. npm test (Node 22) 38/38.

OP OpenVibers committed 28 Sept 2026 · 48b7443f91 · 7 files, +76 −12


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex b64f2b6..76dea88 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 1.24.0 — 2026-09-28
++
++**No fake green in readiness** (roadmap WS-Q task 7: "skipped counts as yellow; nothing may be marked green by being synthesised from skipped parts"). `ready.js` had only `ok` and `fail`, so a check whose feature was not configured had to claim `ok` (Network's Discord bot, Media's events outbox and News's source pull did) or fail and turn the service yellow for a setting.
++- **`skip(reason, detail?)`**, exported by `openvibe-shared/ready`, or a check returning `{ skipped: reason }`: the check reports status `skipped` with `reason` (and `detail`), never `ok`, and is listed in the new `skipped` array of the body.
++- A skipped **optional** check leaves a service whose other checks passed `ready`. A skipped **required** check is listed in `degraded` too, and the service is `degraded` (still HTTP 200: nothing failed).
++- A service is `ready` only when at least one check really passed: one with no checks, or with every check skipped, is `degraded`. This changes what a service with no checks reports (before: `ready`).
++
## 1.23.1 — 2026-09-27
**Two release-watch fixes found by the release-lifecycle acceptance scenarios** (D46, roadmap WS-P task 16; OpenVibe.Host `scripts/release-acceptance.js`). release-watch.js stays within its 5 KB brotli budget (4.99 KB; comments shortened).
ddiff --git a/README.md b/README.md
iindex 0a6dd86..7b49542 100644
---- a/README.md
++++ b/README.md
@@@ -72,6 +72,12 @@ A check fails when it throws, times out, returns `false`, a string (the reason)
`{ ok: false, error }`. Name a dependency required only when the service really cannot serve
without it.
++A check that verified nothing (its feature is not configured, its dependency is switched off)
++returns `skip('not configured')` (`require('openvibe-shared/ready').skip`, or `{ skipped: reason }`),
++never `true`: it reports status `skipped` with the reason and is listed in `skipped`. No fake green
++(roadmap WS-Q task 7): a skipped required check makes the service `degraded`, and a service is
++`ready` only when at least one check really passed.
++
### Configuration (server, WS-C task 7)
`openvibe-shared/config` keeps one namespace of a service's configuration as immutable revisions
ddiff --git a/STATUS.json b/STATUS.json
iindex e9f43c8..e508c9e 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "1.23.1",
++ "version": "1.24.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/package-lock.json b/package-lock.json
iindex dbd321a..e459658 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "1.23.1",
++ "version": "1.24.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "1.23.1",
++ "version": "1.24.0",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 5fa443a..74656a0 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "1.23.1",
++ "version": "1.24.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"exports": {
ddiff --git a/ready.js b/ready.js
iindex 8b562dc..bbc4c7f 100644
---- a/ready.js
++++ b/ready.js
@@@ -20,6 +20,13 @@
// throw or time out. Overall `ready` is false only when a REQUIRED check fails (HTTP 503); a failed
// optional check keeps the service ready (HTTP 200) and is listed in `degraded`, so a missing optional
// dependency never reads as "healthy" or as "down".
++//
++// No fake green (roadmap WS-Q task 7): a check that did not verify anything (its feature is not
++// configured, its dependency is switched off) returns `skip('not configured')` ({ skipped: reason })
++// and reports status `skipped` with the reason, never `ok`. Skipped checks are listed in `skipped`.
++// A skipped REQUIRED check is also `degraded` (yellow: the service cannot say it serves that), and a
++// service is `ready` (green) only when at least one check really passed: none, or all skipped, is
++// `degraded`.
// ═══════════════════════════════════════════════════════════════
const NAME_RE = /^[a-z][a-z0-9_.-]{0,63}$/;
@@@ -63,6 +70,11 @@ function createReadiness({ service, release = null, checks = [], details = null,
const v = await withTimeout(Promise.resolve().then(() => c.check()), c.timeoutMs);
if (v === false) { out.status = 'fail'; out.error = 'check returned false'; }
else if (typeof v === 'string') { out.status = 'fail'; out.error = safeReason(v); }
++ else if (v && typeof v === 'object' && !Array.isArray(v) && v.skipped) {
++ out.status = 'skipped';
++ out.reason = safeReason(v.skipped === true ? 'not checked' : v.skipped);
++ if (v.detail !== undefined) out.detail = v.detail;
++ }
else if (v && typeof v === 'object' && !Array.isArray(v)) {
if (v.ok === false) { out.status = 'fail'; out.error = safeReason(v.error || v.detail || 'failed'); }
if (v.detail !== undefined && (v.ok !== false || typeof v.detail === 'object')) out.detail = v.detail;
@@@ -84,20 +96,24 @@ function createReadiness({ service, release = null, checks = [], details = null,
async function run() {
const results = await Promise.all(list.map(runOne));
const out = {};
-- const failed = []; const degraded = [];
++ const failed = []; const degraded = []; const skipped = [];
list.forEach((c, i) => {
out[c.name] = results[i];
-- if (results[i].status !== 'ok') (c.required ? failed : degraded).push(c.name);
++ if (results[i].status === 'skipped') { skipped.push(c.name); if (c.required) degraded.push(c.name); }
++ else if (results[i].status !== 'ok') (c.required ? failed : degraded).push(c.name);
});
const ready = failed.length === 0;
++ // Green is built from checks that passed, never from skipped ones or from none.
++ const verified = results.some((r) => r.status === 'ok');
const body = {
ready,
-- status: !ready ? 'not_ready' : degraded.length ? 'degraded' : 'ready',
++ status: !ready ? 'not_ready' : degraded.length || !verified ? 'degraded' : 'ready',
service,
release: typeof release === 'function' ? release() : release,
checked_at: now().toISOString(),
failed,
degraded,
++ skipped,
checks: out,
};
if (details) {
@@@ -112,7 +128,7 @@ function createReadiness({ service, release = null, checks = [], details = null,
async function handler(_req, res) {
let body;
try { body = await run(); } catch (err) {
-- body = { ready: false, status: 'not_ready', service, checked_at: now().toISOString(), failed: ['readiness'], degraded: [], checks: {}, error: safeReason(err) };
++ body = { ready: false, status: 'not_ready', service, checked_at: now().toISOString(), failed: ['readiness'], degraded: [], skipped: [], checks: {}, error: safeReason(err) };
}
res.statusCode = body.ready ? 200 : 503;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
@@@ -123,4 +139,9 @@ function createReadiness({ service, release = null, checks = [], details = null,
return { run, handler, checks: list.map((c) => ({ name: c.name, required: c.required })) };
}
--module.exports = { createReadiness, safeReason };
++/** What a check returns when it verified nothing: status `skipped` with this reason, never `ok`. */
++function skip(reason = 'not checked', detail) {
++ return detail === undefined ? { skipped: String(reason || 'not checked') } : { skipped: String(reason || 'not checked'), detail };
++}
++
++module.exports = { createReadiness, safeReason, skip };
ddiff --git a/test/ready.test.js b/test/ready.test.js
iindex d7a5170..0ab2e69 100644
---- a/test/ready.test.js
++++ b/test/ready.test.js
@@@ -1,8 +1,9 @@
'use strict';
// ready.js: required vs optional checks, status/latency/checked_at on every check, timeouts,
--// cached checks keep the time they really ran, and failure reasons never leak secrets.
++// cached checks keep the time they really ran, failure reasons never leak secrets, and skipped checks
++// never make a service green (roadmap WS-Q task 7).
const assert = require('assert');
--const { createReadiness, safeReason } = require('../ready');
++const { createReadiness, safeReason, skip } = require('../ready');
(async () => {
assert.throws(() => createReadiness({}), /service/);
@@@ -75,9 +76,38 @@ const { createReadiness, safeReason } = require('../ready');
assert.strictEqual(res2.statusCode, 200);
assert.strictEqual(JSON.parse(res2.body).checks.falsey.error, 'check returned false');
++ // No fake green: nothing checked is not green.
const none = await createReadiness({ service: 'x' }).run();
assert.strictEqual(none.ready, true);
-- assert.strictEqual(none.status, 'ready');
++ assert.strictEqual(none.status, 'degraded', 'no check ran: not green');
++ assert.deepStrictEqual(none.skipped, []);
++
++ // Skipped checks: status skipped with the reason, never ok; an optional one leaves a verified service green,
++ // a required one is yellow, and a service whose every check was skipped is yellow.
++ const withSkips = createReadiness({ service: 'x', checks: [
++ { name: 'db', check: () => true },
++ { name: 'discord_bot', required: false, check: () => skip('not configured') },
++ { name: 'legacy', required: false, check: () => ({ skipped: true }) },
++ ] });
++ body = await withSkips.run();
++ assert.strictEqual(body.status, 'ready', 'optional skips do not turn a verified service yellow');
++ assert.deepStrictEqual(body.skipped, ['discord_bot', 'legacy']);
++ assert.deepStrictEqual(body.degraded, []);
++ assert.deepStrictEqual([body.checks.discord_bot.status, body.checks.discord_bot.reason], ['skipped', 'not configured']);
++ assert.strictEqual(body.checks.legacy.reason, 'not checked');
++ assert.ok(!('error' in body.checks.discord_bot));
++ const reqSkip = createReadiness({ service: 'x', checks: [
++ { name: 'db', check: () => true },
++ { name: 'storage', check: () => skip('bucket not configured', { bucket: null }) },
++ ] });
++ body = await reqSkip.run();
++ assert.strictEqual(body.ready, true, 'a skip is not a failure');
++ assert.strictEqual(body.status, 'degraded', 'a skipped required check is yellow');
++ assert.deepStrictEqual(body.degraded, ['storage']);
++ assert.deepStrictEqual(body.checks.storage.detail, { bucket: null });
++ body = await createReadiness({ service: 'x', checks: [{ name: 'a', required: false, check: () => skip('off') }] }).run();
++ assert.strictEqual(body.status, 'degraded', 'every check skipped: not green');
++ assert.deepStrictEqual(skip(), { skipped: 'not checked' });
assert.strictEqual(safeReason(Object.assign(new Error('x'), { name: 'AbortError' })), 'timeout');
assert.strictEqual(safeReason('bad token=abc123&x=1'), 'bad token=…&x=1');