31 lines
1.4 KiB
JavaScript
31 lines
1.4 KiB
JavaScript
import { spawn as nodeSpawn } from 'node:child_process';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const ID_RE = /^[a-z0-9-]+$/;
|
|
|
|
// Runs `ssh voidact@<ip> <action-id>`. The host's authorized_keys pins a forced
|
|
// wrapper (deploy/void-act) that maps the id → systemctl restart <service> from
|
|
// its OWN whitelist. We pass ONLY the id as a single argv element — no shell.
|
|
export function restartService({ ip, actionId }, {
|
|
keyPath = process.env.ACTIONS_SSH_KEY,
|
|
user = process.env.ACTIONS_SSH_USER || 'voidact',
|
|
spawnImpl = nodeSpawn
|
|
} = {}) {
|
|
if (!ID_RE.test(actionId || '')) return Promise.reject(new Error(`invalid action id: ${actionId}`));
|
|
// Pin known_hosts beside the key (writable, void-owned) so the channel doesn't
|
|
// depend on the service's HOME for ~/.ssh.
|
|
const knownHosts = join(dirname(keyPath), 'known_hosts');
|
|
const args = ['-i', keyPath, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new',
|
|
'-o', `UserKnownHostsFile=${knownHosts}`, `${user}@${ip}`, actionId];
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawnImpl('ssh', args);
|
|
let out = '', err = '';
|
|
child.stdout.on('data', (d) => { out += d; });
|
|
child.stderr.on('data', (d) => { err += d; });
|
|
child.on('close', (code) => code === 0
|
|
? resolve({ ok: true, output: out.trim() })
|
|
: reject(new Error(`ssh ${actionId} → exit ${code}: ${err.trim()}`)));
|
|
child.on('error', reject);
|
|
});
|
|
}
|