mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 06:23:18 +00:00
fix(fleet): resolve version detection pipeline for Docker builds (#402)
* fix(fleet): resolve version detection pipeline for Docker builds The Dockerfile backend-builder stage was missing a COPY of the root package.json, causing generate-version.js to fall back to "0.0.0-dev" at build time. At runtime, the filesystem walk also failed (root package.json not in the final image), producing the string "unknown" which the frontend rendered as "vunknown". Changes: - Dockerfile: copy root package.json into backend-builder stage - CapabilityRegistry: return null (not "unknown") for unresolvable versions; add isValidVersion() type guard; normalize remote meta responses to strip "unknown"/"0.0.0-dev" sentinel values - Fleet endpoints: hoist gateway version validation outside per-node loops; treat unresolvable remote versions as "potentially outdated" instead of silently marking them up to date - FleetView: guard all version display points (card badge, update button, gateway label, modal columns) via shared formatVersion() - EditorLayout, CapabilityGate: use shared isValidVersion utility - New frontend/src/lib/version.ts shared utility - Docs: add troubleshooting section for version display edge cases - Screenshots: updated Fleet Overview and Node Updates modal * docs: update fleet node updates screenshot with live remote node
This commit is contained in:
+15
-5
@@ -30,7 +30,7 @@ import { WebhookService } from './services/WebhookService';
|
||||
import { SSOService } from './services/SSOService';
|
||||
import { SchedulerService } from './services/SchedulerService';
|
||||
import { RegistryService } from './services/RegistryService';
|
||||
import { CAPABILITIES, getSenchoVersion, fetchRemoteMeta, getActiveCapabilities, type RemoteMeta } from './services/CapabilityRegistry';
|
||||
import { CAPABILITIES, getSenchoVersion, isValidVersion, fetchRemoteMeta, getActiveCapabilities, type RemoteMeta } from './services/CapabilityRegistry';
|
||||
import SelfUpdateService from './services/SelfUpdateService';
|
||||
import semver from 'semver';
|
||||
import { CronExpressionParser } from 'cron-parser';
|
||||
@@ -1251,6 +1251,7 @@ app.get('/api/fleet/update-status', async (_req: Request, res: Response): Promis
|
||||
const db = DatabaseService.getInstance();
|
||||
const nodes = db.getNodes();
|
||||
const gatewayVersion = getSenchoVersion();
|
||||
const gatewayValid = isValidVersion(gatewayVersion);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
nodes.map(async (node) => {
|
||||
@@ -1273,6 +1274,14 @@ app.get('/api/fleet/update-status', async (_req: Request, res: Response): Promis
|
||||
}
|
||||
}
|
||||
|
||||
// Assume remote nodes are outdated when their version is unresolvable
|
||||
let updateAvailable = false;
|
||||
if (!isValidVersion(version)) {
|
||||
updateAvailable = node.type === 'remote';
|
||||
} else if (gatewayValid) {
|
||||
updateAvailable = semver.lt(version, gatewayVersion!);
|
||||
}
|
||||
|
||||
const currentTracker = updateTracker.get(node.id);
|
||||
return {
|
||||
nodeId: node.id,
|
||||
@@ -1280,9 +1289,7 @@ app.get('/api/fleet/update-status', async (_req: Request, res: Response): Promis
|
||||
type: node.type,
|
||||
version,
|
||||
latestVersion: gatewayVersion,
|
||||
updateAvailable: version === null
|
||||
? (node.type === 'remote') // Remote node without /api/meta is pre-capability-negotiation — definitely outdated
|
||||
: (version !== gatewayVersion && !!semver.valid(version) && semver.lt(version, gatewayVersion)),
|
||||
updateAvailable,
|
||||
updateStatus: currentTracker?.status ?? null,
|
||||
};
|
||||
})
|
||||
@@ -1391,7 +1398,10 @@ app.post('/api/fleet/update-all', async (req: Request, res: Response): Promise<v
|
||||
|
||||
const results = await Promise.allSettled(candidates.map(async (node) => {
|
||||
const meta = await fetchRemoteMeta(node.api_url!, node.api_token!);
|
||||
if (!meta.version || !semver.valid(meta.version) || !semver.lt(meta.version, gatewayVersion) || !meta.capabilities.includes('self-update')) {
|
||||
if (!meta.capabilities.includes('self-update')) {
|
||||
return { name: node.name, triggered: false };
|
||||
}
|
||||
if (isValidVersion(meta.version) && isValidVersion(gatewayVersion) && !semver.lt(meta.version, gatewayVersion)) {
|
||||
return { name: node.name, triggered: false };
|
||||
}
|
||||
const response = await fetch(`${node.api_url!.replace(/\/$/, '')}/api/system/update`, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import semver from 'semver';
|
||||
import { SENCHO_VERSION } from '../generated/version';
|
||||
|
||||
/**
|
||||
@@ -35,8 +36,13 @@ export const CAPABILITIES = [
|
||||
|
||||
export type Capability = (typeof CAPABILITIES)[number];
|
||||
|
||||
/** Returns true when the string is a usable semver version. */
|
||||
export function isValidVersion(v: string | null | undefined): v is string {
|
||||
return !!v && v !== 'unknown' && v !== '0.0.0-dev' && !!semver.valid(v);
|
||||
}
|
||||
|
||||
// Resolved once per process at import time, then cached.
|
||||
function resolveVersion(): string {
|
||||
function resolveVersion(): string | null {
|
||||
if (SENCHO_VERSION !== '0.0.0-dev') return SENCHO_VERSION;
|
||||
|
||||
// Fallback for manual ts-node runs without the predev hook.
|
||||
@@ -49,12 +55,13 @@ function resolveVersion(): string {
|
||||
} catch { /* not found, keep walking */ }
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
return 'unknown';
|
||||
console.warn('[CapabilityRegistry] Could not resolve Sencho version from any source');
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedVersion = resolveVersion();
|
||||
|
||||
export function getSenchoVersion(): string {
|
||||
export function getSenchoVersion(): string | null {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
@@ -83,8 +90,9 @@ export async function fetchRemoteMeta(baseUrl: string, apiToken: string): Promis
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
timeout: 5000,
|
||||
});
|
||||
const rawVersion: string | undefined = res.data.version;
|
||||
return {
|
||||
version: res.data.version ?? null,
|
||||
version: isValidVersion(rawVersion) ? rawVersion : null,
|
||||
capabilities: Array.isArray(res.data.capabilities) ? res.data.capabilities : [],
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user