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:
lefarcen
2026-09-07 07:01:15 +00:00
committed by GitHub
parent c5ae6292c4
commit d18ada9e35
2 changed files with 324 additions and 35 deletions
+162 -32
View File
@@ -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");
}