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
Changes
@@@ -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);@@@ -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 });
@@ -59,7 +59,12 @@ async function validate(ctx, id, opts = {}) {
@@ -59,7 +59,12 @@ async function validate(ctx, id, opts = {}) {
try {
try {
const g = git(exec, svc);
const g = git(exec, svc);
const [branch, head, dirty] = [await g.branch(), await g.head(), await g.dirty()];
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)}`);
add(dirty.length ? 'error' : 'ok', 'checkout', dirty.length ? `tracked local changes: ${dirty.slice(0, 8).join(', ')}` : `clean at ${head.slice(0, 12)}`);
} catch (err) {
} catch (err) {
add('error', 'checkout', err.message);
add('error', 'checkout', err.message);
@@ -36,6 +36,28 @@ runTests([
@@ -36,6 +36,28 @@ runTests([
assert.ok(res.findings.some((f) => f.level === 'error' && /owned by nobody/.test(f.message)));
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 () => {
test('env validation reports names only — never a value — in text and JSON output', async () => {
const host = scenario();
const host = scenario();
host.put('/etc/openvibe/live.env', `JWT_SECRET=${SECRET}\nBASE_URL=\nPAYPAL_CLIENT_SECRET=""\n`, { mode: 0o644 });
host.put('/etc/openvibe/live.env', `JWT_SECRET=${SECRET}\nBASE_URL=\nPAYPAL_CLIENT_SECRET=""\n`, { mode: 0o644 });