openvibers/openvibe-git Commits

Forge runner: log like the service, and use the container's paths (#32)

OP OpenVibers committed 2 days ago · 181af14c5e · 5 files, +27 −19

* Forge runner: log like the service, and use the container's paths

createRunner received console (an object) as log and called it as a function, so the installed service exited before registering. Normalize a logger object the way every other service module does.

The sandbox environment named HOME, npm's cache and PNPM_HOME with the host work path; inside the container they are /workspace/..., so npm tried to create /home/workstation. Use the container path (the host path is only the bind source).

* Forge runner: the relay is CommonJS, and pnpm is in the image

A repository with "type": "module" made node treat .ci/relay.js as ESM; it is now relay.cjs.

Corepack downloads pnpm with Node's fetch, which does not honour http_proxy, so Games could not install. The install script builds localhost/openvibe-ci-node22:1 (node 22 + pnpm 10.6.5) and Games uses it; no run-time download.

Changes

ddiff --git a/deploy/ci/git-ci.example.json b/deploy/ci/git-ci.example.json
iindex c8af98d..feba4cf 100644
---- a/deploy/ci/git-ci.example.json
++++ b/deploy/ci/git-ci.example.json
@@@ -15,7 +15,8 @@
"timeout_seconds": 1800
},
"openvibers/openvibe.games": {
-- "install_command": "corepack enable --install-directory /workspace/.pnpm/bin && pnpm install --frozen-lockfile",
++ "image": "localhost/openvibe-ci-node22:1",
++ "install_command": "pnpm install --frozen-lockfile",
"test_command": "pnpm typecheck && pnpm lint && pnpm test",
"timeout_seconds": 1800
},
ddiff --git a/deploy/ci/install.sh b/deploy/ci/install.sh
iindex 27a074f..30130c3 100755
---- a/deploy/ci/install.sh
++++ b/deploy/ci/install.sh
@@@ -58,6 +58,12 @@ EOF
say "pulling node:22-bookworm into $STORAGE_ROOT (the run image)"
$PODMAN pull docker.io/library/node:22-bookworm
fi
++ # Games builds with pnpm; corepack would have to download it at run time, and Node's fetch has no proxy.
++ if ! $PODMAN image exists localhost/openvibe-ci-node22:1; then
++ say "building localhost/openvibe-ci-node22:1 (node 22 + pnpm 10.6.5)"
++ printf 'FROM docker.io/library/node:22-bookworm\nRUN npm install -g [email protected]\n' \
++ | $PODMAN build --pull=never -t localhost/openvibe-ci-node22:1 -
++ fi
elif command -v bwrap >/dev/null 2>&1; then
say "sandbox engine: bubblewrap $(bwrap --version | awk '{print $2}') (podman is not rootless here)"
else
ddiff --git a/runner/run.js b/runner/run.js
iindex 9a15aed..9fed0e3 100644
---- a/runner/run.js
++++ b/runner/run.js
@@@ -69,6 +69,7 @@ async function cloneAt({ dir, cloneUrl, sha, branch, log, attempts = 24, waitMs
}
function createRunner({ cfg, api, log = console, clock = Date.now }) {
++ const say = typeof log === 'function' ? log : (...args) => log.log(...args);
const engine = detectEngine(cfg.engine);
const rootArgs = engine.kind === 'podman' ? ['--root', cfg.podmanRoot, '--runroot', cfg.podmanRunRoot] : [];
// Only what podman and systemd-run need: never the runner's token or the rest of the service environment.
@@@ -82,9 +83,9 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
};
if (engine.kind === 'podman') {
const image = spawnSync('podman', [...rootArgs, 'image', 'exists', cfg.image], { encoding: 'utf8', timeout: 20_000 });
-- if (image.error || image.status !== 0) log(`[git-ci-runner] image ${cfg.image} is not in ${cfg.podmanRoot}; run deploy/ci/install.sh`);
++ if (image.error || image.status !== 0) say(`[git-ci-runner] image ${cfg.image} is not in ${cfg.podmanRoot}; run deploy/ci/install.sh`);
}
-- log(`[git-ci-runner] sandbox: ${engine.why}; ${Object.keys(cfg.repos).length} repositories configured`);
++ say(`[git-ci-runner] sandbox: ${engine.why}; ${Object.keys(cfg.repos).length} repositories configured`);
let activeKill = null;
let stopping = false;
@@@ -98,7 +99,7 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
fs.mkdirSync(path.join(runDir, '.home'), { recursive: true });
fs.mkdirSync(path.join(runDir, '.cache', 'npm'), { recursive: true });
fs.mkdirSync(path.join(runDir, '.pnpm'), { recursive: true });
-- fs.writeFileSync(path.join(runDir, '.ci', 'relay.js'), RELAY, { mode: 0o600 });
++ fs.writeFileSync(path.join(runDir, '.ci', 'relay.cjs'), RELAY, { mode: 0o600 });
fs.writeFileSync(localLog, '', { mode: 0o600 });
const limits = job.limits || { timeout_seconds: 1800, cpus: 4, memory_mb: 6144 };
@@@ -119,7 +120,7 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
const chunk = buffer;
buffer = '';
lastFlush = clock();
-- try { await api.log(run.id, chunk); } catch (err) { log(`[git-ci-runner] log not sent: ${err.message}`); }
++ try { await api.log(run.id, chunk); } catch (err) { say(`[git-ci-runner] log not sent: ${err.message}`); }
};
const onData = (data) => {
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
@@@ -134,10 +135,10 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
api.heartbeat(run.id).then((out) => {
if (out && out.superseded && !superseded) {
superseded = true;
-- log(`[git-ci-runner] ${run.id} was superseded; stopping it`);
++ say(`[git-ci-runner] ${run.id} was superseded; stopping it`);
if (currentKill) currentKill();
}
-- }).catch((err) => log(`[git-ci-runner] heartbeat: ${err.message}`));
++ }).catch((err) => say(`[git-ci-runner] heartbeat: ${err.message}`));
}, cfg.heartbeatMs);
const sandbox = (phase, command, proxySock) => {
@@@ -157,7 +158,7 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
const left = deadline - clock();
if (left < 5000) return { timedOut: true, code: null, skipped: true };
const box = sandbox(label, script, proxySock);
-- log(`[git-ci-runner] ${run.id} ${label}`);
++ say(`[git-ci-runner] ${run.id} ${label}`);
activeKill = () => box.kill();
const res = await runProcess({
command: box.command, args: box.args, env: sandboxEnv, timeoutMs: left,
@@@ -173,7 +174,7 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
let description = '';
let exitCode = null;
try {
-- log(`[git-ci-runner] ${run.id} ${job.repo.full_name} ${run.branch} ${run.sha.slice(0, 12)}`);
++ say(`[git-ci-runner] ${run.id} ${job.repo.full_name} ${run.branch} ${run.sha.slice(0, 12)}`);
await cloneAt({ dir: runDir, cloneUrl: job.repo.clone_url, sha: run.sha, branch: run.branch, log: (line) => onData(`${line}\n`) });
proxy = createProxy({ allow: cfg.proxyAllow, log: { warn: (line) => onData(`${line}\n`) } });
@@@ -206,7 +207,7 @@ function createRunner({ cfg, api, log = console, clock = Date.now }) {
try {
await api.finish(run.id, { state, description, exit_code: exitCode, duration_ms: duration });
} catch (err) {
-- log(`[git-ci-runner] ${run.id} verdict not sent (${state}): ${err.message}`);
++ say(`[git-ci-runner] ${run.id} verdict not sent (${state}): ${err.message}`);
}
}
fs.rmSync(runDir, { recursive: true, force: true });
ddiff --git a/runner/sandbox.js b/runner/sandbox.js
iindex 18bb6ed..4e03a0a 100644
---- a/runner/sandbox.js
++++ b/runner/sandbox.js
@@@ -39,8 +39,8 @@ function detectEngine(preferred = 'auto') {
throw new Error('no sandbox engine: rootless podman or bubblewrap is required');
}
--const envList = (workspace, extra = {}) => ({
-- HOME: `${workspace}/.home`,
++const envList = (extra = {}) => ({
++ HOME: '/workspace/.home',
TMPDIR: '/tmp',
PATH: '/opt/node/bin:/usr/local/bin:/usr/bin:/bin',
LANG: 'C.UTF-8',
@@@ -48,11 +48,11 @@ const envList = (workspace, extra = {}) => ({
CI: 'true',
NO_COLOR: '1',
TZ: 'UTC',
-- npm_config_cache: `${workspace}/.cache/npm`,
++ npm_config_cache: '/workspace/.cache/npm',
npm_config_fund: 'false',
npm_config_audit: 'false',
npm_config_update_notifier: 'false',
-- PNPM_HOME: `${workspace}/.pnpm`,
++ PNPM_HOME: '/workspace/.pnpm',
...extra,
});
@@@ -60,7 +60,7 @@ const envList = (workspace, extra = {}) => ({
function installScript(command, { proxy = true } = {}) {
return `#!/bin/sh
set -eu
--${proxy ? `node /workspace/.ci/relay.js /workspace/.ci/proxy.sock 3128 &
++${proxy ? `node /workspace/.ci/relay.cjs /workspace/.ci/proxy.sock 3128 &
relay=$!
trap 'kill "$relay" 2>/dev/null || true' EXIT
export http_proxy=http://127.0.0.1:3128 https_proxy=http://127.0.0.1:3128
@@@ -86,7 +86,7 @@ function podmanArgs({ name, workspace, image, limits, proxySock = null, script }
'-v', `${workspace}:/workspace:rw`,
'--stop-timeout', '5',
];
-- for (const [key, value] of Object.entries(envList(workspace))) args.push('-e', `${key}=${value}`);
++ for (const [key, value] of Object.entries(envList())) args.push('-e', `${key}=${value}`);
if (proxySock) args.push('-v', `${proxySock}:/workspace/.ci/proxy.sock:rw`);
args.push(image, '/bin/sh', '-c', script);
return args;
@@@ -127,7 +127,7 @@ function bwrapArgs({ workspace, limits, proxySock = null, script }) {
'--disable-userns', '--die-with-parent', '--new-session', '--cap-drop', 'ALL', '--clearenv',
'--uid', String(UID), '--gid', String(GID), '--hostname', 'ci',
];
-- for (const [key, value] of Object.entries({ ...envList(workspace), PATH: `${nodeBin}:/usr/local/bin:/usr/bin:/bin` })) {
++ for (const [key, value] of Object.entries({ ...envList(), PATH: `${nodeBin}:/usr/local/bin:/usr/bin:/bin` })) {
args.push('--setenv', key, value);
}
args.push('--ro-bind', '/usr', '/usr');
ddiff --git a/test/ci-runner.test.js b/test/ci-runner.test.js
iindex 22623e4..ec13072 100644
---- a/test/ci-runner.test.js
++++ b/test/ci-runner.test.js
@@@ -79,7 +79,7 @@ const { hashToken, parseHash, createCiAuth } = require('../server/ci-auth');
assert.ok(!install.includes('--privileged'));
assert.match(installScript('npm ci'), /http_proxy=http:\/\/127\.0\.0\.1:3128/);
assert.doesNotMatch(installScript('npm test', { proxy: false }), /http_proxy/);
-- assert.equal(envList('/tmp/run-1').HOME, '/tmp/run-1/.home');
++ assert.equal(envList().HOME, '/workspace/.home');
const testPhase = podmanArgs({
name: 'ov-ci-cir_1-test', workspace: '/tmp/run-1', image: 'node:22-bookworm', limits,
@@@ -129,7 +129,7 @@ const { hashToken, parseHash, createCiAuth } = require('../server/ci-auth');
heartbeat: async () => ({ found: true, state: 'running', superseded: false }),
finish: async (_id, verdict) => { finished = verdict; return { ok: true }; },
};
-- const runner = createRunner({ cfg: runCfg, api, log: () => {} });
++ const runner = createRunner({ cfg: runCfg, api, log: { log: () => {}, warn: () => {} } });
const out = await runner.runJob({
run: { id: 'cir_runner_e2e', sha, branch: 'main' },
repo: { full_name: 'openvibers/e2e', clone_url: `file://${bare}`, default_branch: 'main' },