openvibers/openvibe.shared Commits
Shared 4.1.1: released_at reads the commit time on release-layout services; showcase grids balance 5/6/9 cards (#67)
Changes
@@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking 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).++## 4.1.1 — 2026-10-12++++- **`release.js` reads the commit time on release-layout services.** A release directory belongs to the deploying++ user while the service runs as its own, so git refused it ("detected dubious ownership") and `released_at` fell back++ to the boot time — on openvibe.live and openre.stream, for example. The read-only git calls now pass++ `-c safe.directory=<the release's real path>`, so `released_at` is the commit time again (the sitemaps' static++ `lastmod` and open tabs' release checks use it).++- **Showcase grids balance five, six or nine cards** in three columns on a wide screen (≥ 900 px), instead of four++ and a lonely last row (the Wiki and Events homes showed 4 + 2).++## 4.1.0 — 2026-10-12- **Every `shell.page()` site is an installable web app, with no per-site code.** A new `icons/` directory ships@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.1.0",++ "version": "4.1.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -51,8 +51,14 @@ const KINDS = ['style', 'content', 'script', 'server'];const V1_0 = ['service', 'release', 'released_at', 'booted_at', 'contracts_version', 'packages', 'min_client_release', 'mixed_version_window_hours'];const SCHEMA_PATH = 'openvibe-contracts/contracts/registry/release-manifest.v1.json';++// A release directory belongs to the deploying user (root under ovhost) while the service runs as its own user, and++// git then refuses the repository ("detected dubious ownership"): released_at fell back to the boot time on every++// release-layout service. These reads are read-only, of this release's own checkout, so the directory is named safe++// for them on the command line (git honours safe.directory there), resolved through the `current` symlink.function git(root, args) {-- try { return execFileSync('git', args, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }++ let real = root;++ try { real = fs.realpathSync(root); } catch { /* keep root */ }++ try { return execFileSync('git', ['-c', `safe.directory=${real}`, ...args], { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }}function pkgVersion(name, root) {@@@ -18,6 +18,7 @@.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}++@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}@@@ -184,6 +184,7 @@ const CSS = `.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}++@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}nnew file mode 100644@@@ -0,0 +1,35 @@++'use strict';++// release.js reads the release's commit and its time from git in the release's own checkout, reached the way ovhost's++// release layout reaches it (a `current` symlink), with the directory named safe for those reads: a release owned by++// the deploying user is otherwise refused by git ("dubious ownership") and released_at fell back to the boot time.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { execFileSync } = require('child_process');++const { createRelease } = require('../release');++++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-release-git-'));++try {++ const rel = path.join(tmp, 'releases', 'abc');++ fs.mkdirSync(rel, { recursive: true });++ const env = { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: '[email protected]', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: '[email protected]', GIT_AUTHOR_DATE: '2026-10-01T12:00:00Z', GIT_COMMITTER_DATE: '2026-10-01T12:00:00Z' };++ execFileSync('git', ['init', '-q'], { cwd: rel, env });++ fs.writeFileSync(path.join(rel, 'package.json'), '{"name":"x","version":"1.0.0"}\n');++ execFileSync('git', ['add', '.'], { cwd: rel, env });++ execFileSync('git', ['commit', '-q', '-m', 'release'], { cwd: rel, env });++ const sha = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: rel }).toString().trim();++ const current = path.join(tmp, 'current');++ fs.symlinkSync(rel, current);++++ const m = createRelease({ service: 'probe', root: current, env: {}, schema: null }).manifest();++ assert.strictEqual(m.release, sha, 'the commit, through the symlink');++ assert.strictEqual(m.released_at, '2026-10-01T12:00:00.000Z', "the commit's time, not the boot time");++ assert.notStrictEqual(m.released_at, m.booted_at);++++ const src = fs.readFileSync(path.join(__dirname, '..', 'release.js'), 'utf8');++ assert.match(src, /'-c', `safe\.directory=\$\{real\}`/, 'git is told the release directory is safe to read');++ console.log('release-git: ok');++} finally {++ fs.rmSync(tmp, { recursive: true, force: true });++}
@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,16 @@ 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.
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).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 4.1.1 — 2026-10-12
+
+- **`release.js` reads the commit time on release-layout services.** A release directory belongs to the deploying
+ user while the service runs as its own, so git refused it ("detected dubious ownership") and `released_at` fell back
+ to the boot time — on openvibe.live and openre.stream, for example. The read-only git calls now pass
+ `-c safe.directory=<the release's real path>`, so `released_at` is the commit time again (the sitemaps' static
+ `lastmod` and open tabs' release checks use it).
+- **Showcase grids balance five, six or nine cards** in three columns on a wide screen (≥ 900 px), instead of four
+ and a lonely last row (the Wiki and Events homes showed 4 + 2).
+
## 4.1.0 — 2026-10-12
## 4.1.0 — 2026-10-12
- **Every `shell.page()` site is an installable web app, with no per-site code.** A new `icons/` directory ships
- **Every `shell.page()` site is an installable web app, with no per-site code.** A new `icons/` directory ships
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.1.0",
+ "version": "4.1.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -51,8 +51,14 @@ const KINDS = ['style', 'content', 'script', 'server'];
@@ -51,8 +51,14 @@ const KINDS = ['style', 'content', 'script', 'server'];
const V1_0 = ['service', 'release', 'released_at', 'booted_at', 'contracts_version', 'packages', 'min_client_release', 'mixed_version_window_hours'];
const V1_0 = ['service', 'release', 'released_at', 'booted_at', 'contracts_version', 'packages', 'min_client_release', 'mixed_version_window_hours'];
const SCHEMA_PATH = 'openvibe-contracts/contracts/registry/release-manifest.v1.json';
const SCHEMA_PATH = 'openvibe-contracts/contracts/registry/release-manifest.v1.json';
+// A release directory belongs to the deploying user (root under ovhost) while the service runs as its own user, and
+// git then refuses the repository ("detected dubious ownership"): released_at fell back to the boot time on every
+// release-layout service. These reads are read-only, of this release's own checkout, so the directory is named safe
+// for them on the command line (git honours safe.directory there), resolved through the `current` symlink.
function git(root, args) {
function git(root, args) {
- try { return execFileSync('git', args, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }
+ let real = root;
+ try { real = fs.realpathSync(root); } catch { /* keep root */ }
+ try { return execFileSync('git', ['-c', `safe.directory=${real}`, ...args], { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); } catch { return ''; }
}
}
function pkgVersion(name, root) {
function pkgVersion(name, root) {
@@ -18,6 +18,7 @@
@@ -18,6 +18,7 @@
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
+@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
@@ -184,6 +184,7 @@ const CSS = `
@@ -184,6 +184,7 @@ const CSS = `
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-sec>h2,.sc-cta>h2{font-size:clamp(22px,2.6vw,30px);letter-spacing:-.02em;margin:0 0 6px}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-lede{color:var(--text-secondary,#96a7c2);margin:0 0 18px;max-width:760px;line-height:1.55}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
.sc-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
+@media (min-width:900px){.sc-grid:has(>:nth-child(5):last-child),.sc-grid:has(>:nth-child(6):last-child),.sc-grid:has(>:nth-child(9):last-child){grid-template-columns:repeat(3,minmax(0,1fr))}}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
.sc-card{display:flex;gap:14px;align-items:flex-start;padding:16px;border-radius:16px;border:1px solid var(--border,rgba(255,255,255,.08));background:var(--bg-secondary,#101828);color:inherit;text-decoration:none;transition:border-color .15s,transform .15s}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
a.sc-card:hover,a.sc-card:focus-visible{border-color:var(--accent,#3b82f6);transform:translateY(-2px);outline:0}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
.sc-ic{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;font-size:17px;color:var(--accent-light,#60a5fa);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}
new file mode 100644
new file mode 100644
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+'use strict';
+// release.js reads the release's commit and its time from git in the release's own checkout, reached the way ovhost's
+// release layout reaches it (a `current` symlink), with the directory named safe for those reads: a release owned by
+// the deploying user is otherwise refused by git ("dubious ownership") and released_at fell back to the boot time.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { createRelease } = require('../release');
+
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-release-git-'));
+try {
+ const rel = path.join(tmp, 'releases', 'abc');
+ fs.mkdirSync(rel, { recursive: true });
+ const env = { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: '[email protected]', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: '[email protected]', GIT_AUTHOR_DATE: '2026-10-01T12:00:00Z', GIT_COMMITTER_DATE: '2026-10-01T12:00:00Z' };
+ execFileSync('git', ['init', '-q'], { cwd: rel, env });
+ fs.writeFileSync(path.join(rel, 'package.json'), '{"name":"x","version":"1.0.0"}\n');
+ execFileSync('git', ['add', '.'], { cwd: rel, env });
+ execFileSync('git', ['commit', '-q', '-m', 'release'], { cwd: rel, env });
+ const sha = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: rel }).toString().trim();
+ const current = path.join(tmp, 'current');
+ fs.symlinkSync(rel, current);
+
+ const m = createRelease({ service: 'probe', root: current, env: {}, schema: null }).manifest();
+ assert.strictEqual(m.release, sha, 'the commit, through the symlink');
+ assert.strictEqual(m.released_at, '2026-10-01T12:00:00.000Z', "the commit's time, not the boot time");
+ assert.notStrictEqual(m.released_at, m.booted_at);
+
+ const src = fs.readFileSync(path.join(__dirname, '..', 'release.js'), 'utf8');
+ assert.match(src, /'-c', `safe\.directory=\$\{real\}`/, 'git is told the release directory is safe to read');
+ console.log('release-git: ok');
+} finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+}