Compare commits

...
3 Commits
Author SHA1 Message Date
f79770cdb0 release: prepare MemHub 1.0.2 (#19)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:54:14 +08:00
9ba602ffd2 ci: upload tagged Linux runtimes to draft releases (#16)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:51:58 +08:00
842fbe6044 fix(memhub): relaunch safely with existing runtimes (#18)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:50:23 +08:00
27 changed files with 233 additions and 34 deletions

No files matched your search

+62
View File
@@ -102,3 +102,65 @@ jobs:
if-no-files-found: error
compression-level: 0
retention-days: 14
publish-draft-release:
if: ${{ inputs.run_lifecycle && startsWith(github.ref, 'refs/tags/v') }}
needs: matrix
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: memhub-runtime-linux-*
path: memhub/release-assets
merge-multiple: true
- name: Verify tag and Linux release assets
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
node scripts/verify-release-version.mjs \
--channel stable \
--version "$version" \
--build-number "$(node -p 'require("./package.json").release.build_number')" \
--tag "$GITHUB_REF_NAME"
for arch in x64 arm64; do
archive="release-assets/memhub-runtime-v${version}-linux-${arch}.tar.gz"
test -f "$archive"
test -f "$archive.sha256"
(
cd release-assets
sha256sum -c "$(basename "$archive.sha256")"
)
done
- name: Upload Linux assets to a draft GitHub Release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
if gh release view "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
test "$(
gh release view "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--json isDraft \
--jq .isDraft
)" = "true"
else
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
--generate-notes \
--title "$GITHUB_REF_NAME"
fi
gh release upload "$GITHUB_REF_NAME" \
release-assets/memhub-runtime-v*-linux-*.tar.gz \
release-assets/memhub-runtime-v*-linux-*.tar.gz.sha256 \
--repo "$GITHUB_REPOSITORY" \
--clobber
+11 -2
View File
@@ -16,11 +16,20 @@ Hy Memory and MemHub are versioned and released independently.
Because PyPI release metadata is immutable, the next SDK patch release must
carry the updated license metadata; `2.0.1` retains its published metadata.
## MemHub 1.0.1 — Unreleased
## MemHub 1.0.2 — Unreleased
- Updated the source version to `1.0.1` (build 5).
- Updated the source version to `1.0.2` (build 6).
- Pinned the public SDK to `hy-memory==2.0.2`.
- Pinned `onnxruntime==1.23.2` to retain macOS x64 packaging support.
- Fixed relaunch and upgrade failures when a prior managed Runtime exists.
- Embedded canonical build identity when an external packaging pipeline omits
release environment fields.
- Added automatic Linux Runtime upload to tagged Draft Releases.
## MemHub 1.0.1 — Superseded before release
- The candidate tag was not published as a GitHub Release; `1.0.2` replaces it
after the managed-Runtime relaunch fix.
## MemHub 1.0.0
+3 -3
View File
@@ -6,13 +6,13 @@ SDK 和记忆引擎。One Memory / `one-memory` 是历史模块、数据目录
## 发行与平台合同
公共 Python SDK 为 `hy-memory==2.0.2`。MemHub 源码版本为 `1.0.1`
(build 5),GitHub 仓库为
公共 Python SDK 为 `hy-memory==2.0.2`。MemHub 源码版本为 `1.0.2`
(build 6),GitHub 仓库为
[`Tencent-Hunyuan/Hy-Memory`](https://github.com/Tencent-Hunyuan/Hy-Memory),npm 包名为
`memhub-cli`。当前代码架构见
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。
`v1.0.1` 的发行目标是 Linux x64/arm64 Runtime 与 macOS arm64/x64 App 和 Runtime。
`v1.0.2` 的发行目标是 Linux x64/arm64 Runtime 与 macOS arm64/x64 App 和 Runtime。
Linux 制品由 GitHub Actions 在原生架构 runner 上构建并完成生命周期验收;macOS
制品由发行流水线完成 Developer ID 签名、公证和验收后手动上传。Windows 不支持。
只有同时出现在 GitHub Release 与 `release-manifest.json` 中、且通过公开校验的制品才属于
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/cli",
"version": "1.0.1",
"version": "1.0.2",
"description": "MemHub bootstrap CLI for installing and managing the local-first shared memory runtime",
"license": "Apache-2.0",
"repository": {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project]
name = "one-memory"
version = "1.0.1"
version = "1.0.2"
description = "MemHub local-first memory core"
readme = "README.md"
requires-python = ">=3.12"
+1 -1
View File
@@ -1801,7 +1801,7 @@ wheels = [
[[package]]
name = "one-memory"
version = "1.0.1"
version = "1.0.2"
source = { editable = "." }
dependencies = [
{ name = "aiomysql" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/desktop",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"main": "dist/main.js",
"scripts": {
@@ -0,0 +1,27 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { expect, it } from "vitest";
it("bakes fallback release identity when the packaging pipeline omits it", async () => {
const config = await readFile(
path.resolve(__dirname, "../tsup.config.ts"),
"utf8",
);
expect(config).toContain("resolveBuildNumber");
expect(config).toContain(
"const releaseBuildNumber = resolveBuildNumber(process.env)",
);
expect(config).toMatch(
/__ONE_MEMORY_BUILD_NUMBER__: JSON\.stringify\(\s*String\(releaseBuildNumber\)/,
);
expect(config).toMatch(
/process\.env\.ONE_MEMORY_COMMIT\?\.trim\(\) \|\| readSourceCommit\(\)/,
);
expect(config).toMatch(
/execFileSync\("git", \["rev-parse", "HEAD"\],[\s\S]*?encoding: "utf8"/,
);
expect(config).toContain(
"__ONE_MEMORY_COMMIT__: JSON.stringify(releaseCommit)",
);
});
@@ -234,7 +234,9 @@ describe("desktop lifecycle failure paths", () => {
await f.flush();
expect(f.createWindow).not.toHaveBeenCalled();
expect(f.quit().preventDefault).toHaveBeenCalled();
f.stopping.resolve();
// Teardown can fail after the startup error (for example, while writing a
// diagnostic). It must not replace the useful error with a second dialog.
f.stopping.reject(new Error("diagnostic_detail_key_invalid"));
await f.flush();
// Whatever did come up before the failure still has to be torn down.
expect(f.stop).toHaveBeenCalledTimes(1);
+3 -1
View File
@@ -108,7 +108,9 @@ export function startDesktopLifecycle(options: LifecycleOptions): void {
shutdown = startup
.catch(() => {})
.then(options.stop)
.catch(options.onError)
// Quit-time failures are logged by stop itself. A second modal here can
// only obscure the startup error that initiated this shutdown.
.catch(report)
.finally(() => {
stopped = true;
app.quit();
+4 -3
View File
@@ -124,9 +124,10 @@ export function hourlyRuntimeInstanceId(): string {
* `build_number` is the only ordering authority the installer compares, so a
* package that omits it defaults to 0 and can never outrank an earlier install:
* R->R.1 would be refused as a downgrade. The value is baked into the bundle's
* `LSEnvironment` at pack time (`apps/desktop/scripts/after-pack.cjs`); this
* reads it back and never invents one, so a source checkout that did not pass a
* build number stays undefined rather than fabricating order.
* `LSEnvironment` at pack time (`apps/desktop/scripts/after-pack.cjs`) and is
* also compiled from the canonical root release contract. This reader accepts
* either source so Finder launches still carry ordering even when an external
* packaging pipeline omitted the plist field.
*/
export function resolvePackagedBuildNumber(
environmentValue: unknown,
+1 -1
View File
@@ -1404,7 +1404,7 @@ if (desktopVersion !== null) {
component: "desktop.lifecycle",
event: "desktop.runtime_stop",
outcome: event.outcome,
details: { runtime: event.detail },
details: { runtime_state: event.detail },
});
},
});
+20 -7
View File
@@ -1,12 +1,29 @@
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { defineConfig } from "tsup";
const require = createRequire(import.meta.url);
const { resolveBuildVersion } =
const { resolveBuildNumber, resolveBuildVersion } =
require("../../scripts/release-version.cjs") as {
resolveBuildNumber(environment?: NodeJS.ProcessEnv, root?: string): number;
resolveBuildVersion(environment?: NodeJS.ProcessEnv, root?: string): string;
};
const releaseVersion = resolveBuildVersion(process.env);
const releaseBuildNumber = resolveBuildNumber(process.env);
const releaseCommit =
process.env.ONE_MEMORY_COMMIT?.trim() || readSourceCommit();
function readSourceCommit(): string {
try {
return execFileSync("git", ["rev-parse", "HEAD"], {
cwd: fileURLToPath(new URL("../..", import.meta.url)),
encoding: "utf8",
}).trim();
} catch {
return "unknown";
}
}
export default defineConfig({
entry: ["src/main.ts", "src/preload.ts"],
@@ -27,11 +44,7 @@ export default defineConfig({
__ONE_MEMORY_TRIAL_PROFILE__: JSON.stringify(
process.env.ONE_MEMORY_BUILD_TRIAL_PROFILE ?? null,
),
__ONE_MEMORY_BUILD_NUMBER__: JSON.stringify(
process.env.ONE_MEMORY_BUILD_NUMBER ?? "unknown",
),
__ONE_MEMORY_COMMIT__: JSON.stringify(
process.env.ONE_MEMORY_COMMIT ?? "unknown",
),
__ONE_MEMORY_BUILD_NUMBER__: JSON.stringify(String(releaseBuildNumber)),
__ONE_MEMORY_COMMIT__: JSON.stringify(releaseCommit),
},
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/site",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/web",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"scripts": {
+5 -3
View File
@@ -1,6 +1,6 @@
# MemHub 安装与发行
MemHub `v1.0.1`(build 5)的发行目标如下:
MemHub `v1.0.2`(build 6)的发行目标如下:
| 平台 | 发行物 | 构建与验收 |
| --- | --- | --- |
@@ -12,8 +12,10 @@ MemHub `v1.0.1`(build 5)的发行目标如下:
正式支持以对应 GitHub Release 的 `release-manifest.json` 为准。Manifest 记录版本、
build、Git commit、协议范围、文件大小和 SHA-256;缺少条目的平台不属于该版本支持范围。
Linux Runtime 可通过仓库的 `Linux lifecycle` GitHub Actions 手动构建和下载。macOS
制品由发行流水线完成签名与公证后上传;GitHub 不保存 Apple 私钥。
在 `v*` tag 上手动运行仓库的 `Linux lifecycle` GitHub Actions 后,Linux Runtime
和 SHA-256 文件会自动上传到对应的 Draft Release;普通分支运行只保留 Actions
Artifact。macOS 制品由发行流水线完成签名与公证后上传到同一个 Draft Release;
GitHub 不保存 Apple 私钥。所有平台制品核对完成后再发布 Release。
最低支持 macOS 13。当前更新方式是从 GitHub Releases 手动下载新版本,不包含
auto-updater。
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "one-memory",
"version": "1.0.1",
"version": "1.0.2",
"release": {
"build_number": 5,
"build_number": 6,
"macos_min_version": "13.0",
"app_update": "manual",
"runtime_manifest_schema": 1,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/contracts",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"types": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/host-bridge",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/operations",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"types": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@one-memory/runtime-control",
"version": "1.0.1",
"version": "1.0.2",
"private": true,
"type": "module",
"bin": {
@@ -217,6 +217,50 @@ describe("local Runtime installation", () => {
},
);
it("ignores Finder metadata without weakening payload verification", async () => {
const value = await distribution();
const plan = await planLocalInstall({
source: value.source,
installRoot: value.installRoot,
platform: process.platform,
arch: process.arch,
});
const first = await applyLocalInstall(plan);
await writeFile(path.join(plan.version_directory, ".DS_Store"), "finder");
await expect(
verifyInstalledDistribution(
plan.version_directory,
process.platform,
process.arch,
),
).resolves.toMatchObject({ version: plan.version });
const repeated = await applyLocalInstall(plan);
expect(first.created_version).toBe(true);
expect(repeated.created_version).toBe(false);
await writeFile(
path.join(plan.version_directory, "unexpected-runtime-file"),
"not metadata",
);
await expect(
verifyInstalledDistribution(
plan.version_directory,
process.platform,
process.arch,
),
).rejects.toThrow("distribution_manifest_incomplete");
});
it("does not let a non-file masquerade as Finder metadata", async () => {
const value = await distribution();
await symlink("../outside", path.join(value.source, ".DS_Store"));
await expect(
verifyInstalledDistribution(value.source, process.platform, process.arch),
).rejects.toThrow("distribution_path_invalid");
});
it.each(["extra", "hash"])(
"same-version reuse still rejects %s changes to installed canonical trees",
async (change) => {
@@ -622,6 +622,12 @@ async function treeFiles(root: string, current = root): Promise<string[]> {
entries.sort((left, right) => left.name.localeCompare(right.name));
const result: string[] = [];
for (const entry of entries) {
// Finder may add this metadata file after installation. It is not Runtime
// payload and must not make an otherwise verified install unreusable.
if (entry.name === ".DS_Store") {
if (!entry.isFile()) throw new Error("distribution_path_invalid");
continue;
}
const target = path.join(current, entry.name);
const relative = path.relative(root, target);
if (!safeRelative(relative)) throw new Error("distribution_path_invalid");
@@ -256,6 +256,7 @@ describe("installPackagedRuntime", () => {
});
const selectedBefore = await readlink(current);
const inodeBefore = (await lstat(current)).ino;
await writeFile(path.join(current, ".DS_Store"), "finder metadata");
await expect(
installPackagedRuntime({
resourcesPath: resources,
@@ -429,6 +429,11 @@ async function treeFiles(root: string, current = root): Promise<string[]> {
entries.sort((left, right) => left.name.localeCompare(right.name));
const result: string[] = [];
for (const entry of entries) {
// Finder metadata is not part of the packaged Runtime identity.
if (entry.name === ".DS_Store") {
if (!entry.isFile()) throw new Error("distribution_path_invalid");
continue;
}
const target = path.join(current, entry.name);
const relative = path.relative(root, target);
if (entry.isSymbolicLink()) {
+1 -1
View File
@@ -13,7 +13,7 @@ test("installation docs disclose publication and lifecycle boundaries", async ()
);
assert.match(readme, /公共 Python SDK 为 `hy-memory==2\.0\.2`/);
assert.match(readme, /`v1\.0\.1` 的发行目标是 Linux x64\/arm64 Runtime/);
assert.match(readme, /`v1\.0\.2` 的发行目标是 Linux x64\/arm64 Runtime/);
assert.match(readme, /Linux 制品由 GitHub Actions/);
assert.match(readme, /macOS.*Developer ID 签名、公证/s);
assert.match(readme, /release-manifest\.json/);
@@ -97,4 +97,29 @@ describe("Linux runtime scripts", () => {
assert.match(script, /cleanup-uninstall/);
assert.doesNotMatch(script, /install:\s*["']blocked["']/);
});
it("uploads successful tagged builds only to a draft release", async () => {
const workflow = await readFile(
path.join(directory, "../../../../.github/workflows/linux-lifecycle.yml"),
"utf8",
);
assert.match(
workflow,
/publish-draft-release:[\s\S]*startsWith\(github\.ref, 'refs\/tags\/v'\)/,
);
assert.match(
workflow,
/publish-draft-release:[\s\S]*permissions:\s*\n\s*contents: write/,
);
assert.match(workflow, /actions\/download-artifact@[a-f0-9]{40}/);
assert.match(
workflow,
/verify-release-version\.mjs[\s\S]*--channel stable/,
);
assert.match(workflow, /sha256sum -c/);
assert.match(workflow, /--json isDraft[\s\S]*--jq \.isDraft/);
assert.match(workflow, /gh release create[\s\S]*--draft/);
assert.match(workflow, /gh release upload[\s\S]*--clobber/);
});
});