openvibers/openvibe.host Commits
Give Bot its vhost and enable its service during deploy
Changes
@@@ -930,7 +930,10 @@],"nginx": {"repoVhosts": "deploy/nginx/*.conf",-- "installOnDeploy": true++ "installOnDeploy": true,++ "skipVhosts": [++ "openvibe.bot.conf"++ ]},"announce": {"releaseFiles": "dist/*/release.json"@@@ -2110,8 +2113,10 @@}},"bot": {-- "_note": "Not deployed yet; unit, env file, port and database from OpenVibe.Bot's deploy/ directory.",++ "_note": "Not deployed yet. Bot owns openvibe.bot.conf: deploy bot installs its proxy vhost after Sites skips that filename. Before the first deploy, provision bot.env, the database and the existing TLS certificate; use ovhost deploy bot --restart if the checkout is already current so it installs and enables the service unit. Keep Sites on its ovhost deploy path (its legacy fallback still installs every vhost).","repo": "/opt/openvibe.bot",++ "installUnits": true,++ "enableUnits": true,"units": ["openvibe-bot.service"],@@@ -2123,6 +2128,11 @@"required": "from-example"},"port": 4630,++ "nginx": {++ "vhost": "openvibe.bot.conf",++ "repoVhost": "deploy/nginx/openvibe.bot.conf",++ "installOnDeploy": true++ },"ready": {"url": "http://127.0.0.1:4630/api/ready","timeoutSeconds": 30@@@ -185,6 +185,7 @@ function normaliseService(id, raw, defaults) {generated: s.generated || [],preflight: normalisePreflight(s.preflight, where),installUnits: s.installUnits === true,++ enableUnits: s.enableUnits === true,unitsMatch: s.unitsMatch || null,announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },units: s.units || [],@@@ -310,6 +311,7 @@ function normaliseService(id, raw, defaults) {if (n.vhost && !FILE_NAME_RE.test(n.vhost)) throw new InventoryError(`${where}.nginx.vhost must be a file name, not a path`);if (n.repoVhost) rel(n.repoVhost, `${where}.nginx.repoVhost`);if (n.repoVhosts) rel(n.repoVhosts.replace(/\*\.conf$/, 'x.conf'), `${where}.nginx.repoVhosts`);++ if (n.skipVhosts != null && (!Array.isArray(n.skipVhosts) || n.skipVhosts.some((name) => !FILE_NAME_RE.test(name)))) throw new InventoryError(`${where}.nginx.skipVhosts must list vhost file names`);if (n.variant && !['http', 'sse', 'websocket'].includes(n.variant)) throw new InventoryError(`${where}.nginx.variant must be http, sse or websocket`);if (n.certName && !FILE_NAME_RE.test(n.certName)) throw new InventoryError(`${where}.nginx.certName must be a name`);// The tenants database may be a PostgreSQL database (the Host API is on PostgreSQL): the@@@ -168,7 +168,7 @@ async function computePlan(ctx, svc, { mode = 'deploy', to = null, fetch = true,const vhostsRemoved = [];if (svc.nginx && svc.nginx.installOnDeploy && svc.nginx.repoVhosts) {const vdir = path.dirname(svc.nginx.repoVhosts);-- for (const f of changed) if (f.endsWith('.conf') && path.dirname(f) === vdir && (await g.show(target, f)) == null) vhostsRemoved.push(path.basename(f));++ for (const f of changed) if (f.endsWith('.conf') && path.dirname(f) === vdir && !svc.nginx.skipVhosts?.includes(path.basename(f)) && (await g.show(target, f)) == null) vhostsRemoved.push(path.basename(f));}return {@@@ -290,7 +290,7 @@ async function installRepoVhosts(ctx, svc) {let names;if (pattern.endsWith('*.conf')) {const dir = path.dirname(pattern);-- names = ((await ctx.exec.readdir(path.join(svc.repo, dir))) || []).filter((e) => !e.isDir && e.name.endsWith('.conf')).map((e) => path.join(dir, e.name));++ names = ((await ctx.exec.readdir(path.join(svc.repo, dir))) || []).filter((e) => !e.isDir && e.name.endsWith('.conf') && !n.skipVhosts?.includes(e.name)).map((e) => path.join(dir, e.name));} else names = [pattern];const files = [];for (const rel of names.sort()) files.push({ name: path.basename(rel), text: await ctx.exec.readFile(path.join(svc.repo, rel)) });@@@ -313,6 +313,12 @@ async function installUnits(ctx, svc, record) {ctx.log(`installed unit files: ${changed.join(', ')}; daemon-reload`);}record.unitsInstalled = changed;++ if (svc.enableUnits) {++ for (const unit of svc.units) {++ const enabled = await systemd.systemctl(ctx.exec, 'enable', unit);++ if (enabled.code !== 0) throw new Error(`systemctl enable ${unit} failed: ${enabled.stderr.trim()}`);++ }++ }}async function backupBeforeRestart(ctx, svc, record) {@@@ -34,6 +34,10 @@ runTests([assert.strictEqual(inv.services.live.codeDir, '/opt/openvibe.live/current');assert.strictEqual(inv.services.live.release.git, '/opt/openvibe.live/repo');assert.strictEqual(inv.services.openre.release.git, '/opt/openre.stream/repo');++ assert.deepStrictEqual(inv.services.sites.nginx.skipVhosts, ['openvibe.bot.conf']);++ assert.deepStrictEqual([inv.services.bot.nginx.vhost, inv.services.bot.nginx.repoVhost, inv.services.bot.nginx.installOnDeploy], ['openvibe.bot.conf', 'deploy/nginx/openvibe.bot.conf', true]);++ assert.strictEqual(inv.services.bot.installUnits, true);++ assert.strictEqual(inv.services.bot.enableUnits, true);const text = JSON.stringify(raw);assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');}),@@@ -63,6 +67,7 @@ runTests([const base = { owner: 'ubuntu' };assert.throws(() => normalise({ services: { x: { ...base, repo: 'opt/x' } } }), /absolute path/);assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', nginx: { vhost: '../../etc/passwd' } } } }), /file name/);++ assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', nginx: { skipVhosts: ['../../etc/passwd'] } } } }), /vhost file names/);assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', ready: { url: 'http://evil.example/ready' } } } }), /loopback/);assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', protected: { kind: 'sqlite-count', db: '/d.db', sql: 'DELETE FROM vods' } } } }), /SELECT/);assert.throws(() => normalise({ services: { 'X Y': { ...base, repo: '/opt/x' } } }), /service id/);@@@ -179,6 +179,19 @@ runTests([assert.ok(!host.calls.some((c) => c.cmd === 'npm' || c.cmd === 'nginx'));}),++ test('sites: a vhost assigned to another service is never installed over its proxy', async () => {++ const host = sitesHost();++ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));++ raw.services.sites.nginx = { repoVhosts: 'deploy/nginx/*.conf', installOnDeploy: true, skipVhosts: ['openvibe.bot.conf'] };++ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });++ host.put('/etc/nginx/sites-available/openvibe.bot.conf', 'server { proxy_pass http://127.0.0.1:4630; }');++ host.push({ 'deploy/nginx/openvibe.bot.conf': 'server { root /opt/openvibe.sites/dist/openvibe.bot; }' }, 'bot placeholder');++ const r = await host.cli('deploy', 'sites');++ assert.strictEqual(r.code, 0, r.out);++ assert.strictEqual(host.read('/etc/nginx/sites-available/openvibe.bot.conf'), 'server { proxy_pass http://127.0.0.1:4630; }');++ assert.ok(host.read('/etc/nginx/sites-available/openvibe.news.conf'), 'other Sites vhosts still install');++ }),++test('sites: nginx -t failing never reloads nginx: the previous vhosts, checkout and build come back (exit 2)', async () => {const host = sitesHost();assert.strictEqual((await host.cli('deploy', 'sites', '--restart')).code, 0);@@@ -317,6 +330,21 @@ runTests([assert.strictEqual((await lastRecord(host, 'network')).strategy, 'git-checkout');}),++ test('unit bootstrap enables an installed service before its first restart', async () => {++ const host = networkHost();++ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));++ raw.services.network.enableUnits = true;++ raw.services.network.nginx = { vhost: 'openvibe.bot.conf', repoVhost: 'deploy/nginx/openvibe.bot.conf', installOnDeploy: true };++ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });++ host.push({ 'deploy/systemd/openvibe-network.service': '[Service]\nExecStart=node server/index.js\nTimeoutStopSec=20\n', 'deploy/nginx/openvibe.bot.conf': 'server { proxy_pass http://127.0.0.1:4630; }', 'server/index.js': 'net2();' }, 'unit and vhost');++ const r = await host.cli('deploy', 'network');++ assert.strictEqual(r.code, 0, r.out);++ assert.strictEqual(host.read('/etc/nginx/sites-available/openvibe.bot.conf'), 'server { proxy_pass http://127.0.0.1:4630; }');++ const actions = host.calls.filter((c) => c.cmd === 'systemctl').map((c) => c.args[0]);++ assert.ok(actions.indexOf('daemon-reload') < actions.indexOf('enable'), actions.join(', '));++ assert.ok(actions.indexOf('enable') < actions.indexOf('restart'), actions.join(', '));++ }),++// ── what the wrappers probe ──test('capabilities and --version: what a deploy wrapper checks before handing over to ovhost', async () => {const host = gamesHost();
@@ -930,7 +930,10 @@
@@ -930,7 +930,10 @@
],
],
"nginx": {
"nginx": {
"repoVhosts": "deploy/nginx/*.conf",
"repoVhosts": "deploy/nginx/*.conf",
- "installOnDeploy": true
+ "installOnDeploy": true,
+ "skipVhosts": [
+ "openvibe.bot.conf"
+ ]
},
},
"announce": {
"announce": {
"releaseFiles": "dist/*/release.json"
"releaseFiles": "dist/*/release.json"
@@ -2110,8 +2113,10 @@
@@ -2110,8 +2113,10 @@
}
}
},
},
"bot": {
"bot": {
- "_note": "Not deployed yet; unit, env file, port and database from OpenVibe.Bot's deploy/ directory.",
+ "_note": "Not deployed yet. Bot owns openvibe.bot.conf: deploy bot installs its proxy vhost after Sites skips that filename. Before the first deploy, provision bot.env, the database and the existing TLS certificate; use ovhost deploy bot --restart if the checkout is already current so it installs and enables the service unit. Keep Sites on its ovhost deploy path (its legacy fallback still installs every vhost).",
"repo": "/opt/openvibe.bot",
"repo": "/opt/openvibe.bot",
+ "installUnits": true,
+ "enableUnits": true,
"units": [
"units": [
"openvibe-bot.service"
"openvibe-bot.service"
],
],
@@ -2123,6 +2128,11 @@
@@ -2123,6 +2128,11 @@
"required": "from-example"
"required": "from-example"
},
},
"port": 4630,
"port": 4630,
+ "nginx": {
+ "vhost": "openvibe.bot.conf",
+ "repoVhost": "deploy/nginx/openvibe.bot.conf",
+ "installOnDeploy": true
+ },
"ready": {
"ready": {
"url": "http://127.0.0.1:4630/api/ready",
"url": "http://127.0.0.1:4630/api/ready",
"timeoutSeconds": 30
"timeoutSeconds": 30
@@ -185,6 +185,7 @@ function normaliseService(id, raw, defaults) {
@@ -185,6 +185,7 @@ function normaliseService(id, raw, defaults) {
generated: s.generated || [],
generated: s.generated || [],
preflight: normalisePreflight(s.preflight, where),
preflight: normalisePreflight(s.preflight, where),
installUnits: s.installUnits === true,
installUnits: s.installUnits === true,
+ enableUnits: s.enableUnits === true,
unitsMatch: s.unitsMatch || null,
unitsMatch: s.unitsMatch || null,
announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },
announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },
units: s.units || [],
units: s.units || [],
@@ -310,6 +311,7 @@ function normaliseService(id, raw, defaults) {
@@ -310,6 +311,7 @@ function normaliseService(id, raw, defaults) {
if (n.vhost && !FILE_NAME_RE.test(n.vhost)) throw new InventoryError(`${where}.nginx.vhost must be a file name, not a path`);
if (n.vhost && !FILE_NAME_RE.test(n.vhost)) throw new InventoryError(`${where}.nginx.vhost must be a file name, not a path`);
if (n.repoVhost) rel(n.repoVhost, `${where}.nginx.repoVhost`);
if (n.repoVhost) rel(n.repoVhost, `${where}.nginx.repoVhost`);
if (n.repoVhosts) rel(n.repoVhosts.replace(/\*\.conf$/, 'x.conf'), `${where}.nginx.repoVhosts`);
if (n.repoVhosts) rel(n.repoVhosts.replace(/\*\.conf$/, 'x.conf'), `${where}.nginx.repoVhosts`);
+ if (n.skipVhosts != null && (!Array.isArray(n.skipVhosts) || n.skipVhosts.some((name) => !FILE_NAME_RE.test(name)))) throw new InventoryError(`${where}.nginx.skipVhosts must list vhost file names`);
if (n.variant && !['http', 'sse', 'websocket'].includes(n.variant)) throw new InventoryError(`${where}.nginx.variant must be http, sse or websocket`);
if (n.variant && !['http', 'sse', 'websocket'].includes(n.variant)) throw new InventoryError(`${where}.nginx.variant must be http, sse or websocket`);
if (n.certName && !FILE_NAME_RE.test(n.certName)) throw new InventoryError(`${where}.nginx.certName must be a name`);
if (n.certName && !FILE_NAME_RE.test(n.certName)) throw new InventoryError(`${where}.nginx.certName must be a name`);
// The tenants database may be a PostgreSQL database (the Host API is on PostgreSQL): the
// The tenants database may be a PostgreSQL database (the Host API is on PostgreSQL): the
@@ -168,7 +168,7 @@ async function computePlan(ctx, svc, { mode = 'deploy', to = null, fetch = true,
@@ -168,7 +168,7 @@ async function computePlan(ctx, svc, { mode = 'deploy', to = null, fetch = true,
const vhostsRemoved = [];
const vhostsRemoved = [];
if (svc.nginx && svc.nginx.installOnDeploy && svc.nginx.repoVhosts) {
if (svc.nginx && svc.nginx.installOnDeploy && svc.nginx.repoVhosts) {
const vdir = path.dirname(svc.nginx.repoVhosts);
const vdir = path.dirname(svc.nginx.repoVhosts);
- for (const f of changed) if (f.endsWith('.conf') && path.dirname(f) === vdir && (await g.show(target, f)) == null) vhostsRemoved.push(path.basename(f));
+ for (const f of changed) if (f.endsWith('.conf') && path.dirname(f) === vdir && !svc.nginx.skipVhosts?.includes(path.basename(f)) && (await g.show(target, f)) == null) vhostsRemoved.push(path.basename(f));
}
}
return {
return {
@@ -290,7 +290,7 @@ async function installRepoVhosts(ctx, svc) {
@@ -290,7 +290,7 @@ async function installRepoVhosts(ctx, svc) {
let names;
let names;
if (pattern.endsWith('*.conf')) {
if (pattern.endsWith('*.conf')) {
const dir = path.dirname(pattern);
const dir = path.dirname(pattern);
- names = ((await ctx.exec.readdir(path.join(svc.repo, dir))) || []).filter((e) => !e.isDir && e.name.endsWith('.conf')).map((e) => path.join(dir, e.name));
+ names = ((await ctx.exec.readdir(path.join(svc.repo, dir))) || []).filter((e) => !e.isDir && e.name.endsWith('.conf') && !n.skipVhosts?.includes(e.name)).map((e) => path.join(dir, e.name));
} else names = [pattern];
} else names = [pattern];
const files = [];
const files = [];
for (const rel of names.sort()) files.push({ name: path.basename(rel), text: await ctx.exec.readFile(path.join(svc.repo, rel)) });
for (const rel of names.sort()) files.push({ name: path.basename(rel), text: await ctx.exec.readFile(path.join(svc.repo, rel)) });
@@ -313,6 +313,12 @@ async function installUnits(ctx, svc, record) {
@@ -313,6 +313,12 @@ async function installUnits(ctx, svc, record) {
ctx.log(`installed unit files: ${changed.join(', ')}; daemon-reload`);
ctx.log(`installed unit files: ${changed.join(', ')}; daemon-reload`);
}
}
record.unitsInstalled = changed;
record.unitsInstalled = changed;
+ if (svc.enableUnits) {
+ for (const unit of svc.units) {
+ const enabled = await systemd.systemctl(ctx.exec, 'enable', unit);
+ if (enabled.code !== 0) throw new Error(`systemctl enable ${unit} failed: ${enabled.stderr.trim()}`);
+ }
+ }
}
}
async function backupBeforeRestart(ctx, svc, record) {
async function backupBeforeRestart(ctx, svc, record) {
@@ -34,6 +34,10 @@ runTests([
@@ -34,6 +34,10 @@ runTests([
assert.strictEqual(inv.services.live.codeDir, '/opt/openvibe.live/current');
assert.strictEqual(inv.services.live.codeDir, '/opt/openvibe.live/current');
assert.strictEqual(inv.services.live.release.git, '/opt/openvibe.live/repo');
assert.strictEqual(inv.services.live.release.git, '/opt/openvibe.live/repo');
assert.strictEqual(inv.services.openre.release.git, '/opt/openre.stream/repo');
assert.strictEqual(inv.services.openre.release.git, '/opt/openre.stream/repo');
+ assert.deepStrictEqual(inv.services.sites.nginx.skipVhosts, ['openvibe.bot.conf']);
+ assert.deepStrictEqual([inv.services.bot.nginx.vhost, inv.services.bot.nginx.repoVhost, inv.services.bot.nginx.installOnDeploy], ['openvibe.bot.conf', 'deploy/nginx/openvibe.bot.conf', true]);
+ assert.strictEqual(inv.services.bot.installUnits, true);
+ assert.strictEqual(inv.services.bot.enableUnits, true);
const text = JSON.stringify(raw);
const text = JSON.stringify(raw);
assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');
assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');
}),
}),
@@ -63,6 +67,7 @@ runTests([
@@ -63,6 +67,7 @@ runTests([
const base = { owner: 'ubuntu' };
const base = { owner: 'ubuntu' };
assert.throws(() => normalise({ services: { x: { ...base, repo: 'opt/x' } } }), /absolute path/);
assert.throws(() => normalise({ services: { x: { ...base, repo: 'opt/x' } } }), /absolute path/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', nginx: { vhost: '../../etc/passwd' } } } }), /file name/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', nginx: { vhost: '../../etc/passwd' } } } }), /file name/);
+ assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', nginx: { skipVhosts: ['../../etc/passwd'] } } } }), /vhost file names/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', ready: { url: 'http://evil.example/ready' } } } }), /loopback/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', ready: { url: 'http://evil.example/ready' } } } }), /loopback/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', protected: { kind: 'sqlite-count', db: '/d.db', sql: 'DELETE FROM vods' } } } }), /SELECT/);
assert.throws(() => normalise({ services: { x: { ...base, repo: '/opt/x', protected: { kind: 'sqlite-count', db: '/d.db', sql: 'DELETE FROM vods' } } } }), /SELECT/);
assert.throws(() => normalise({ services: { 'X Y': { ...base, repo: '/opt/x' } } }), /service id/);
assert.throws(() => normalise({ services: { 'X Y': { ...base, repo: '/opt/x' } } }), /service id/);
@@ -179,6 +179,19 @@ runTests([
@@ -179,6 +179,19 @@ runTests([
assert.ok(!host.calls.some((c) => c.cmd === 'npm' || c.cmd === 'nginx'));
assert.ok(!host.calls.some((c) => c.cmd === 'npm' || c.cmd === 'nginx'));
}),
}),
+ test('sites: a vhost assigned to another service is never installed over its proxy', async () => {
+ const host = sitesHost();
+ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));
+ raw.services.sites.nginx = { repoVhosts: 'deploy/nginx/*.conf', installOnDeploy: true, skipVhosts: ['openvibe.bot.conf'] };
+ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });
+ host.put('/etc/nginx/sites-available/openvibe.bot.conf', 'server { proxy_pass http://127.0.0.1:4630; }');
+ host.push({ 'deploy/nginx/openvibe.bot.conf': 'server { root /opt/openvibe.sites/dist/openvibe.bot; }' }, 'bot placeholder');
+ const r = await host.cli('deploy', 'sites');
+ assert.strictEqual(r.code, 0, r.out);
+ assert.strictEqual(host.read('/etc/nginx/sites-available/openvibe.bot.conf'), 'server { proxy_pass http://127.0.0.1:4630; }');
+ assert.ok(host.read('/etc/nginx/sites-available/openvibe.news.conf'), 'other Sites vhosts still install');
+ }),
+
test('sites: nginx -t failing never reloads nginx: the previous vhosts, checkout and build come back (exit 2)', async () => {
test('sites: nginx -t failing never reloads nginx: the previous vhosts, checkout and build come back (exit 2)', async () => {
const host = sitesHost();
const host = sitesHost();
assert.strictEqual((await host.cli('deploy', 'sites', '--restart')).code, 0);
assert.strictEqual((await host.cli('deploy', 'sites', '--restart')).code, 0);
@@ -317,6 +330,21 @@ runTests([
@@ -317,6 +330,21 @@ runTests([
assert.strictEqual((await lastRecord(host, 'network')).strategy, 'git-checkout');
assert.strictEqual((await lastRecord(host, 'network')).strategy, 'git-checkout');
}),
}),
+ test('unit bootstrap enables an installed service before its first restart', async () => {
+ const host = networkHost();
+ const raw = JSON.parse(host.read('/etc/openvibe/host.json'));
+ raw.services.network.enableUnits = true;
+ raw.services.network.nginx = { vhost: 'openvibe.bot.conf', repoVhost: 'deploy/nginx/openvibe.bot.conf', installOnDeploy: true };
+ host.put('/etc/openvibe/host.json', JSON.stringify(raw), { mode: 0o640 });
+ host.push({ 'deploy/systemd/openvibe-network.service': '[Service]\nExecStart=node server/index.js\nTimeoutStopSec=20\n', 'deploy/nginx/openvibe.bot.conf': 'server { proxy_pass http://127.0.0.1:4630; }', 'server/index.js': 'net2();' }, 'unit and vhost');
+ const r = await host.cli('deploy', 'network');
+ assert.strictEqual(r.code, 0, r.out);
+ assert.strictEqual(host.read('/etc/nginx/sites-available/openvibe.bot.conf'), 'server { proxy_pass http://127.0.0.1:4630; }');
+ const actions = host.calls.filter((c) => c.cmd === 'systemctl').map((c) => c.args[0]);
+ assert.ok(actions.indexOf('daemon-reload') < actions.indexOf('enable'), actions.join(', '));
+ assert.ok(actions.indexOf('enable') < actions.indexOf('restart'), actions.join(', '));
+ }),
+
// ── what the wrappers probe ──
// ── what the wrappers probe ──
test('capabilities and --version: what a deploy wrapper checks before handing over to ovhost', async () => {
test('capabilities and --version: what a deploy wrapper checks before handing over to ovhost', async () => {
const host = gamesHost();
const host = gamesHost();