diff --git a/src/commands/version.ts b/src/commands/version.ts index a34a71c6..c93725c3 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,32 +1,39 @@ -import {isNullish} from '@dfinity/utils'; -import {red} from 'kleur'; +import {isEmptyString, isNullish} from '@dfinity/utils'; +import {green, red} from 'kleur'; import {clean} from 'semver'; import {version as cliCurrentVersion} from '../../package.json'; -import {githubCliLastRelease} from '../rest/github.rest'; -import {checkVersion} from '../services/version.services'; +import { + githubCliLastRelease, + githubJunoDockerLastRelease, + type GithubLastReleaseResult +} from '../rest/github.rest'; +import {findEmulatorVersion} from '../services/emulator/version.services'; +import {checkVersion, type CheckVersionResult} from '../services/version.services'; import {detectPackageManager} from '../utils/pm.utils'; -export const version = async () => { - await cliVersion(); -}; -const cliVersion = async () => { - const githubRelease = await githubCliLastRelease(); +export const version = async () => { + const check = await cliVersion(); - if (githubRelease === undefined) { - console.log(red('Cannot fetch last release version of Juno on GitHub 😢.')); + if (check.diff === 'error') { return; } - const {tag_name} = githubRelease; + await emulatorVersion(); +}; - const latestVersion = clean(tag_name); +const cliVersion = async (): Promise => { + const result = await buildVersionFromGitHub({ + release: 'CLI', + releaseFn: githubCliLastRelease + }); - if (isNullish(latestVersion)) { - console.log(red(`Cannot extract version from release. Reach out Juno❗️`)); - return; + if (result.result === 'error') { + return {diff: 'error'}; } - checkVersion({ + const {latestVersion} = result; + + return checkVersion({ currentVersion: cliCurrentVersion, latestVersion, displayHint: 'CLI', @@ -46,3 +53,66 @@ const installHint = (): string => { return 'npm i -g @junobuild/cli'; } }; + +const emulatorVersion = async () => { + const emulatorResult = await findEmulatorVersion(); + + if (emulatorResult.status !== 'success') { + return; + } + + const {version: emulatorCurrentVersion} = emulatorResult; + + const result = await buildVersionFromGitHub({ + release: 'Juno Docker', + releaseFn: githubJunoDockerLastRelease + }); + + if (result.result === 'error') { + return; + } + + const {latestVersion} = result; + + // Images prior to v0.6.3 lacked proper metadata in org.opencontainers.image.version. + // Earlier releases contained invalid values such as "0-arm64", while v0.6.2 returned an empty string. + // Note: sanitizing the version read via docker/podman inspect causes these cases to resolve to null. + if (isEmptyString(emulatorCurrentVersion)) { + console.log(`Your Emulator is behind the latest version (${green(`v${latestVersion}`)}).`); + return; + } + + checkVersion({ + currentVersion: emulatorCurrentVersion, + latestVersion, + displayHint: 'Emulator' + }); +}; + +const buildVersionFromGitHub = async ({ + releaseFn, + release +}: { + releaseFn: () => Promise; + release: 'CLI' | 'Juno Docker'; +}): Promise<{result: 'success'; latestVersion: string} | {result: 'error'}> => { + const githubRelease = await releaseFn(); + + if (githubRelease.status === 'error') { + console.log(red(`Cannot fetch the last version of ${release} on GitHub 😢.`)); + return {result: 'error'}; + } + + const { + release: {tag_name} + } = githubRelease; + + const latestVersion = clean(tag_name); + + if (isNullish(latestVersion)) { + console.log(red(`Cannot extract version from the ${release} release. Reach out Juno❗️`)); + return {result: 'error'}; + } + + return {result: 'success', latestVersion}; +}; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 32cd4fdb..6905e7e8 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -6,6 +6,7 @@ export const ORBITER_WASM_NAME = 'orbiter'; export const NODE_VERSION = 20; export const JUNO_CDN_URL = 'https://cdn.juno.build'; export const GITHUB_API_CLI_URL = 'https://api.github.com/repos/junobuild/cli'; +export const GITHUB_API_JUNO_DOCKER_URL = 'https://api.github.com/repos/junobuild/juno-docker'; /** * Revoked principals that must not be used. diff --git a/src/rest/github.rest.ts b/src/rest/github.rest.ts index c92b1e7c..2501984a 100644 --- a/src/rest/github.rest.ts +++ b/src/rest/github.rest.ts @@ -1,4 +1,4 @@ -import {GITHUB_API_CLI_URL} from '../constants/constants'; +import {GITHUB_API_CLI_URL, GITHUB_API_JUNO_DOCKER_URL} from '../constants/constants'; export interface GitHubAsset { url: string; // 'https://api.github.com/repos/peterpeterparker/dummy/releases/assets/91555492' @@ -38,16 +38,29 @@ const GITHUB_API_HEADERS: RequestInit = { } }; -const githubLastRelease = async (apiUrl: string): Promise => { - const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS); +export type GithubLastReleaseResult = + | {status: 'success'; release: GitHubRelease} + | {status: 'error'; err?: unknown}; - if (!response.ok) { - return undefined; - } +const githubLastRelease = async ( + apiUrl: string +): Promise<{status: 'success'; release: GitHubRelease} | {status: 'error'; err?: unknown}> => { + try { + const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS); + + if (!response.ok) { + return {status: 'error'}; + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await response.json(); + const release = await response.json(); + return {status: 'success', release}; + } catch (err: unknown) { + return {status: 'error', err}; + } }; -export const githubCliLastRelease = async (): Promise => +export const githubCliLastRelease = async (): Promise => await githubLastRelease(GITHUB_API_CLI_URL); + +export const githubJunoDockerLastRelease = async (): Promise => + await githubLastRelease(GITHUB_API_JUNO_DOCKER_URL); diff --git a/src/services/emulator/version.services.ts b/src/services/emulator/version.services.ts new file mode 100644 index 00000000..d3dce41a --- /dev/null +++ b/src/services/emulator/version.services.ts @@ -0,0 +1,32 @@ +import {notEmptyString} from '@dfinity/utils'; +import {clean} from 'semver'; +import {readEmulatorConfig} from '../../configs/emulator.config'; +import {inspectImageVersion} from '../../utils/runner.utils'; + +export const findEmulatorVersion = async (): Promise< + | {status: 'skipped'} + | {status: 'error'; err: unknown} + | {status: 'success'; version: string | undefined | null} +> => { + const parsedResult = await readEmulatorConfig(); + + if (!parsedResult.success) { + return {status: 'skipped'}; + } + + const { + config: {derivedConfig} + } = parsedResult; + + const inspectResult = await inspectImageVersion(derivedConfig); + + if ('err' in inspectResult) { + return {status: 'error', err: inspectResult.err}; + } + + const {version: versionText} = inspectResult; + + const version = notEmptyString(versionText) ? clean(versionText) : undefined; + + return {status: 'success', version}; +}; diff --git a/src/services/version.services.ts b/src/services/version.services.ts index 45b7a4db..3fb877ce 100644 --- a/src/services/version.services.ts +++ b/src/services/version.services.ts @@ -77,6 +77,10 @@ const loadSatelliteVersion = async ({ return {result: 'success', version: legacyVersion}; }; +export interface CheckVersionResult { + diff: 'up-to-date' | 'outdated' | 'error'; +} + export const checkVersion = ({ currentVersion, latestVersion, @@ -86,23 +90,25 @@ export const checkVersion = ({ currentVersion: string; latestVersion: string; displayHint: string; - commandLineHint: string; -}) => { + commandLineHint?: string; +}): CheckVersionResult => { const diff = compare(currentVersion, latestVersion); if (diff === 0) { console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`); - return; + return {diff: 'up-to-date'}; } if (diff === 1) { console.log(yellow(`Your ${displayHint} version is more recent than the latest available 🤔.`)); - return; + return {diff: 'error'}; } console.log( `Your ${displayHint} (${yellow(`v${currentVersion}`)}) is behind the latest version (${green( `v${latestVersion}` - )}) available. Run ${cyan(commandLineHint)} to update it.` + )}).${nonNullish(commandLineHint) ? ` Run ${cyan(commandLineHint)} to update it.` : ''}` ); + + return {diff: 'outdated'}; }; diff --git a/src/utils/runner.utils.ts b/src/utils/runner.utils.ts index 1847cc81..8f483c99 100644 --- a/src/utils/runner.utils.ts +++ b/src/utils/runner.utils.ts @@ -87,3 +87,30 @@ export const isContainerRunning = async ({ return {err}; } }; + +export const inspectImageVersion = async ({ + runner, + image +}: Pick): Promise< + {version: string} | {err: unknown} +> => { + try { + let output = ''; + + await spawn({ + command: runner, + args: [ + 'inspect', + '--format', + '{{ index .Config.Labels "org.opencontainers.image.version"}}', + image + ], + stdout: (o) => (output += o), + silentOut: true + }); + + return {version: output.trim()}; + } catch (err: unknown) { + return {err}; + } +};