mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(ci): make the DSH upstream drift card readable (#7824)
The maintainer who received this alert could not tell what it was. The headline jumped straight to a verdict, so nothing said an upstream release had been detected or where the message came from; the body opened with semver theory; the blocking branch never mentioned the version we pin, making the most urgent card the least informative one; and nothing said when upstream shipped, so there was no way to tell a release from this morning apart from one that had been sitting for a fortnight. The headline now names the check and only the check — severity travels in the header template, which Feishu renders before any text. The first sentence is three facts with no reasoning attached: what upstream released, when, and what we still pin, on both severities. The consequence follows, and the semver rationale is compressed into a parenthetical behind it. The three-file checklist is untouched. Reading the publish date means asking npm for the full packument: the compact one (`application/vnd.npm.install-v1+json`) carries no `time` map at all. `readPublishedAt` degrades to null for every shape short of that, because the date is one sentence of context on an alert whose entire reason to exist is being sent. Also replaces a self-check assertion that could never fail: `String(card.header).length === 0` is "[object Object]".length whatever the card says. It now reads the fields a human reads. Claude-Session: https://claude.ai/code/session_01B8MaA1ABkvbnfaYUKw3UH3
This commit is contained in:
@@ -52,6 +52,25 @@ export interface DriftVerdict {
|
||||
latest: string;
|
||||
/** Whether the agent def would accept `latest` without an untested warning. */
|
||||
accepted: boolean;
|
||||
/**
|
||||
* When the registry published `latest`, ISO-8601, or `null` when it did not
|
||||
* say. Nullable on purpose — see `readPublishedAt`.
|
||||
*/
|
||||
publishedAt: string | null;
|
||||
}
|
||||
|
||||
interface CardBlock {
|
||||
tag: string;
|
||||
text?: { tag: string; content: string };
|
||||
}
|
||||
|
||||
export interface DriftCard {
|
||||
config: { wide_screen_mode: boolean };
|
||||
elements: CardBlock[];
|
||||
header: {
|
||||
template: "orange" | "red";
|
||||
title: { content: string; tag: "plain_text" };
|
||||
};
|
||||
}
|
||||
|
||||
function requiredEnv(name: string): string {
|
||||
@@ -101,47 +120,111 @@ export function classifyDrift(args: {
|
||||
accepted: boolean;
|
||||
latest: string;
|
||||
pinned: string;
|
||||
publishedAt: string | null;
|
||||
}): DriftVerdict {
|
||||
const { accepted, latest, pinned } = args;
|
||||
if (!accepted) return { accepted, latest, pinned, severity: "unsupported" };
|
||||
if (latest !== pinned) return { accepted, latest, pinned, severity: "behind" };
|
||||
return { accepted, latest, pinned, severity: "in-sync" };
|
||||
const { accepted, latest, pinned, publishedAt } = args;
|
||||
const facts = { accepted, latest, pinned, publishedAt };
|
||||
if (!accepted) return { ...facts, severity: "unsupported" };
|
||||
if (latest !== pinned) return { ...facts, severity: "behind" };
|
||||
return { ...facts, severity: "in-sync" };
|
||||
}
|
||||
|
||||
async function fetchLatestVersion(): Promise<string> {
|
||||
/**
|
||||
* When the registry says `version` was published, or `null` when it does not.
|
||||
*
|
||||
* Nullable rather than throwing, and deliberately so: the publish date is one
|
||||
* sentence of context on an alert whose entire reason to exist is being sent.
|
||||
* A registry that answers with a compact packument (no `time` map at all), a
|
||||
* `time` map that has not caught up with `dist-tags`, or a non-string entry
|
||||
* must cost the card that sentence — never the alert.
|
||||
*/
|
||||
export function readPublishedAt(packument: unknown, version: string): string | null {
|
||||
if (typeof packument !== "object" || packument === null) return null;
|
||||
const time = (packument as { time?: unknown }).time;
|
||||
if (typeof time !== "object" || time === null) return null;
|
||||
const published = (time as Record<string, unknown>)[version];
|
||||
return typeof published === "string" && published.length > 0 ? published : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `2026-09-03(4 天前)` — the date plus the one thing a bare date does not
|
||||
* answer: whether this landed this morning or has been sitting for a fortnight.
|
||||
* `null` for anything unparseable, so callers can drop the clause entirely.
|
||||
*/
|
||||
export function describePublishedAt(publishedAt: string | null, now: Date): string | null {
|
||||
if (publishedAt === null) return null;
|
||||
const at = new Date(publishedAt);
|
||||
if (Number.isNaN(at.getTime())) return null;
|
||||
const day = at.toISOString().slice(0, 10);
|
||||
const elapsed = Math.round(
|
||||
(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) -
|
||||
Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate())) /
|
||||
86_400_000,
|
||||
);
|
||||
if (elapsed < 0) return day;
|
||||
if (elapsed === 0) return `${day}(今天)`;
|
||||
return `${day}(${elapsed} 天前)`;
|
||||
}
|
||||
|
||||
async function fetchLatestRelease(): Promise<{ latest: string; publishedAt: string | null }> {
|
||||
// The compact packument (`application/vnd.npm.install-v1+json`) carries only
|
||||
// name / dist-tags / versions / modified — it has no `time` map, so the card
|
||||
// could never say when upstream shipped. This runs once a day; the full
|
||||
// document is worth its bytes.
|
||||
const response = await fetch(`${REGISTRY}/${encodeURIComponent(PACKAGE)}`, {
|
||||
headers: { accept: "application/vnd.npm.install-v1+json" },
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`registry returned HTTP ${response.status} for ${PACKAGE}`);
|
||||
}
|
||||
const body = (await response.json()) as { "dist-tags"?: Record<string, string> };
|
||||
const latest = body["dist-tags"]?.latest;
|
||||
const body = (await response.json()) as unknown;
|
||||
const tags = (body as { "dist-tags"?: Record<string, string> })["dist-tags"];
|
||||
const latest = tags?.latest;
|
||||
if (!latest) throw new Error(`${PACKAGE} has no dist-tag "latest"`);
|
||||
return latest;
|
||||
return { latest, publishedAt: readPublishedAt(body, latest) };
|
||||
}
|
||||
|
||||
export function buildCard(verdict: DriftVerdict): Record<string, unknown> {
|
||||
/**
|
||||
* The card a maintainer wakes up to.
|
||||
*
|
||||
* Read top to bottom it answers, in this order: what check is this, what
|
||||
* happened, why it matters, what to change. An earlier version led with a
|
||||
* verdict as its headline and opened its body with semver theory, and the
|
||||
* maintainer who received it could not tell what the message even was. So the
|
||||
* headline names the check and nothing else — severity is carried by the header
|
||||
* template, which Feishu renders before a single word of text — and the first
|
||||
* sentence is three facts with no reasoning attached: the upstream version,
|
||||
* when it shipped, and what we still pin.
|
||||
*/
|
||||
export function buildCard(verdict: DriftVerdict, now: Date = new Date()): DriftCard {
|
||||
const blocking = verdict.severity === "unsupported";
|
||||
const headline = blocking
|
||||
? `DeepSeek Harness ${verdict.latest} 不在我们支持的范围内`
|
||||
: `DeepSeek Harness 已发布 ${verdict.latest},我们还钉在 ${verdict.pinned}`;
|
||||
const released = describePublishedAt(verdict.publishedAt, now);
|
||||
const when = released === null ? "" : `,发布于 ${released}`;
|
||||
const situation =
|
||||
`上游 \`${PACKAGE}\` 已发布 \`${verdict.latest}\`${when}。` +
|
||||
`我们的安装脚本目前钉在 \`${verdict.pinned}\`。`;
|
||||
// The bold lead sentence gets its own line: markdown will not close a `**`
|
||||
// run that is followed straight by a CJK character, so `**…。**装到` renders
|
||||
// the asterisks literally.
|
||||
const consequence = blocking
|
||||
? [
|
||||
`装到 \`${verdict.latest}\` 的用户会被告知「未经测试」,`,
|
||||
"而且 `packages/dsh-runtime` 的 peer 范围很可能直接解析失败——",
|
||||
"semver 要求同 major.minor.patch 且自身带 prerelease 的比较器,",
|
||||
"所以每条新的 prerelease 线都得单独加一个比较器,组件才装得上。",
|
||||
].join("")
|
||||
: `按我们安装脚本装的用户会拿到 \`${verdict.pinned}\`,比 registry 的 \`latest\` 旧。`;
|
||||
`**\`${verdict.latest}\` 不在我们声明支持的范围内。**`,
|
||||
"装到这个版本的用户,CLI 会在设置里被标成「未经测试」," +
|
||||
"而且 `packages/dsh-runtime` 的 peer 范围多半直接解析不出来,组件根本装不上。",
|
||||
"(semver 只在比较器带同一 major.minor.patch、且自身也带 prerelease 标签时才放行一个 prerelease," +
|
||||
"所以每条新的 prerelease 线都得单独加一个比较器。)",
|
||||
].join("\n")
|
||||
: [
|
||||
`**\`${verdict.latest}\` 我们是支持的,只是安装脚本还没跟上。**`,
|
||||
`照我们的一行安装命令装的用户会拿到 \`${verdict.pinned}\`,` +
|
||||
"比 registry 上的 `latest` 旧一档——能用,但不是最新的。",
|
||||
].join("\n");
|
||||
|
||||
return {
|
||||
config: { wide_screen_mode: true },
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: { tag: "lark_md", content: `${consequence}` },
|
||||
},
|
||||
{ tag: "div", text: { tag: "lark_md", content: situation } },
|
||||
{ tag: "div", text: { tag: "lark_md", content: consequence } },
|
||||
{ tag: "hr" },
|
||||
{
|
||||
tag: "div",
|
||||
@@ -158,12 +241,12 @@ export function buildCard(verdict: DriftVerdict): Record<string, unknown> {
|
||||
],
|
||||
header: {
|
||||
template: blocking ? "red" : "orange",
|
||||
title: { content: headline, tag: "plain_text" },
|
||||
title: { content: "DeepSeek Harness 上游新版检测", tag: "plain_text" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function signedEnvelope(card: Record<string, unknown>): Record<string, unknown> {
|
||||
function signedEnvelope(card: DriftCard): Record<string, unknown> {
|
||||
const body = { card, msg_type: "interactive" };
|
||||
const signSecret = process.env.FEISHU_SIGN_SECRET?.trim() ?? "";
|
||||
if (signSecret.length === 0) return body;
|
||||
@@ -210,7 +293,7 @@ export function interpretFeishuResponse(args: {
|
||||
};
|
||||
}
|
||||
|
||||
async function postFeishu(card: Record<string, unknown>): Promise<void> {
|
||||
async function postFeishu(card: DriftCard): Promise<void> {
|
||||
const webhook = requiredEnv("FEISHU_WEBHOOK");
|
||||
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
||||
const response = await fetch(webhook, {
|
||||
@@ -242,12 +325,13 @@ async function run(dryRun: boolean): Promise<void> {
|
||||
const defSource = readRepoFile(AGENT_DEF);
|
||||
const pattern = readAcceptedPattern(defSource);
|
||||
const listed = readListedVersions(defSource);
|
||||
const latest = await fetchLatestVersion();
|
||||
const { latest, publishedAt } = await fetchLatestRelease();
|
||||
const accepted = listed.includes(latest) || (pattern?.test(latest) ?? false);
|
||||
const verdict = classifyDrift({ accepted, latest, pinned });
|
||||
const verdict = classifyDrift({ accepted, latest, pinned, publishedAt });
|
||||
|
||||
console.log(
|
||||
`[dsh-drift] pinned=${verdict.pinned} latest=${verdict.latest} ` +
|
||||
`published=${verdict.publishedAt ?? "unknown"} ` +
|
||||
`accepted=${verdict.accepted} severity=${verdict.severity}`,
|
||||
);
|
||||
|
||||
@@ -280,6 +364,20 @@ function selfCheck(): void {
|
||||
throw new Error("self-check could not read the listed versions");
|
||||
}
|
||||
|
||||
const registryAnswer = {
|
||||
"dist-tags": { latest: "0.1.1-rc.2" },
|
||||
name: PACKAGE,
|
||||
time: { "0.1.0-rc.8": "2026-08-21T09:02:11.000Z", "0.1.1-rc.2": "2026-09-03T06:21:52.107Z" },
|
||||
};
|
||||
if (readPublishedAt(registryAnswer, "0.1.1-rc.2") !== "2026-09-03T06:21:52.107Z") {
|
||||
throw new Error("self-check could not read the publish date out of a packument");
|
||||
}
|
||||
// The compact packument has no `time` map. Degrading, not throwing, is the
|
||||
// whole contract: an alert without a date still beats no alert.
|
||||
if (readPublishedAt({ "dist-tags": { latest: "0.1.1-rc.2" } }, "0.1.1-rc.2") !== null) {
|
||||
throw new Error("self-check expected a missing publish date to read as null");
|
||||
}
|
||||
|
||||
// The state that shipped twice: the installer and the def agreed with each
|
||||
// other and both were behind the registry. Anything that reports this as
|
||||
// healthy has lost the only thing this check is for.
|
||||
@@ -287,19 +385,51 @@ function selfCheck(): void {
|
||||
accepted: false,
|
||||
latest: "0.1.1-rc.2",
|
||||
pinned: "0.1.0-rc.8",
|
||||
publishedAt: "2026-09-03T06:21:52.107Z",
|
||||
});
|
||||
if (missed.severity !== "unsupported") {
|
||||
throw new Error("self-check expected the shipped-twice state to be unsupported");
|
||||
}
|
||||
if (String(buildCard(missed).header).length === 0) {
|
||||
throw new Error("self-check expected a card for a drifted verdict");
|
||||
|
||||
// `String(card.header)` is "[object Object]" whatever the card says, so the
|
||||
// assertion that used to stand here could never fail. Read the fields a human
|
||||
// reads instead: the card has to name the check, and its body has to carry
|
||||
// all three facts — upstream version, publish date, what we pin — including
|
||||
// on the blocking branch, which once mentioned no pinned version at all.
|
||||
const card = buildCard(missed, new Date("2026-09-07T00:00:00.000Z"));
|
||||
const body = card.elements.map((element) => element.text?.content ?? "").join("\n");
|
||||
if (!card.header.title.content.includes("新版检测")) {
|
||||
throw new Error("self-check expected the headline to name the check");
|
||||
}
|
||||
for (const fact of [missed.latest, missed.pinned, "2026-09-03"]) {
|
||||
if (!body.includes(fact)) {
|
||||
throw new Error(`self-check expected the card body to state ${fact}`);
|
||||
}
|
||||
}
|
||||
const undated = buildCard({ ...missed, publishedAt: null });
|
||||
const undatedBody = undated.elements.map((element) => element.text?.content ?? "").join("\n");
|
||||
if (!undatedBody.includes(missed.latest) || undatedBody.includes("Invalid Date")) {
|
||||
throw new Error("self-check expected an undated verdict to still produce a readable card");
|
||||
}
|
||||
|
||||
const behind = classifyDrift({ accepted: true, latest: "0.1.2", pinned: "0.1.1-rc.2" });
|
||||
const behind = classifyDrift({
|
||||
accepted: true,
|
||||
latest: "0.1.2",
|
||||
pinned: "0.1.1-rc.2",
|
||||
publishedAt: null,
|
||||
});
|
||||
if (behind.severity !== "behind") {
|
||||
throw new Error("self-check expected an accepted-but-older latest to be behind");
|
||||
}
|
||||
const synced = classifyDrift({ accepted: true, latest: "0.1.1-rc.2", pinned: "0.1.1-rc.2" });
|
||||
if (buildCard(behind).header.template !== "orange") {
|
||||
throw new Error("self-check expected a non-blocking verdict to stay orange");
|
||||
}
|
||||
const synced = classifyDrift({
|
||||
accepted: true,
|
||||
latest: "0.1.1-rc.2",
|
||||
pinned: "0.1.1-rc.2",
|
||||
publishedAt: null,
|
||||
});
|
||||
if (synced.severity !== "in-sync") {
|
||||
throw new Error("self-check expected an exact match to be in-sync");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user