Files
nexu-io__open-design/.github/scripts/dsh-upstream-drift.ts
lefarcen d18ada9e35 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
2026-09-07 07:01:15 +00:00

458 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Watch upstream DeepSeek Harness releases and say something when we fall behind.
*
* Twice in one week the same failure shipped: `@deepseek-ai/dsh` published a new
* release line, and nothing in this repo noticed. The first time it was a pin on
* one release candidate; the second, a pin on one patch line. Both were found by
* a person reporting that their install did not work.
*
* The existing drift check (`e2e/tests/dsh-installer-version-policy.test.ts`)
* compares our installers against our agent def — the two halves of *our*
* config. It stays green while both are equally out of date, which is exactly
* what happened. This one compares them against the registry.
*
* Three places have to move together when upstream ships a new line, and this
* names all three, because forgetting the third is the one that actually breaks
* an install rather than merely warning about it:
*
* 1. `tools/release/resources/dsh-bootstrap/install-dsh.{sh,ps1}` — the
* version and the `--before` resolution window (canonical product source;
* landing `public/install-dsh.*` copies must stay byte-identical until
* extraction).
* 2. `apps/daemon/src/runtimes/defs/deepseek-harness.ts` — the accepted
* release line.
* 3. `packages/dsh-runtime/package.json` — the peer ranges. semver only lets a
* prerelease satisfy a range when a comparator carries the same
* major.minor.patch tuple AND its own prerelease tag, so every new upstream
* prerelease line needs its own comparator or the connection component
* cannot install at all.
*
* Commands:
* run [--dry-run] Compare against the live registry; post to Feishu on drift.
* `--dry-run` prints what it would post and exits 0.
* self-check Exercise the classification against fixtures. No network.
*/
import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";
import path from "node:path";
const PACKAGE = "@deepseek-ai/dsh";
const REGISTRY = "https://registry.npmjs.org";
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..");
const INSTALLER_SH = "tools/release/resources/dsh-bootstrap/install-dsh.sh";
const AGENT_DEF = "apps/daemon/src/runtimes/defs/deepseek-harness.ts";
const PEER_MANIFEST = "packages/dsh-runtime/package.json";
export type DriftSeverity = "in-sync" | "behind" | "unsupported";
export interface DriftVerdict {
severity: DriftSeverity;
pinned: string;
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 {
const value = process.env[name]?.trim() ?? "";
if (value.length === 0) throw new Error(`${name} is required`);
return value;
}
function readRepoFile(relative: string): string {
return readFileSync(path.join(REPO_ROOT, relative), "utf8");
}
/** The version our one-line installer hands a user today. */
export function readPinnedVersion(installerSource: string): string {
const found = /DSH_VERSION='([^']+)'/u.exec(installerSource)?.[1];
if (!found) throw new Error(`could not read DSH_VERSION from ${INSTALLER_SH}`);
return found;
}
/**
* The release line the agent def accepts, rebuilt from the literal in source.
* Absent is valid — it means the def only accepts the versions it lists.
*/
export function readAcceptedPattern(defSource: string): RegExp | null {
const found = /supportedVersionPattern:\s*\/(.+?)\/([a-z]*)\s*,/u.exec(defSource);
const body = found?.[1];
if (!body) return null;
return new RegExp(body, found?.[2] ?? "");
}
export function readListedVersions(defSource: string): string[] {
const list = /supportedVersions:\s*\[([^\]]*)\]/u.exec(defSource)?.[1] ?? "";
return [...list.matchAll(/'([^']+)'/gu)]
.map((entry) => entry[1])
.filter((entry): entry is string => typeof entry === "string");
}
/**
* How badly we have fallen behind.
*
* `behind` means a user who takes our installer gets an older version than the
* registry serves — annoying, but everything works. `unsupported` means a user
* who has what npm serves is outside what we claim to support: Settings calls
* their CLI untested, and the peer ranges very likely do not resolve either.
*/
export function classifyDrift(args: {
accepted: boolean;
latest: string;
pinned: string;
publishedAt: string | null;
}): DriftVerdict {
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" };
}
/**
* 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/json" },
});
if (!response.ok) {
throw new Error(`registry returned HTTP ${response.status} for ${PACKAGE}`);
}
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, publishedAt: readPublishedAt(body, latest) };
}
/**
* 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 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}\` 不在我们声明支持的范围内。**`,
"装到这个版本的用户,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: situation } },
{ tag: "div", text: { tag: "lark_md", content: consequence } },
{ tag: "hr" },
{
tag: "div",
text: {
tag: "lark_md",
content: [
"**三处要一起改**(漏掉第三条会让组件装不上,不只是警告):",
`1. \`${INSTALLER_SH}\` 和 \`.ps1\` — 版本 + \`--before\` 时间窗`,
`2. \`${AGENT_DEF}\` — 接受的发布线`,
`3. \`${PEER_MANIFEST}\` — peer 范围加一条比较器`,
].join("\n"),
},
},
],
header: {
template: blocking ? "red" : "orange",
title: { content: "DeepSeek Harness 上游新版检测", tag: "plain_text" },
},
};
}
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;
const timestamp = Math.floor(Date.now() / 1000).toString();
const sign = createHmac("sha256", `${timestamp}\n${signSecret}`).update("").digest("base64");
return { sign, timestamp, ...body };
}
export interface FeishuDelivery {
code: number | null;
delivered: boolean;
retryable: boolean;
}
/**
* Decide whether Feishu actually accepted the card.
*
* A rejected webhook request still comes back HTTP 200 with a nonzero `code`,
* so treating every 2xx as success loses the one message this workflow exists
* to send — silently, which is the worst way to lose an alert. Same contract as
* the landing notifier: code 0 (or absent) is delivered; 429, 5xx and Feishu's
* 9499 are worth another attempt.
*/
export function interpretFeishuResponse(args: {
status: number;
text: string;
}): FeishuDelivery {
let code: number | null = null;
try {
const parsed = JSON.parse(args.text) as { StatusCode?: unknown; code?: unknown };
const raw = parsed.code ?? parsed.StatusCode ?? null;
code = typeof raw === "number" ? raw : null;
} catch {
// Feishu normally returns JSON; fall back to the HTTP status alone.
}
const ok = args.status >= 200 && args.status < 300;
if (ok && (code === 0 || code === null)) {
return { code, delivered: true, retryable: false };
}
return {
code,
delivered: false,
retryable: args.status === 429 || args.status >= 500 || code === 9499,
};
}
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, {
body: JSON.stringify(signedEnvelope(card)),
headers: { "content-type": "application/json" },
method: "POST",
});
const text = await response.text();
const delivery = interpretFeishuResponse({ status: response.status, text });
if (delivery.delivered) {
console.log(`[dsh-drift] delivered (HTTP ${response.status}, code ${delivery.code ?? "n/a"})`);
return;
}
console.warn(
`[dsh-drift] attempt ${attempt}/5 failed: HTTP ${response.status} ` +
`code ${String(delivery.code)} ${text.slice(0, 300)}`,
);
if (!delivery.retryable || attempt === 5) {
throw new Error(
`Feishu webhook failed: HTTP ${response.status} code ${String(delivery.code)}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
async function run(dryRun: boolean): Promise<void> {
const pinned = readPinnedVersion(readRepoFile(INSTALLER_SH));
const defSource = readRepoFile(AGENT_DEF);
const pattern = readAcceptedPattern(defSource);
const listed = readListedVersions(defSource);
const { latest, publishedAt } = await fetchLatestRelease();
const accepted = listed.includes(latest) || (pattern?.test(latest) ?? false);
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}`,
);
if (verdict.severity === "in-sync") return;
const card = buildCard(verdict);
if (dryRun) {
console.log(JSON.stringify(card, null, 2));
return;
}
await postFeishu(card);
console.log("[dsh-drift] posted to Feishu");
}
function selfCheck(): void {
const installer = "NODE_VERSION='24.19.0'\nDSH_VERSION='0.1.1-rc.2'\n";
if (readPinnedVersion(installer) !== "0.1.1-rc.2") {
throw new Error("self-check could not read the pinned version");
}
const def = [
" supportedVersions: ['0.1.0-rc.8', '0.1.1-rc.2'],",
" supportedVersionPattern: /^0\\.1\\.\\d+(?:-rc\\.\\d+)?$/u,",
].join("\n");
const pattern = readAcceptedPattern(def);
if (!pattern) throw new Error("self-check could not rebuild the accepted pattern");
if (!pattern.test("0.1.1-rc.2") || pattern.test("0.2.0-rc.1")) {
throw new Error("self-check rebuilt a pattern that does not match the source literal");
}
if (!readListedVersions(def).includes("0.1.0-rc.8")) {
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.
const missed = classifyDrift({
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");
}
// `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",
publishedAt: null,
});
if (behind.severity !== "behind") {
throw new Error("self-check expected an accepted-but-older latest to be behind");
}
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");
}
console.log("[dsh-drift] self-check passed");
}
// Only dispatch when this file is the process entrypoint. Without the guard,
// importing it to reuse the pure helpers executes `run`: a live registry
// request during test collection, and — once drift exists — an attempted
// Feishu post before a single assertion has run.
const entry = process.argv[1];
const invokedDirectly = entry !== undefined && path.resolve(entry) === import.meta.filename;
if (invokedDirectly) {
const command = process.argv[2] ?? "run";
if (command === "self-check") {
selfCheck();
} else if (command === "run") {
await run(process.argv.includes("--dry-run"));
} else {
console.error(`unknown command: ${command}`);
process.exit(2);
}
}