Skip to content

Commit 1233f53

Browse files
feat: shows emulator version (#487)
* feat: shows emulator version Signed-off-by: David Dal Busco <[email protected]> * feat: read emulator version Signed-off-by: David Dal Busco <[email protected]> * chore: lint Signed-off-by: David Dal Busco <[email protected]> --------- Signed-off-by: David Dal Busco <[email protected]>
1 parent 68b8cd1 commit 1233f53

6 files changed

Lines changed: 180 additions & 31 deletions

File tree

src/commands/version.ts

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
1-
import {isNullish} from '@dfinity/utils';
2-
import {red} from 'kleur';
1+
import {isEmptyString, isNullish} from '@dfinity/utils';
2+
import {green, red} from 'kleur';
33
import {clean} from 'semver';
44
import {version as cliCurrentVersion} from '../../package.json';
5-
import {githubCliLastRelease} from '../rest/github.rest';
6-
import {checkVersion} from '../services/version.services';
5+
import {
6+
githubCliLastRelease,
7+
githubJunoDockerLastRelease,
8+
type GithubLastReleaseResult
9+
} from '../rest/github.rest';
10+
import {findEmulatorVersion} from '../services/emulator/version.services';
11+
import {checkVersion, type CheckVersionResult} from '../services/version.services';
712
import {detectPackageManager} from '../utils/pm.utils';
8-
export const version = async () => {
9-
await cliVersion();
10-
};
1113

12-
const cliVersion = async () => {
13-
const githubRelease = await githubCliLastRelease();
14+
export const version = async () => {
15+
const check = await cliVersion();
1416

15-
if (githubRelease === undefined) {
16-
console.log(red('Cannot fetch last release version of Juno on GitHub 😢.'));
17+
if (check.diff === 'error') {
1718
return;
1819
}
1920

20-
const {tag_name} = githubRelease;
21+
await emulatorVersion();
22+
};
2123

22-
const latestVersion = clean(tag_name);
24+
const cliVersion = async (): Promise<CheckVersionResult> => {
25+
const result = await buildVersionFromGitHub({
26+
release: 'CLI',
27+
releaseFn: githubCliLastRelease
28+
});
2329

24-
if (isNullish(latestVersion)) {
25-
console.log(red(`Cannot extract version from release. Reach out Juno❗️`));
26-
return;
30+
if (result.result === 'error') {
31+
return {diff: 'error'};
2732
}
2833

29-
checkVersion({
34+
const {latestVersion} = result;
35+
36+
return checkVersion({
3037
currentVersion: cliCurrentVersion,
3138
latestVersion,
3239
displayHint: 'CLI',
@@ -46,3 +53,66 @@ const installHint = (): string => {
4653
return 'npm i -g @junobuild/cli';
4754
}
4855
};
56+
57+
const emulatorVersion = async () => {
58+
const emulatorResult = await findEmulatorVersion();
59+
60+
if (emulatorResult.status !== 'success') {
61+
return;
62+
}
63+
64+
const {version: emulatorCurrentVersion} = emulatorResult;
65+
66+
const result = await buildVersionFromGitHub({
67+
release: 'Juno Docker',
68+
releaseFn: githubJunoDockerLastRelease
69+
});
70+
71+
if (result.result === 'error') {
72+
return;
73+
}
74+
75+
const {latestVersion} = result;
76+
77+
// Images prior to v0.6.3 lacked proper metadata in org.opencontainers.image.version.
78+
// Earlier releases contained invalid values such as "0-arm64", while v0.6.2 returned an empty string.
79+
// Note: sanitizing the version read via docker/podman inspect causes these cases to resolve to null.
80+
if (isEmptyString(emulatorCurrentVersion)) {
81+
console.log(`Your Emulator is behind the latest version (${green(`v${latestVersion}`)}).`);
82+
return;
83+
}
84+
85+
checkVersion({
86+
currentVersion: emulatorCurrentVersion,
87+
latestVersion,
88+
displayHint: 'Emulator'
89+
});
90+
};
91+
92+
const buildVersionFromGitHub = async ({
93+
releaseFn,
94+
release
95+
}: {
96+
releaseFn: () => Promise<GithubLastReleaseResult>;
97+
release: 'CLI' | 'Juno Docker';
98+
}): Promise<{result: 'success'; latestVersion: string} | {result: 'error'}> => {
99+
const githubRelease = await releaseFn();
100+
101+
if (githubRelease.status === 'error') {
102+
console.log(red(`Cannot fetch the last version of ${release} on GitHub 😢.`));
103+
return {result: 'error'};
104+
}
105+
106+
const {
107+
release: {tag_name}
108+
} = githubRelease;
109+
110+
const latestVersion = clean(tag_name);
111+
112+
if (isNullish(latestVersion)) {
113+
console.log(red(`Cannot extract version from the ${release} release. Reach out Juno❗️`));
114+
return {result: 'error'};
115+
}
116+
117+
return {result: 'success', latestVersion};
118+
};

src/constants/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const ORBITER_WASM_NAME = 'orbiter';
66
export const NODE_VERSION = 20;
77
export const JUNO_CDN_URL = 'https://cdn.juno.build';
88
export const GITHUB_API_CLI_URL = 'https://api.github.com/repos/junobuild/cli';
9+
export const GITHUB_API_JUNO_DOCKER_URL = 'https://api.github.com/repos/junobuild/juno-docker';
910

1011
/**
1112
* Revoked principals that must not be used.

src/rest/github.rest.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {GITHUB_API_CLI_URL} from '../constants/constants';
1+
import {GITHUB_API_CLI_URL, GITHUB_API_JUNO_DOCKER_URL} from '../constants/constants';
22

33
export interface GitHubAsset {
44
url: string; // 'https://api.github.com/repos/peterpeterparker/dummy/releases/assets/91555492'
@@ -38,16 +38,29 @@ const GITHUB_API_HEADERS: RequestInit = {
3838
}
3939
};
4040

41-
const githubLastRelease = async (apiUrl: string): Promise<GitHubRelease | undefined> => {
42-
const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS);
41+
export type GithubLastReleaseResult =
42+
| {status: 'success'; release: GitHubRelease}
43+
| {status: 'error'; err?: unknown};
4344

44-
if (!response.ok) {
45-
return undefined;
46-
}
45+
const githubLastRelease = async (
46+
apiUrl: string
47+
): Promise<{status: 'success'; release: GitHubRelease} | {status: 'error'; err?: unknown}> => {
48+
try {
49+
const response = await fetch(`${apiUrl}/releases/latest`, GITHUB_API_HEADERS);
50+
51+
if (!response.ok) {
52+
return {status: 'error'};
53+
}
4754

48-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
49-
return await response.json();
55+
const release = await response.json();
56+
return {status: 'success', release};
57+
} catch (err: unknown) {
58+
return {status: 'error', err};
59+
}
5060
};
5161

52-
export const githubCliLastRelease = async (): Promise<GitHubRelease | undefined> =>
62+
export const githubCliLastRelease = async (): Promise<GithubLastReleaseResult> =>
5363
await githubLastRelease(GITHUB_API_CLI_URL);
64+
65+
export const githubJunoDockerLastRelease = async (): Promise<GithubLastReleaseResult> =>
66+
await githubLastRelease(GITHUB_API_JUNO_DOCKER_URL);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {notEmptyString} from '@dfinity/utils';
2+
import {clean} from 'semver';
3+
import {readEmulatorConfig} from '../../configs/emulator.config';
4+
import {inspectImageVersion} from '../../utils/runner.utils';
5+
6+
export const findEmulatorVersion = async (): Promise<
7+
| {status: 'skipped'}
8+
| {status: 'error'; err: unknown}
9+
| {status: 'success'; version: string | undefined | null}
10+
> => {
11+
const parsedResult = await readEmulatorConfig();
12+
13+
if (!parsedResult.success) {
14+
return {status: 'skipped'};
15+
}
16+
17+
const {
18+
config: {derivedConfig}
19+
} = parsedResult;
20+
21+
const inspectResult = await inspectImageVersion(derivedConfig);
22+
23+
if ('err' in inspectResult) {
24+
return {status: 'error', err: inspectResult.err};
25+
}
26+
27+
const {version: versionText} = inspectResult;
28+
29+
const version = notEmptyString(versionText) ? clean(versionText) : undefined;
30+
31+
return {status: 'success', version};
32+
};

src/services/version.services.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const loadSatelliteVersion = async ({
7777
return {result: 'success', version: legacyVersion};
7878
};
7979

80+
export interface CheckVersionResult {
81+
diff: 'up-to-date' | 'outdated' | 'error';
82+
}
83+
8084
export const checkVersion = ({
8185
currentVersion,
8286
latestVersion,
@@ -86,23 +90,25 @@ export const checkVersion = ({
8690
currentVersion: string;
8791
latestVersion: string;
8892
displayHint: string;
89-
commandLineHint: string;
90-
}) => {
93+
commandLineHint?: string;
94+
}): CheckVersionResult => {
9195
const diff = compare(currentVersion, latestVersion);
9296

9397
if (diff === 0) {
9498
console.log(`Your ${displayHint} (${green(`v${currentVersion}`)}) is up-to-date.`);
95-
return;
99+
return {diff: 'up-to-date'};
96100
}
97101

98102
if (diff === 1) {
99103
console.log(yellow(`Your ${displayHint} version is more recent than the latest available 🤔.`));
100-
return;
104+
return {diff: 'error'};
101105
}
102106

103107
console.log(
104108
`Your ${displayHint} (${yellow(`v${currentVersion}`)}) is behind the latest version (${green(
105109
`v${latestVersion}`
106-
)}) available. Run ${cyan(commandLineHint)} to update it.`
110+
)}).${nonNullish(commandLineHint) ? ` Run ${cyan(commandLineHint)} to update it.` : ''}`
107111
);
112+
113+
return {diff: 'outdated'};
108114
};

src/utils/runner.utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,30 @@ export const isContainerRunning = async ({
8787
return {err};
8888
}
8989
};
90+
91+
export const inspectImageVersion = async ({
92+
runner,
93+
image
94+
}: Pick<CliEmulatorDerivedConfig, 'runner' | 'image'>): Promise<
95+
{version: string} | {err: unknown}
96+
> => {
97+
try {
98+
let output = '';
99+
100+
await spawn({
101+
command: runner,
102+
args: [
103+
'inspect',
104+
'--format',
105+
'{{ index .Config.Labels "org.opencontainers.image.version"}}',
106+
image
107+
],
108+
stdout: (o) => (output += o),
109+
silentOut: true
110+
});
111+
112+
return {version: output.trim()};
113+
} catch (err: unknown) {
114+
return {err};
115+
}
116+
};

0 commit comments

Comments
 (0)