mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 09:54:26 +00:00
fix(fleet): resolve ENOENT when triggering remote node self-update (#413)
The execSync call in SelfUpdateService used cwd from Docker Compose labels (host-side path) which does not exist inside the container. Removed cwd (the -f flag provides the absolute compose file path), added explicit shell, added Docker Compose CLI check at startup, and improved error capture via stderr.
This commit is contained in:
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
* **fleet:** fix `spawnSync /bin/sh ENOENT` when triggering remote node self-update. The `execSync` call used `cwd: workingDir` from Docker Compose labels, which is a host-side path that does not exist inside the container. Removed `cwd` (the `-f` flag already provides the absolute compose file path), added `shell: true`, and added a Docker Compose CLI availability check at startup.
|
||||
* **fleet:** fix version detection returning stale value from `generated/version.ts` instead of the authoritative `package.json`. This caused remote nodes to show "unknown" version and false "Update available" badges when both nodes were on the same version. `resolveVersion()` now reads the root `package.json` first and only falls back to the build-time constant if the walk fails.
|
||||
* **fleet:** fix permanently stuck "Timed out" / "Failed" badges after node update attempts. The in-memory update tracker now supports clearing via a new DELETE endpoint, and terminal states are automatically clearable through the Recheck button.
|
||||
* **fleet:** fix update completion detection for remote nodes that cannot report their version. The gateway now uses three completion signals: version change, process restart detection (`startedAt`), and offline/online detection (node went unreachable during update and came back). This eliminates false timeouts on nodes running older Sencho versions.
|
||||
|
||||
@@ -24,7 +24,7 @@ class SelfUpdateService {
|
||||
async initialize(): Promise<void> {
|
||||
const hostname = process.env.HOSTNAME;
|
||||
if (!hostname) {
|
||||
console.log('[SelfUpdate] HOSTNAME not set — self-update unavailable (not running in Docker?)');
|
||||
console.log('[SelfUpdate] HOSTNAME not set - self-update unavailable (not running in Docker?)');
|
||||
disableCapability('self-update');
|
||||
return;
|
||||
}
|
||||
@@ -40,16 +40,25 @@ class SelfUpdateService {
|
||||
const serviceName = labels['com.docker.compose.service'];
|
||||
|
||||
if (!workingDir || !configFiles || !serviceName) {
|
||||
console.log('[SelfUpdate] Container lacks Docker Compose labels — self-update unavailable');
|
||||
console.log('[SelfUpdate] Container lacks Docker Compose labels - self-update unavailable');
|
||||
disableCapability('self-update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify docker compose CLI is available inside the container
|
||||
try {
|
||||
execSync('docker compose version', { shell: '/bin/sh', stdio: 'pipe', timeout: 5000 });
|
||||
} catch {
|
||||
console.log('[SelfUpdate] docker compose CLI not available in container');
|
||||
disableCapability('self-update');
|
||||
return;
|
||||
}
|
||||
|
||||
this.composeContext = { workingDir, configFiles, serviceName };
|
||||
this.canSelfUpdate = true;
|
||||
console.log(`[SelfUpdate] Ready — service="${serviceName}" in ${workingDir}`);
|
||||
console.log(`[SelfUpdate] Ready - service="${serviceName}" in ${workingDir}`);
|
||||
} catch (error) {
|
||||
console.log('[SelfUpdate] Could not inspect own container — self-update unavailable:', (error as Error).message);
|
||||
console.log('[SelfUpdate] Could not inspect own container - self-update unavailable:', (error as Error).message);
|
||||
disableCapability('self-update');
|
||||
}
|
||||
}
|
||||
@@ -70,30 +79,31 @@ class SelfUpdateService {
|
||||
|
||||
triggerUpdate(): void {
|
||||
if (!this.composeContext) return;
|
||||
const { workingDir, configFiles, serviceName } = this.composeContext;
|
||||
const { configFiles, serviceName } = this.composeContext;
|
||||
const env = { ...process.env, PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' };
|
||||
this.lastUpdateError = null;
|
||||
|
||||
console.log(`[SelfUpdate] Pulling latest image for ${serviceName}...`);
|
||||
try {
|
||||
execSync(`docker compose -f ${configFiles} pull ${serviceName}`, {
|
||||
cwd: workingDir,
|
||||
env,
|
||||
shell: '/bin/sh',
|
||||
stdio: 'pipe',
|
||||
timeout: 300_000, // 5 min max for pull
|
||||
});
|
||||
} catch (error) {
|
||||
this.lastUpdateError = (error as Error).message;
|
||||
const stderr = (error as { stderr?: Buffer })?.stderr?.toString().trim();
|
||||
this.lastUpdateError = stderr || (error as Error).message;
|
||||
console.error('[SelfUpdate] Pull failed:', this.lastUpdateError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SelfUpdate] Recreating container for ${serviceName}... (last breath)`);
|
||||
exec(`docker compose -f ${configFiles} up -d --force-recreate ${serviceName}`, {
|
||||
cwd: workingDir,
|
||||
env,
|
||||
shell: '/bin/sh',
|
||||
});
|
||||
// Process will be killed by Docker during recreate — no code runs after this
|
||||
// Process will be killed by Docker during recreate, no code runs after this
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user