/** * `celilo apt-upgrade` — upgrade the deb-installed celilo packages and apply * pending DB migrations. The management server (celilo-bootstrap) installs * celilo via apt, so keeping it current means the apt chain, not `bun update -g` * (that path is `system update`'s self-update, for npm-global installs). * * Steps (ISS-0100 — the postinst does NOT auto-apply migrations): * 1. apt-get update * 2. apt-get -y --only-upgrade install celilo celilo-bootstrap * 3. a FRESH `celilo system migrate` — spawned as the just-installed binary so * the new version's migrations run, not the ones loaded in this process. * * This is the RW target behind the MCP's `celilo_apt_upgrade` tool. It runs as * the celilo user (via api-serve); the apt steps sudo to root, gated by the * scoped /etc/sudoers.d/celilo-apt-upgrade grant that celilo-bootstrap ships. */ import { spawnSync } from 'node:child_process'; import type { CommandResult } from '../types'; /** Wrapper the deb installs; the fresh migrate step runs the upgraded binary. */ const CELILO_BIN = '/usr/local/bin/celilo'; /** One command to run in the chain — argv plus a human label for output. */ interface Step { label: string; argv: string[]; } const STEPS: Step[] = [ { label: 'apt-get update', argv: ['sudo', 'apt-get', 'update'] }, { label: 'apt-get upgrade celilo, celilo-bootstrap', argv: ['sudo', 'apt-get', '-y', '--only-upgrade', 'install', 'celilo', 'celilo-bootstrap'], }, { label: 'apply DB migrations', argv: [CELILO_BIN, 'system', 'migrate'] }, // The dispatcher runs the code it LOADED, not the code on disk. Without this // step celilo-mgr sat 9 days on event-bus v0.1.8 after apt installed v0.2.0, // faithfully running the bug the upgrade shipped to fix (celilo#604). The // upgraded binary does the restart so the verification is the new code's. { label: 'restart the event dispatcher', argv: [CELILO_BIN, 'events', 'restart-daemon'] }, ]; /** Run one argv, inheriting stdio so its output streams through api-serve. */ export type StepRunner = (argv: string[]) => { status: number | null }; const defaultRunner: StepRunner = (argv) => spawnSync(argv[0], argv.slice(1), { stdio: 'inherit' }); export async function handleAptUpgrade( _args: string[], _flags: Record, runStep: StepRunner = defaultRunner, ): Promise { const done: string[] = []; for (const step of STEPS) { process.stdout.write(`\n▸ ${step.label}\n`); const { status } = runStep(step.argv); if (status !== 0) { // Name what DID happen. A failure on the last step means new code is // installed and the dispatcher is still serving the old — the operator // has to be told that in the failure itself, not left to infer it. return { success: false, error: `apt-upgrade failed at "${step.label}" (exit ${status ?? 'signal'}). Completed: ${done.length > 0 ? done.join(', ') : 'nothing'}. Nothing further was run.`, }; } done.push(step.label); } return { success: true, message: 'celilo apt packages upgraded, migrations applied, dispatcher restarted on new code.', }; }