Skip to content

Commit 7d6842d

Browse files
authored
Merge pull request #155 from odefun/feat/upgrade-checksum-8306860
2 parents a070e4b + b56e112 commit 7d6842d

File tree

3 files changed

+74
-11
lines changed

3 files changed

+74
-11
lines changed

.github/workflows/release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ jobs:
7373
with:
7474
path: dist
7575

76+
- name: Generate checksums
77+
run: |
78+
find dist -type f -name "ode-*" -exec cp {} dist/ \;
79+
cd dist
80+
sha256sum ode-* > SHA256SUMS
81+
7682
- name: Read version
7783
id: vars
7884
run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
@@ -85,3 +91,4 @@ jobs:
8591
prerelease: false
8692
files: |
8793
dist/**/ode-*
94+
dist/SHA256SUMS

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ode",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "Coding anywhere with your coding agents connected",
55
"module": "packages/core/index.ts",
66
"type": "module",

packages/core/upgrade.ts

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { basename, dirname, join } from "path";
22
import { mkdtemp, chmod, copyFile, rm, rename } from "fs/promises";
33
import { tmpdir } from "os";
4+
import { createHash } from "crypto";
45

56
const LATEST_RELEASE_URL = "https://api.github.com/repos/odefun/ode/releases/latest";
6-
const DOWNLOAD_BASE_URL = "https://github.com/odefun/ode/releases/latest/download";
7+
const RELEASE_DOWNLOAD_BASE_URL = "https://github.com/odefun/ode/releases/download";
78

89
type UpdateCheckResult = {
910
currentVersion: string;
1011
latestVersion: string | null;
1112
isUpdateAvailable: boolean;
1213
};
1314

15+
type LatestReleaseInfo = {
16+
tag: string;
17+
version: string | null;
18+
};
19+
1420
function normalizeVersion(version: string | null | undefined): string | null {
1521
if (!version) return null;
1622
const trimmed = version.trim().replace(/^v/, "");
@@ -31,17 +37,40 @@ function compareVersions(a: string, b: string): number {
3137
return 0;
3238
}
3339

34-
async function fetchLatestVersion(): Promise<string | null> {
40+
async function fetchLatestReleaseInfo(): Promise<LatestReleaseInfo | null> {
3541
try {
3642
const latestResponse = await fetch(LATEST_RELEASE_URL);
3743
if (!latestResponse.ok) return null;
3844
const latest = (await latestResponse.json()) as { tag_name?: string };
39-
return normalizeVersion(latest.tag_name ?? null);
45+
const tag = typeof latest.tag_name === "string" ? latest.tag_name.trim() : "";
46+
if (!tag) return null;
47+
return {
48+
tag,
49+
version: normalizeVersion(tag),
50+
};
4051
} catch {
4152
return null;
4253
}
4354
}
4455

56+
function sha256Hex(data: Uint8Array): string {
57+
return createHash("sha256").update(data).digest("hex");
58+
}
59+
60+
function parseSha256SumFile(content: string, assetName: string): string | null {
61+
for (const line of content.split(/\r?\n/)) {
62+
const trimmed = line.trim();
63+
if (!trimmed) continue;
64+
const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
65+
if (!match) continue;
66+
const hash = match[1]?.toLowerCase();
67+
const fileName = basename((match[2] ?? "").trim());
68+
if (fileName !== assetName) continue;
69+
return hash ?? null;
70+
}
71+
return null;
72+
}
73+
4574
function resolveAsset(): string {
4675
const platform = process.platform;
4776
const arch = process.arch;
@@ -69,7 +98,8 @@ export function isInstalledBinary(): boolean {
6998

7099
export async function checkForUpdate(currentVersion: string): Promise<UpdateCheckResult> {
71100
const normalizedCurrent = normalizeVersion(currentVersion) ?? "0.0.0";
72-
const latestVersion = await fetchLatestVersion();
101+
const latestRelease = await fetchLatestReleaseInfo();
102+
const latestVersion = latestRelease?.version ?? null;
73103
if (!latestVersion) {
74104
return {
75105
currentVersion: normalizedCurrent,
@@ -86,17 +116,43 @@ export async function checkForUpdate(currentVersion: string): Promise<UpdateChec
86116
}
87117

88118
export async function performUpgrade(): Promise<{ latestVersion: string | null }> {
89-
const latestVersion = await fetchLatestVersion();
119+
const latestRelease = await fetchLatestReleaseInfo();
120+
if (!latestRelease?.tag) {
121+
throw new Error("Failed to resolve latest release tag");
122+
}
123+
124+
const latestVersion = latestRelease.version;
90125
const asset = resolveAsset();
91-
const url = `${DOWNLOAD_BASE_URL}/${asset}`;
92-
const response = await fetch(url);
93-
if (!response.ok) {
94-
throw new Error(`Failed to download ${url} (${response.status})`);
126+
const downloadBaseUrl = `${RELEASE_DOWNLOAD_BASE_URL}/${encodeURIComponent(latestRelease.tag)}`;
127+
const binaryUrl = `${downloadBaseUrl}/${asset}`;
128+
const checksumsUrl = `${downloadBaseUrl}/SHA256SUMS`;
129+
130+
const [binaryResponse, checksumsResponse] = await Promise.all([
131+
fetch(binaryUrl),
132+
fetch(checksumsUrl),
133+
]);
134+
if (!binaryResponse.ok) {
135+
throw new Error(`Failed to download ${binaryUrl} (${binaryResponse.status})`);
136+
}
137+
if (!checksumsResponse.ok) {
138+
throw new Error(`Failed to download ${checksumsUrl} (${checksumsResponse.status})`);
139+
}
140+
141+
const [data, checksumsContent] = await Promise.all([
142+
binaryResponse.arrayBuffer().then((buf) => new Uint8Array(buf)),
143+
checksumsResponse.text(),
144+
]);
145+
const expectedHash = parseSha256SumFile(checksumsContent, asset);
146+
if (!expectedHash) {
147+
throw new Error(`SHA256SUMS missing entry for ${asset}`);
148+
}
149+
const actualHash = sha256Hex(data);
150+
if (actualHash !== expectedHash) {
151+
throw new Error(`Checksum mismatch for ${asset}`);
95152
}
96153

97154
const tempDir = await mkdtemp(join(tmpdir(), "ode-upgrade-"));
98155
const tempPath = join(tempDir, asset);
99-
const data = new Uint8Array(await response.arrayBuffer());
100156
await Bun.write(tempPath, data);
101157
if (process.platform !== "win32") {
102158
await chmod(tempPath, 0o755);

0 commit comments

Comments
 (0)