Compare commits

...
Author SHA1 Message Date
alvinfeiandCursor a15e72178e fix(web): distinguish scanned and saved imports
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 20:54:54 +08:00
a04fcb50d5 docs: announce MemHub and CLI releases (#56)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:56:32 +08:00
a28ade2faf fix(release): hash Runtime symlinks consistently (#55)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:56:07 +08:00
1c424928a2 fix(cli): trust GitHub release asset redirects (#54)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:49:40 +08:00
kristinhe1111 688878171e chore(release): bump MemHub build number to 13 (#52) 2026-10-18 18:03:08 +08:00
18 changed files with 271 additions and 52 deletions

No files matched your search

+2 -1
View File
@@ -32,7 +32,8 @@ English | [中文](README.zh.md)
## Latest News
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Released** **Hy Memory 2.0 (`hy-memory==2.0.3`)** on [PyPI](https://pypi.org/project/hy-memory/2.0.3/) and open-sourced the **Hy-Memory** monorepo together with **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 📦 **Released [MemHub v1.0.4](https://github.com/Tencent-Hunyuan/Hy-Memory/releases/tag/v1.0.4)** and **[memhub-cli 1.0.5](https://www.npmjs.com/package/memhub-cli/v/1.0.5)**, with a signed macOS desktop app and verified macOS/Linux Runtimes!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Released** **Hy Memory 2.0 (`hy-memory==2.0.4`)** on [PyPI](https://pypi.org/project/hy-memory/2.0.4/) and open-sourced the **Hy-Memory** monorepo together with **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Open-sourced** **[Hy-Memory-30B-A3B](https://huggingface.co/tencent/Hy-Memory-30B-A3B)** on Hugging Face along with its [technical report](https://arxiv.org/abs/2610.22196) on attributed reinforcement learning for long-term memory writing!
* *<mark>[new]</mark>* 🔥🔥 **[2026-09]**: 📊 **Released** **[CMB: A Coding Memory Benchmark for Long-Horizon and Interleaved Coding Agents](docs/papers/CMB_A_Coding_Memory_Benchmark_for_Long_Horizon_and_Interleaved_Coding_Agents.pdf)** (submitted to **ICLR 2027**).
* *<mark>[new]</mark>* 🔥🔥 **[2026-08]**: 🎉 **Accepted** at **EMNLP 2026**: **[Memory Beyond Recall: A Dual-Process Cognitive Memory System for Self-Evolving LLM Agents](https://arxiv.org/abs/2606.09483)**!
+2 -1
View File
@@ -32,7 +32,8 @@
## 最新动态
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **正式发布** **Hy Memory 2.0 (`hy-memory==2.0.3`)**(已上线 [PyPI](https://pypi.org/project/hy-memory/2.0.3/)),并开源 **Hy-Memory** monorepo 与 **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 📦 **正式发布 [MemHub v1.0.4](https://github.com/Tencent-Hunyuan/Hy-Memory/releases/tag/v1.0.4)** 与 **[memhub-cli 1.0.5](https://www.npmjs.com/package/memhub-cli/v/1.0.5)**,提供已签名的 macOS 桌面应用及经过验证的 macOS/Linux Runtime!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **正式发布** **Hy Memory 2.0 (`hy-memory==2.0.4`)**(已上线 [PyPI](https://pypi.org/project/hy-memory/2.0.4/)),并开源 **Hy-Memory** monorepo 与 **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **模型开源** 在 Hugging Face 发布 **[Hy-Memory-30B-A3B](https://huggingface.co/tencent/Hy-Memory-30B-A3B)** 长期记忆写入模型及[技术报告](https://arxiv.org/abs/2610.22196)!
* *<mark>[new]</mark>* 🔥🔥 **[2026-09]**: 📊 **基准与方法发布** 推出长周期与交错式编程记忆评测基准 **[CMB: A Coding Memory Benchmark for Long-Horizon and Interleaved Coding Agents](docs/papers/CMB_A_Coding_Memory_Benchmark_for_Long_Horizon_and_Interleaved_Coding_Agents.pdf)**(已投稿 **ICLR 2027**)。
* *<mark>[new]</mark>* 🔥🔥 **[2026-08]**: 🎉 **论文录用** 双系统认知记忆架构论文 **[Memory Beyond Recall: A Dual-Process Cognitive Memory System for Self-Evolving LLM Agents](https://arxiv.org/abs/2606.09483)** 被 **EMNLP 2026** 接收!
@@ -12,7 +12,6 @@
* It is generated rather than hand-written so the two cannot drift: this reads
* the real package.json and only strips what is provably not needed at runtime.
*/
import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
@@ -20,12 +19,14 @@ import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const root = resolve(here, "..");
const repositoryRoot = resolve(root, "../..");
const require = createRequire(import.meta.url);
const { resolveBuildVersion } = require("../../../scripts/release-version.cjs");
const releaseVersion = resolveBuildVersion(process.env, repositoryRoot);
const { assertSemver } = require("../../../scripts/release-version.cjs");
const pkgPath = resolve(root, "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const publishVersion = assertSemver(
process.env.MEMHUB_NPM_VERSION?.trim() || pkg.version,
"MEMHUB_NPM_VERSION",
);
const publishName = process.env.MEMHUB_NPM_PACKAGE?.trim();
if (!publishName || !/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(publishName)) {
throw new Error(
@@ -53,7 +54,7 @@ mkdirSync(outDir, { recursive: true });
const published = { ...pkg };
published.name = publishName;
published.version = releaseVersion;
published.version = publishVersion;
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
if (!published[section]) continue;
published[section] = Object.fromEntries(
+6 -5
View File
@@ -331,7 +331,7 @@ import {
renderVersion,
renderVersionDocument,
} from "./usage.js";
import { CLI_VERSION, PRODUCT } from "./version.js";
import { CLI_VERSION, PRODUCT, RUNTIME_RELEASE_VERSION } from "./version.js";
import { runWatchLoop, type WatchEvent, type WatchRead } from "./watch-loop.js";
export interface RunControls {
@@ -417,7 +417,8 @@ async function selectInstallSource(
throw new Error("release_manifest_url_unavailable");
}
const manifestUrl =
explicitManifestUrl ?? template.replaceAll("{version}", CLI_VERSION);
explicitManifestUrl ??
template.replaceAll("{version}", RUNTIME_RELEASE_VERSION);
const allowedHosts = parseAllowedReleaseHosts(
environment.MEMHUB_RELEASE_ALLOWED_HOSTS,
);
@@ -462,7 +463,7 @@ async function resolveRemoteInstallPreview(
): Promise<ResolvedRuntimeRelease> {
return (controls.resolveRuntimeRelease ?? resolveRuntimeRelease)({
manifestUrl: selection.manifestUrl,
expectedVersion: CLI_VERSION,
expectedVersion: RUNTIME_RELEASE_VERSION,
channel: selection.channel,
platform: process.platform,
arch: process.arch,
@@ -476,7 +477,7 @@ async function acquireRemoteInstallSource(
): Promise<AcquiredRuntimeDistribution> {
return (controls.acquireRuntimeDistribution ?? acquireRuntimeDistribution)({
manifestUrl: selection.manifestUrl,
expectedVersion: CLI_VERSION,
expectedVersion: RUNTIME_RELEASE_VERSION,
channel: selection.channel,
platform: process.platform,
arch: process.arch,
@@ -6520,7 +6521,7 @@ function parseInstallOptions(
...(releaseManifestUrl ? { releaseManifestUrl } : {}),
releaseChannel:
releaseChannel ??
(CLI_VERSION.includes("-") ? "prerelease" : "stable"),
(RUNTIME_RELEASE_VERSION.includes("-") ? "prerelease" : "stable"),
installRoot: resolvedInstallRoot,
home: resolvedHome,
uid: uid ?? process.getuid?.(),
+6 -1
View File
@@ -48,7 +48,11 @@ describe("publish manifest is production-safe", () => {
execFileSync("node", ["scripts/prepare-publish-manifest.mjs"], {
cwd: root,
stdio: "pipe",
env: { ...process.env, MEMHUB_NPM_PACKAGE: "@example/memhub-cli" },
env: {
...process.env,
MEMHUB_NPM_PACKAGE: "@example/memhub-cli",
MEMHUB_NPM_VERSION: "1.0.5",
},
});
const published = JSON.parse(
readFileSync(path.join(root, "dist-publish", "package.json"), "utf8"),
@@ -56,6 +60,7 @@ describe("publish manifest is production-safe", () => {
expect(published.dependencies).toBeUndefined();
expect(published.private).toBe(false);
expect(published.name).toBe("@example/memhub-cli");
expect(published.version).toBe("1.0.5");
expect(published.bin).toMatchObject({
memhub: "dist/one-memory.cjs",
"memhub-cli": "dist/one-memory.cjs",
+15 -6
View File
@@ -1,5 +1,9 @@
declare const __ONE_MEMORY_VERSION__: string;
declare const __ONE_MEMORY_BUILD_NUMBER__: string;
declare const __MEMHUB_DEFAULT_RUNTIME_VERSION__: string;
const SEMVER =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
const embeddedVersion =
typeof __ONE_MEMORY_VERSION__ === "string"
@@ -9,13 +13,18 @@ const embeddedBuildNumber =
typeof __ONE_MEMORY_BUILD_NUMBER__ === "string"
? __ONE_MEMORY_BUILD_NUMBER__.trim()
: "";
const embeddedRuntimeVersion =
typeof __MEMHUB_DEFAULT_RUNTIME_VERSION__ === "string"
? __MEMHUB_DEFAULT_RUNTIME_VERSION__.trim()
: "";
export const CLI_VERSION =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(
embeddedVersion,
)
? embeddedVersion
: "development";
export const CLI_VERSION = SEMVER.test(embeddedVersion)
? embeddedVersion
: "development";
export const RUNTIME_RELEASE_VERSION = SEMVER.test(embeddedRuntimeVersion)
? embeddedRuntimeVersion
: CLI_VERSION;
export const CLI_BUILD_NUMBER = /^\d+$/.test(embeddedBuildNumber)
? Number(embeddedBuildNumber)
+14 -2
View File
@@ -2,11 +2,22 @@ import { createRequire } from "node:module";
import { defineConfig } from "tsup";
const require = createRequire(import.meta.url);
const { resolveBuildVersion } =
const { assertSemver, resolveBuildVersion } =
require("../../scripts/release-version.cjs") as {
assertSemver(value: string, label?: string): string;
resolveBuildVersion(environment?: NodeJS.ProcessEnv, root?: string): string;
};
const releaseVersion = resolveBuildVersion(process.env);
const packageVersion = (require("./package.json") as { version: string })
.version;
const cliVersion = assertSemver(
process.env.MEMHUB_NPM_VERSION?.trim() || packageVersion,
"MEMHUB_NPM_VERSION",
);
const runtimeReleaseVersion = assertSemver(
process.env.MEMHUB_DEFAULT_RUNTIME_VERSION?.trim() || releaseVersion,
"MEMHUB_DEFAULT_RUNTIME_VERSION",
);
export default defineConfig({
entry: { "one-memory": "src/entry.ts" },
@@ -15,7 +26,7 @@ export default defineConfig({
outDir: "dist",
clean: true,
define: {
__ONE_MEMORY_VERSION__: JSON.stringify(releaseVersion),
__ONE_MEMORY_VERSION__: JSON.stringify(cliVersion),
__ONE_MEMORY_BUILD_NUMBER__: JSON.stringify(
process.env.ONE_MEMORY_BUILD_NUMBER ?? "unknown",
),
@@ -25,6 +36,7 @@ export default defineConfig({
__MEMHUB_DEFAULT_RELEASE_MANIFEST_URL_TEMPLATE__: JSON.stringify(
process.env.MEMHUB_DEFAULT_RELEASE_MANIFEST_URL_TEMPLATE ?? "",
),
__MEMHUB_DEFAULT_RUNTIME_VERSION__: JSON.stringify(runtimeReleaseVersion),
},
// The shared operations package ships TypeScript sources, so it must be
// inlined into the CJS bundle the same way runtime-control is; otherwise the
+1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
__ONE_MEMORY_BUILD_NUMBER__: JSON.stringify("unknown"),
__ONE_MEMORY_COMMIT__: JSON.stringify("unknown"),
__MEMHUB_DEFAULT_RELEASE_MANIFEST_URL_TEMPLATE__: JSON.stringify(""),
__MEMHUB_DEFAULT_RUNTIME_VERSION__: JSON.stringify(version),
},
test: {
testTimeout: 120_000,
@@ -174,20 +174,20 @@ it("continues to step3 while sample GET is pending without calling completion AP
expect(screen.queryByText(/^已生成.*记忆/)).toBeNull();
});
it.each([
"pending",
"paused",
"interrupted",
"failed",
"cancelled",
"completed",
["pending", "已扫描 100 条,正在筛选"],
["paused", "已扫描 100 条,尚未保存"],
["interrupted", "已扫描 100 条,尚未保存"],
["failed", "已扫描 100 条,尚未保存"],
["cancelled", "已扫描 100 条,尚未保存"],
["completed", "已扫描 100 条,本次没有可保存的对话"],
] as const)(
"shows %s without mutating jobs or fetching missing content",
(status) => {
(status, expected) => {
setup({
jobs: [{ ...job, status, stored: 0 }],
sources: [{ ...source, imported_messages: 0 }],
});
expect(screen.getByText("已扫描 100 条,正在保存")).toBeTruthy();
expect(screen.getByText(expected)).toBeTruthy();
expect(api.receipt).not.toHaveBeenCalled();
},
);
@@ -38,6 +38,71 @@ it("uses the selected job's message count, never the source total or fake percen
expect(container.textContent).not.toContain("%");
expect(container.textContent).not.toContain("持续导入中");
});
it("does not describe scanned-but-skipped history as saved", () => {
const { container } = render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
status: "completed",
discovered: 12_375,
stored: 0,
skipped: 12_375,
history_after: "2026-10-15T00:00:00Z",
},
]}
/>,
);
expect(
screen.getByText("已扫描 12,375 条,本次没有符合时间范围的对话"),
).toBeTruthy();
expect(screen.getByText("导入完成")).toBeTruthy();
expect(container.textContent).not.toContain("正在保存");
expect(container.textContent).not.toContain("已保存 12,375");
});
it("calls an active bounded scan filtering rather than saving", () => {
render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
discovered: 3_522,
stored: 0,
history_after: "2026-10-15T00:00:00Z",
},
]}
/>,
);
expect(screen.getByText("已扫描 3,522 条,正在筛选最近 3 天")).toBeTruthy();
});
it("reports only stored rows as saved after a completed partial import", () => {
const { container } = render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
status: "completed",
discovered: 25_177,
stored: 20_826,
skipped: 4_351,
},
]}
/>,
);
expect(screen.getByText("已保存 20,826 条消息")).toBeTruthy();
expect(screen.getByText("导入完成")).toBeTruthy();
expect(container.textContent).not.toContain("25,177");
});
it.each(["paused", "interrupted", "failed", "completed", "cancelled"] as const)(
"does not animate %s as active work",
(status) => {
+19 -5
View File
@@ -3,6 +3,21 @@ import { useEffect, useLayoutEffect, useRef } from "react";
import { AppMark } from "./app-mark";
import "./r6-import-progress.css";
function progressText(job: ImportJob): string | null {
if (job.stored > 0) return `已保存 ${job.stored.toLocaleString()} 条消息`;
if (job.discovered <= 0) return null;
const scanned = `已扫描 ${job.discovered.toLocaleString()} 条`;
if (job.status === "completed")
return job.history_after
? `${scanned},本次没有符合时间范围的对话`
: `${scanned},本次没有可保存的对话`;
if (job.status === "pending" || job.status === "running")
return job.history_after
? `${scanned},正在筛选最近 3 天`
: `${scanned},正在筛选`;
return `${scanned},尚未保存`;
}
export function R6ImportProgress({
sourceIds,
sources,
@@ -86,8 +101,9 @@ export function R6ImportProgress({
interrupted: "已中断",
failed: "导入遇到问题",
cancelled: "已取消",
completed: "已保存",
completed: "导入完成",
};
const progress = job ? progressText(job) : null;
return (
<article
className="r6-import-progress-card"
@@ -102,10 +118,8 @@ export function R6ImportProgress({
</div>
</div>
<h2>{source?.display_name ?? id}</h2>
{job && job.stored > 0 ? (
<p>{`已保存 ${job.stored.toLocaleString()} 条消息`}</p>
) : job && job.discovered > 0 ? (
<p>{`已扫描 ${job.discovered.toLocaleString()} 条,正在保存`}</p>
{progress ? (
<p>{progress}</p>
) : noImportedHistory ? (
<p>没有可保存的历史对话</p>
) : null}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "one-memory",
"version": "1.0.4",
"release": {
"build_number": 12,
"build_number": 13,
"macos_min_version": "13.0",
"app_update": "manual",
"runtime_manifest_schema": 1,
@@ -312,6 +312,48 @@ describe("remote Runtime acquisition", () => {
});
});
it("allows the exact GitHub Release asset redirect hosts by default", async () => {
const value = await fixture();
const manifestUrl =
"https://github.com/example/project/releases/download/v1.2.3/release-manifest.json";
const artifactUrl =
"https://github.com/example/project/releases/download/v1.2.3/runtime.tar.gz";
const manifestAssetUrl =
"https://release-assets.githubusercontent.com/manifest-download";
const runtimeAssetUrl =
"https://release-assets.githubusercontent.com/runtime-download";
const release = {
...value.release,
artifacts: [{ ...value.artifact, url: artifactUrl }],
};
const temporaryParent = await mkdtemp(
path.join(os.tmpdir(), "memhub-github-release-parent-"),
);
roots.push(temporaryParent);
const acquired = await acquireRuntimeDistribution({
manifestUrl,
expectedVersion: "1.2.3",
channel: "stable",
platform: "linux",
arch: "x64",
temporaryParent,
httpGet: httpFixture({
[manifestUrl]: response(new Uint8Array(), 302, {
location: manifestAssetUrl,
}),
[manifestAssetUrl]: response(Buffer.from(JSON.stringify(release))),
[artifactUrl]: response(new Uint8Array(), 302, {
location: runtimeAssetUrl,
}),
[runtimeAssetUrl]: response(value.archiveBytes),
}),
});
expect(acquired.release.version).toBe("1.2.3");
await acquired.cleanup();
});
it("rejects an archive whose internal commit drifts from the Release", async () => {
const value = await fixture();
const release = { ...value.release, git_commit: "b".repeat(40) };
@@ -368,7 +410,6 @@ describe("remote Runtime acquisition", () => {
);
it("rejects a redirect to an untrusted host", async () => {
const value = await fixture();
await expect(
acquireRuntimeDistribution({
manifestUrl: "https://downloads.example.test/release-manifest.json",
@@ -38,6 +38,10 @@ const SHA256 = /^[a-f0-9]{64}$/;
const COMMIT = /^[0-9a-f]{40}$/;
const SAFE_FILE_NAME = /^[0-9A-Za-z][0-9A-Za-z._-]{0,255}$/;
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
const GITHUB_RELEASE_REDIRECT_HOSTS = [
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
] as const;
export type ReleaseChannel = "stable" | "prerelease";
export type RuntimeReleasePlatform = "darwin" | "linux";
@@ -330,8 +334,12 @@ export async function resolveRuntimeRelease(options: {
maxRedirects?: number;
}): Promise<ResolvedRuntimeRelease> {
const manifestUrl = trustedUrl(options.manifestUrl);
const defaultHosts =
normalizedHost(manifestUrl.hostname) === "github.com"
? [manifestUrl.hostname, ...GITHUB_RELEASE_REDIRECT_HOSTS]
: [manifestUrl.hostname];
const allowedHosts = new Set(
(options.allowedHosts ?? [manifestUrl.hostname]).map(normalizedHost),
(options.allowedHosts ?? defaultHosts).map(normalizedHost),
);
if (!allowedHosts.has(normalizedHost(manifestUrl.hostname))) {
throw new Error("release_url_untrusted");
+7
View File
@@ -8,6 +8,7 @@ import {
mkdir,
readdir,
readFile,
readlink,
realpath,
rm,
stat,
@@ -320,6 +321,12 @@ async function distributionBytes(source) {
async function sha256File(file) {
const digest = createHash("sha256");
const info = await lstat(file);
if (info.isSymbolicLink()) {
digest.update("symlink\0");
digest.update(await readlink(file));
return digest.digest("hex");
}
for await (const chunk of createReadStream(file)) digest.update(chunk);
return digest.digest("hex");
}
@@ -6,6 +6,7 @@ import {
readdir,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import os from "node:os";
@@ -121,6 +122,35 @@ test("builds versioned Runtime archives, checksums and a downloader-compatible m
}
});
test("verifies Runtime symlinks with the same identity rule as the installer", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "memhub-release-assets-"));
t.after(() => rm(root, { recursive: true, force: true }));
const source = await distribution(root, "linux", "x64");
const link = "memhub";
await symlink(link, path.join(source, "bin", "node"));
const manifestPath = path.join(source, "manifest.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
manifest.files["bin/node"] = sha256(`symlink\0${link}`);
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const release = await buildReleaseAssets({
output: path.join(root, "release"),
version: "0.1.0-internal.1",
channel: "prerelease",
assetBaseUrl: "https://git.example.test/release/downloads/",
distributions: [source],
});
assert.ok(
release.artifacts.some(
(artifact) =>
artifact.kind === "runtime" &&
artifact.platform === "linux" &&
artifact.arch === "x64",
),
);
});
test("refuses version, build and duplicate target drift", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "memhub-release-assets-"));
t.after(() => rm(root, { recursive: true, force: true }));
+13 -5
View File
@@ -23,7 +23,8 @@ export async function publishCliAfterRelease(options, dependencies = {}) {
const statImpl = dependencies.statImpl ?? stat;
const buildEnvironment = {
...environment,
ONE_MEMORY_VERSION: options.version,
MEMHUB_NPM_VERSION: options.version,
MEMHUB_DEFAULT_RUNTIME_VERSION: options.runtimeVersion,
ONE_MEMORY_BUILD_NUMBER: String(options.buildNumber),
ONE_MEMORY_COMMIT: options.gitCommit,
};
@@ -31,7 +32,7 @@ export async function publishCliAfterRelease(options, dependencies = {}) {
const versionReceipt = await verifyVersion(
[
"--version",
options.version,
options.runtimeVersion,
"--channel",
options.channel,
"--tag",
@@ -43,7 +44,7 @@ export async function publishCliAfterRelease(options, dependencies = {}) {
);
const releaseReceipt = await verifyRelease({
manifestUrl: options.manifestUrl,
version: options.version,
version: options.runtimeVersion,
gitCommit: options.gitCommit,
channel: options.channel,
platform: options.platform,
@@ -106,7 +107,8 @@ export async function publishCliAfterRelease(options, dependencies = {}) {
return {
schema_version: 1,
product: "memhub-cli-publication",
version: versionReceipt.version,
version: options.version,
runtime_version: releaseReceipt.version,
build_number: versionReceipt.build_number,
tag: versionReceipt.tag,
release: releaseReceipt,
@@ -196,6 +198,11 @@ function parseArgs(arguments_) {
options.manifestUrl = value;
else if (flag === "--version" && options.version === undefined)
options.version = value;
else if (
flag === "--runtime-version" &&
options.runtimeVersion === undefined
)
options.runtimeVersion = value;
else if (flag === "--git-commit" && options.gitCommit === undefined)
options.gitCommit = value;
else if (flag === "--channel" && options.channel === undefined)
@@ -219,6 +226,7 @@ function parseArgs(arguments_) {
if (
!options.manifestUrl ||
!options.version ||
!options.runtimeVersion ||
!options.gitCommit ||
!options.channel ||
!options.tag ||
@@ -230,7 +238,7 @@ function parseArgs(arguments_) {
!options.npmTag
) {
throw new Error(
"usage: publish-cli-after-release.mjs --manifest-url <https-url> --version <SemVer> --git-commit <sha> --channel <stable|prerelease> --tag <vSemVer> --build-number <integer> --platform <darwin|linux> --arch <x64|arm64> --registry <https-url> --npm-tag <tag> [--access <public|restricted>] [--provenance] [--allowed-host <hostname>] [--dry-run]",
"usage: publish-cli-after-release.mjs --manifest-url <https-url> --version <CLI SemVer> --runtime-version <Runtime SemVer> --git-commit <sha> --channel <stable|prerelease> --tag <vSemVer> --build-number <integer> --platform <darwin|linux> --arch <x64|arm64> --registry <https-url> --npm-tag <tag> [--access <public|restricted>] [--provenance] [--allowed-host <hostname>] [--dry-run]",
);
}
return options;
@@ -6,6 +6,9 @@ import test from "node:test";
import { publishCliAfterRelease } from "./publish-cli-after-release.mjs";
const CLI_VERSION = "0.1.1";
const RUNTIME_VERSION = "0.1.0-internal.1";
async function fixture(t) {
const root = await mkdtemp(path.join(os.tmpdir(), "memhub-publish-gate-"));
t.after(() => rm(root, { recursive: true, force: true }));
@@ -15,10 +18,10 @@ async function fixture(t) {
await mkdir(output, { recursive: true });
await writeFile(
path.join(publish, "package.json"),
`${JSON.stringify({ name: "@example/memhub-cli", version: "0.1.0-internal.1", private: false })}\n`,
`${JSON.stringify({ name: "@example/memhub-cli", version: CLI_VERSION, private: false })}\n`,
);
await writeFile(
path.join(output, "example-memhub-cli-0.1.0-internal.1.tgz"),
path.join(output, `example-memhub-cli-${CLI_VERSION}.tgz`),
"tarball",
);
return root;
@@ -26,10 +29,11 @@ async function fixture(t) {
const options = {
manifestUrl: "https://releases.example.test/release-manifest.json",
version: "0.1.0-internal.1",
version: CLI_VERSION,
runtimeVersion: RUNTIME_VERSION,
gitCommit: "a".repeat(40),
channel: "prerelease",
tag: "v0.1.0-internal.1",
tag: `v${RUNTIME_VERSION}`,
buildNumber: 1,
platform: "linux",
arch: "x64",
@@ -49,15 +53,24 @@ test("verifies the Runtime before building and publishing the CLI", async (t) =>
{
verifyVersion: async () => {
events.push("version");
return { version: options.version, build_number: 1, tag: options.tag };
return {
version: options.runtimeVersion,
build_number: 1,
tag: options.tag,
};
},
verifyRelease: async (verifyOptions) => {
events.push("release");
assert.equal(verifyOptions.gitCommit, options.gitCommit);
return { version: options.version, sha256: "a".repeat(64) };
assert.equal(verifyOptions.version, options.runtimeVersion);
return { version: options.runtimeVersion, sha256: "a".repeat(64) };
},
runCommand: (command, arguments_, commandOptions) => {
assert.equal(commandOptions.env.ONE_MEMORY_VERSION, options.version);
assert.equal(commandOptions.env.MEMHUB_NPM_VERSION, options.version);
assert.equal(
commandOptions.env.MEMHUB_DEFAULT_RUNTIME_VERSION,
options.runtimeVersion,
);
assert.equal(
commandOptions.env.ONE_MEMORY_BUILD_NUMBER,
String(options.buildNumber),
@@ -74,6 +87,8 @@ test("verifies the Runtime before building and publishing the CLI", async (t) =>
assert.match(events[3], /--access public/);
assert.match(events[3], /--provenance/);
assert.equal(receipt.npm_package, "@example/memhub-cli");
assert.equal(receipt.version, CLI_VERSION);
assert.equal(receipt.runtime_version, RUNTIME_VERSION);
assert.equal(receipt.dry_run, true);
assert.equal(receipt.npm_access, "public");
assert.equal(receipt.provenance, true);
@@ -87,7 +102,7 @@ test("never builds or invokes npm when the Runtime release is unavailable", asyn
{ ...options, root },
{
verifyVersion: async () => ({
version: options.version,
version: options.runtimeVersion,
build_number: 1,
tag: options.tag,
}),
@@ -115,11 +130,11 @@ test("rejects a package identity that differs from the configured publish name",
},
{
verifyVersion: async () => ({
version: options.version,
version: options.runtimeVersion,
build_number: 1,
tag: options.tag,
}),
verifyRelease: async () => ({ version: options.version }),
verifyRelease: async () => ({ version: options.runtimeVersion }),
runCommand: () => undefined,
},
),