feat(sdk): warn when local server image is behind latest (#20352)
Closes #20328. ## Summary - Adds a CLI-side check that warns when `twenty-app-dev` is older than the latest published Docker Hub tag. - Reads `APP_VERSION` from the running container via `docker inspect` — no server endpoint, no version exposed publicly. (`APP_VERSION` is already baked in by `packages/twenty-docker/twenty/Dockerfile` for both `twenty` and `twenty-app-dev` targets.) - Fetches latest semver tag from Docker Hub (same API the admin panel already uses) and caches the result for 24h in `~/.twenty/version-check-cache.json`. - Wired into `twenty dev`, `twenty install`, and `twenty server start`. - Best-effort: silent on container-missing / docker / network errors, never blocks a command. ## Why CLI-side instead of a `/healthz` extension The original issue suggested comparing the running server version against Docker Hub. Exposing the running version on a public endpoint has a small but real security cost (helps attackers fingerprint vulnerable deployments), and the version is already inside the image — so the CLI can read it directly without ever calling the server. ## Test plan - [x] `nx run twenty-sdk:test` — added unit tests for `parseSemver` / `compareSemver` - [x] `nx run twenty-sdk:typecheck` - [x] `nx run twenty-sdk:lint` - [ ] Manual: with an old `twenty-app-dev` image running, run `yarn twenty install` → see warning - [ ] Manual: with an up-to-date image, run `yarn twenty dev` → no warning, cache file written - [ ] Manual: no container at all → no warning, no error
This commit is contained in:
@@ -4,6 +4,7 @@ import { type CheckServerOrchestratorStepOutput } from '@/cli/utilities/dev/orch
|
||||
import { type StartWatchersOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
import { type SyncApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
|
||||
import { type UploadFilesOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
|
||||
import { type VersionInfo } from '@/cli/utilities/version/version-info';
|
||||
import { type Manifest, SyncableEntity } from 'twenty-shared/application';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
@@ -114,6 +115,8 @@ export class OrchestratorState {
|
||||
|
||||
pipeline: OrchestratorStatePipeline;
|
||||
|
||||
versionInfo: VersionInfo | null;
|
||||
|
||||
entities: Map<string, OrchestratorStateEntityInfo>;
|
||||
events: OrchestratorStateEvent[];
|
||||
|
||||
@@ -172,10 +175,17 @@ export class OrchestratorState {
|
||||
appName: null,
|
||||
};
|
||||
|
||||
this.versionInfo = null;
|
||||
|
||||
this.entities = new Map();
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
setVersionInfo(versionInfo: VersionInfo): void {
|
||||
this.versionInfo = versionInfo;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
notify(): void {
|
||||
this.onChange?.();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DevUiEntitySection,
|
||||
ENTITY_ORDER,
|
||||
} from '@/cli/utilities/dev/ui/components/dev-ui-entity-section';
|
||||
import { DevUiVersionRow } from '@/cli/utilities/dev/ui/components/dev-ui-version-row';
|
||||
import React from 'react';
|
||||
|
||||
export const DevUiSyncStatusIndicator = ({
|
||||
@@ -96,6 +97,7 @@ export const DevUiApplicationPanel = ({
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<DevUiVersionRow versionInfo={state.versionInfo} />
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={2} flexDirection="column" marginTop={1}>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context';
|
||||
import { type VersionInfo } from '@/cli/utilities/version/version-info';
|
||||
import React from 'react';
|
||||
|
||||
const STALE_THRESHOLD_DAYS = 7;
|
||||
|
||||
export const DevUiVersionRow = ({
|
||||
versionInfo,
|
||||
}: {
|
||||
versionInfo: VersionInfo | null;
|
||||
}): React.ReactElement | null => {
|
||||
const { Box, Text } = useInk();
|
||||
|
||||
if (versionInfo === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
cliVersion,
|
||||
localServerVersion,
|
||||
latestServerVersion,
|
||||
isMinorOrMajorBehind,
|
||||
daysBehind,
|
||||
} = versionInfo;
|
||||
|
||||
const isStale =
|
||||
isMinorOrMajorBehind &&
|
||||
daysBehind !== null &&
|
||||
daysBehind > STALE_THRESHOLD_DAYS;
|
||||
|
||||
const isUpToDate =
|
||||
localServerVersion !== null &&
|
||||
latestServerVersion !== null &&
|
||||
localServerVersion === latestServerVersion;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text dimColor>Versions: </Text>
|
||||
<Text dimColor>CLI </Text>
|
||||
<Text>v{cliVersion}</Text>
|
||||
{localServerVersion !== null && (
|
||||
<>
|
||||
<Text dimColor> • Server </Text>
|
||||
<Text color={isStale ? 'yellow' : undefined}>
|
||||
v{localServerVersion}
|
||||
</Text>
|
||||
{isUpToDate && <Text color="green"> ✓ latest</Text>}
|
||||
{isStale && latestServerVersion !== null && (
|
||||
<>
|
||||
<Text color="yellow"> → v{latestServerVersion}</Text>
|
||||
{daysBehind !== null && (
|
||||
<Text dimColor> ({daysBehind}d behind)</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user