fix(packaging): protect qwenpaw releases and test Studio health

This commit is contained in:
jinli.yl 2026-08-27 13:19:27 +08:00
parent f986f73ffa
commit 00abb4ef29
7 changed files with 144 additions and 20 deletions

View file

@ -77,6 +77,22 @@ jobs:
assert (static_dir() / "index.html").is_file()
PY
# qwenpaw composes independently released plugins. Keep this before the
# artifact upload so a core release cannot advertise an unavailable extra.
- name: Verify released qwenpaw dependencies
if: inputs.expected_version != ''
run: |
REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)"
python -m venv "${RUNNER_TEMP}/reme-qwenpaw-package-smoke"
"${RUNNER_TEMP}/reme-qwenpaw-package-smoke/bin/python" -m pip install "${REME_WHEEL}[qwenpaw]"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-qwenpaw-package-smoke/bin/python" - <<'PY'
from importlib.metadata import distribution
assert distribution("reme-auto-fin")
assert distribution("reme-daily-paper")
PY
- name: Upload ReMe distributions
if: inputs.upload_artifacts
uses: actions/upload-artifact@v4

View file

@ -1,5 +1,8 @@
name: Release / Python packages
# reme-ai[qwenpaw] is verified before publication. Publish the independently
# versioned reme-auto-fin and reme-daily-paper requirements first.
on:
workflow_dispatch:
inputs:

View file

@ -7,6 +7,7 @@ import type {
StreamChunk,
} from "./types";
import { decodeSseEvent } from "./chat-stream";
import { healthFromResponse } from "./health-status";
import { translate, useLanguageStore, type TranslationKey } from "./i18n";
import {
WORKSPACE_FILE_LIMIT,
@ -70,10 +71,7 @@ export async function getReMeStatus(): Promise<ReMeResponse<string>> {
export async function getReMeHealth(): Promise<ReMeHealth | undefined> {
const response = await callReMe<string>("health_check");
const health = response.metadata.health;
return health && typeof health === "object"
? (health as ReMeHealth)
: undefined;
return healthFromResponse(response);
}
export async function rebuildReMeIndex(): Promise<ReMeResponse<unknown>> {

View file

@ -0,0 +1,42 @@
import type { ReMeComponentHealth, ReMeHealth, ReMeResponse } from "./types";
export interface ComponentMemoryUsage {
human?: string;
}
export interface HealthComponentEntry {
type: string;
name: string;
component: ReMeComponentHealth;
memory?: string;
}
export function healthFromResponse(
response: Pick<ReMeResponse, "metadata">,
): ReMeHealth | undefined {
const health = response.metadata.health;
return health && typeof health === "object"
? (health as ReMeHealth)
: undefined;
}
export function isComponentHealthy(component: ReMeComponentHealth): boolean {
return (
component.is_healthy === true ||
(component.is_started === true && component.is_healthy !== false)
);
}
export function healthComponentEntries(
health?: ReMeHealth,
memory?: Record<string, Record<string, ComponentMemoryUsage>>,
): HealthComponentEntry[] {
return Object.entries(health?.components || {}).flatMap(([type, entries]) =>
Object.entries(entries).map(([name, component]) => ({
type,
name,
component,
memory: memory?.[type]?.[name]?.human,
})),
);
}

View file

@ -24,6 +24,7 @@ import {
rebuildReMeIndex,
REME_API_ENDPOINT,
} from "./api";
import { healthComponentEntries, isComponentHealthy } from "./health-status";
import { useI18n, type TranslationKey } from "./i18n";
import type {
AppConfig,
@ -61,13 +62,6 @@ const COMPONENT_ICONS: Record<string, React.ReactNode> = {
keyword_index: <FileArchive size={18} />,
};
function isComponentHealthy(component: ReMeComponentHealth): boolean {
return (
component.is_healthy === true ||
(component.is_started === true && component.is_healthy !== false)
);
}
const COMPONENT_LABELS = {
embedding_store: "embeddingStore",
file_graph: "fileGraph",
@ -256,15 +250,7 @@ export default function SettingsCenter({
([type, entries]) =>
Object.entries(entries).map(([name, usage]) => ({ type, name, usage })),
);
const healthComponents = Object.entries(health?.components || {}).flatMap(
([type, entries]) =>
Object.entries(entries).map(([name, component]) => ({
type,
name,
component,
memory: memory?.components?.[type]?.[name]?.human,
})),
);
const healthComponents = healthComponentEntries(health, memory?.components);
const healthyComponents = healthComponents.filter(({ component }) =>
isComponentHealthy(component),
).length;

View file

@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import test from "node:test";
import { healthFromResponse } from "../app/health-status.ts";
test("health metadata is returned from the existing health_check response", () => {
const health = {
version: "0.4.1.8",
healthy: true,
components: { file_store: { default: { is_started: true } } },
};
const result = healthFromResponse({ metadata: { health } });
assert.deepEqual(result, health);
});
test("invalid health metadata keeps the settings fallback available", () => {
assert.equal(healthFromResponse({ metadata: {} }), undefined);
assert.equal(healthFromResponse({ metadata: { health: null } }), undefined);
assert.equal(
healthFromResponse({ metadata: { health: "unknown" } }),
undefined,
);
});

View file

@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
healthComponentEntries,
isComponentHealthy,
} from "../app/health-status.ts";
test("component health honors explicit failures and started fallbacks", () => {
assert.equal(isComponentHealthy({ is_healthy: true }), true);
assert.equal(
isComponentHealthy({ is_started: true, is_healthy: null }),
true,
);
assert.equal(
isComponentHealthy({ is_started: true, is_healthy: false }),
false,
);
assert.equal(isComponentHealthy({ is_started: false }), false);
});
test("health components are flattened with matching memory usage", () => {
const components = healthComponentEntries(
{
version: "0.4.1.8",
healthy: true,
components: {
embedding_store: {
default: { is_started: true, dimensions: 1024 },
},
file_graph: {
memory: { is_healthy: false, n_nodes: 12 },
},
},
},
{
embedding_store: { default: { human: "12 MiB" } },
},
);
assert.deepEqual(components, [
{
type: "embedding_store",
name: "default",
component: { is_started: true, dimensions: 1024 },
memory: "12 MiB",
},
{
type: "file_graph",
name: "memory",
component: { is_healthy: false, n_nodes: 12 },
memory: undefined,
},
]);
assert.deepEqual(healthComponentEntries(), []);
});