openvibers/openvibe.host Commits

validate: a git checkout reached through <dir>/current -> <dir>/releases/<id> is a deployed release (Live), so its detached HEAD is not a branch error; tracked changes still are

OP OpenVibers committed 26 Sept 2026 · 2190761230 · 2 files, +28 −1


Changes

ddiff --git a/lib/commands/validate.js b/lib/commands/validate.js
iindex 33b4fcb..721283c 100644
---- a/lib/commands/validate.js
++++ b/lib/commands/validate.js
@@@ -59,7 +59,12 @@ async function validate(ctx, id, opts = {}) {
try {
const g = git(exec, svc);
const [branch, head, dirty] = [await g.branch(), await g.head(), await g.dirty()];
-- if (branch !== svc.branch) add('error', 'checkout', `on branch "${branch}", expected "${svc.branch}"`);
++ // A checkout reached through <dir>/current -> <dir>/releases/<id> is a deployed release (Live's
++ // own deploy script): detached by design, so the branch is not checked; changes still are.
++ const link = branch === 'HEAD' && svc.repo.endsWith('/current') ? await exec.readlink(svc.repo) : null;
++ const release = link && path.resolve(path.dirname(svc.repo), link).startsWith(`${path.dirname(svc.repo)}/releases/`);
++ if (release) add('ok', 'checkout', `release ${link} (detached; deployed by the repository's own script)`);
++ else if (branch !== svc.branch) add('error', 'checkout', `on branch "${branch}", expected "${svc.branch}"`);
add(dirty.length ? 'error' : 'ok', 'checkout', dirty.length ? `tracked local changes: ${dirty.slice(0, 8).join(', ')}` : `clean at ${head.slice(0, 12)}`);
} catch (err) {
add('error', 'checkout', err.message);
ddiff --git a/test/validate.test.js b/test/validate.test.js
iindex 76cc8d8..467cfc0 100644
---- a/test/validate.test.js
++++ b/test/validate.test.js
@@@ -36,6 +36,28 @@ runTests([
assert.ok(res.findings.some((f) => f.level === 'error' && /owned by nobody/.test(f.message)));
}),
++ test('a git checkout reached through current -> releases/<id> is a deployed release: detached is fine, changes are not', async () => {
++ const host = scenario();
++ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));
++ raw.services.rg = { repo: '/opt/rg/current', units: [], lifecycle: lifecycleDoc() };
++ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });
++ const repo = host.createRepo('/opt/rg/current', { branch: 'HEAD' });
++ repo.head = repo.commit({}, { message: 'release' }); // the fake's status does not follow the current symlink
++ host.files.delete('/opt/rg/current');
++ host.put('/opt/rg/releases/20260926-abc/package.json', '{}', { owner: 'ubuntu' });
++ await host.exec.symlink('/opt/rg/releases/20260926-abc', '/opt/rg/current');
++ let res = JSON.parse((await host.cli('validate', 'rg', '--json')).out);
++ assert.ok(!res.findings.some((f) => f.area === 'checkout' && f.level === 'error'), JSON.stringify(res.findings.filter((f) => f.area === 'checkout')));
++ assert.ok(res.findings.some((f) => f.area === 'checkout' && /release \/opt\/rg\/releases\/20260926-abc \(detached/.test(f.message)));
++ // A detached checkout that is not a release still fails the branch check.
++ raw.services.rg2 = { repo: '/opt/rg2', units: [], lifecycle: lifecycleDoc() };
++ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });
++ const r2 = host.createRepo('/opt/rg2', { branch: 'HEAD' });
++ r2.checkout(r2.commit({ 'package.json': '{}' }));
++ res = JSON.parse((await host.cli('validate', 'rg2', '--json')).out);
++ assert.ok(res.findings.some((f) => f.level === 'error' && /on branch "HEAD", expected "main"/.test(f.message)));
++ }),
++
test('env validation reports names only — never a value — in text and JSON output', async () => {
const host = scenario();
host.put('/etc/openvibe/live.env', `JWT_SECRET=${SECRET}\nBASE_URL=\nPAYPAL_CLIENT_SECRET=""\n`, { mode: 0o644 });