# DSH 兼容性(宿主防腐化):盯住**宿主上游的版本线**。 # # 与 contract.yml 同构、分工不同:那边盯知乎开放平台的行为指纹,这边盯 DSH 的 API 面。 # 两边都是会悄悄漂移的上游,一边红了不代表另一边有事,所以两条线各自独立、时间错开。 # # ## 为什么用 dist-tag 而不是版本矩阵 # # DSH 至今没有正式版,版本号本身不构成承诺,tag 才是。三个 tag 的语义**各不相同** # (@deepseek-ai/dsh-* 全家族一致): # # next —— **当前承诺线**。2026-09-24 起本仓的声明面(engines.dsh 与全部 27 条 # @deepseek-ai/dsh-* 声明)落在这一条上,形状见 package.json。 # 使用者按我们给的区间装得到、我们也只测这条线。 # alpha —— **已低于本仓声明的下限**(具体值现查 npm view @deepseek-ai/dsh dist-tags)。 # 换到它上面等于把依赖降到自己的声明范围之外,所以它不再是 pass/fail 信号, # 只留一个「旧线还能不能跑」的记录(见下面 record job)。 # latest —— **不可用**:多数 @deepseek-ai/dsh-* 包上它指向很早的版本,@deepseek-ai/dsh 自己 # 那条也未必落在本插件的声明里(具体值现查 npm view @deepseek-ai/dsh dist-tags)。 # 按默认方式装宿主会装到过期版本。 # # ## 两条线的处理方式不同(2026-09-22 角色对调) # # next 红了 = 使用者会装到 = **必须修** → 整个 run 红,会发通知(declaration / committed 两个 job)。 # alpha 红了 = **记录,不阻断** → job 红、run 绿,不进跟踪 issue。 # # 2026-09-24:承诺线回到 next —— 宿主把新线(rc 档)发在 next 上(alpha 停在旧档;具体值现查 npm view @deepseek-ai/dsh dist-tags),本仓推荐的配置界面下限也跟着那条线走。 # 2026-09-22 曾对调过一次(next 承诺 → alpha 承诺,当时理由是接缝只在 alpha 上);方向怎么走,判据始终是 # 「声明面与推荐线落在哪」,而一条**长期必红**的周更任务会让「红 = 出事」的信号失效 —— 噪音掩盖真问题,比少一条巡逻更糟。 # # 声明面(declaration)是这两条之外的一件事:它不跑任何代码,只问「我们声明的区间还罩不罩得住 # **README 告诉用户去装的那条线**」。那条线现在就是 next(本仓推荐的配置界面下限也在那里), # 所以它**只查 next** —— 查一条我们自己都不再声明的线,红只会变成每周的噪音。 # # 不挂 push:这条线看的是**上游**动没动,本地提交改不了结论。 name: Compat on: schedule: # 每周一 02:00 UTC(北京时间 10:00)。与 contract.yml 的 01:00 错开: # 两条上游线同时红会分不清是谁漂了。 - cron: '0 2 * * 1' workflow_dispatch: permissions: contents: read jobs: # 声明面:peer 范围还罩不罩得住**我们告诉用户去装的那条线**(next)。 # # 单独成 job 的理由:它只读 package.json 再问 npm,几十秒出结果;而且它的结论 # 与「代码还能不能跑」是两件事 —— 混在代码 job 里会被后者的红遮住,反过来也一样。 declaration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version-file: .node-version # 不装任何东西:判据是「我们声明的区间能取到的最新版本」与「tag 指向的版本」是否同一个。 # # **只查 next**:根 README 的「版本兼容」告诉使用者,需要配置界面就把宿主升到 # package.json 的 engines.dsh 声明的版本 —— 那个版本只在 next 线上,而声明面本身 # 也落在那里。判据是「声明区间必须罩得住我们告诉用户去装的那条线」。 # # 曾经两条线都查。2026-09-22 起只查当时的承诺线 alpha;现在承诺线回到 next,而 alpha 那条已低于我们的下限, # 于是**长期必红**,而长期必红会让信号失效。 - name: Declaration surface — the line the README points users at (next) id: declared run: node scripts/compat-swap.mjs check next - name: Summarize if: always() run: | { echo "## DSH 兼容性 · 声明面" echo "" echo "| 线 | check 结果 |" echo "| --- | --- |" echo "| next(承诺线 = README 让用户去装配置界面的那条线) | ${{ steps.declared.outcome }} |" echo "" echo "判据:声明区间必须能覆盖**我们告诉用户去装的那条线**(见根 README 的「版本兼容」)。" echo "" echo "红了怎么办:核对上游变更 → 放宽 peer 范围(形状见 package.json)→ 同步两份 README 的「前置」" echo "→ 按 docs/PUBLISHING.md 的 Q1/Q2 定档。范围放宽不改行为,Q1/Q2 全否即 **patch**," echo "但必须发版(声明在产物里)。" } >> "$GITHUB_STEP_SUMMARY" - name: Verdict if: always() run: | if [ "${{ steps.declared.outcome }}" != "success" ]; then echo "::error::声明面罩不住 next 线:check=${{ steps.declared.outcome }}。处理链见本次运行的 Summary。" exit 1 fi # 旧线(alpha):**只记录,不阻断 run**。 # # 它已低于本仓声明的下限,换到它上面等于把依赖降到声明范围之外 —— 红是这个 job 的常态, # 而不是仓库出事的信号。留着它是为了「旧线到底哪一步先坏」这份记录, # 不让它把每周的失败通知打出去。真正看住的东西在 next 与 declaration 上。 record: runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version-file: .node-version cache: npm # 刻意**不跑 npm ci**:要的是一棵按新声明重新解析出来的树。先 ci 再改,npm 会拿旧树去 # reconcile,实测直接 ERESOLVE(exit 1)并且 node_modules 原封不动停在旧版本上 —— # 后面的测试于是全绿,报出一个假兼容。 # 换包只替换区间里的**下限版本**,比较符与上界原样保留 —— 声明面不会在 CI 里悄悄变形。 - name: Swap the declared ranges onto the legacy alpha line run: node scripts/compat-swap.mjs swap alpha # --ignore-scripts 是必须的:本包的 prepare 会跑 npm run build(内含 tsc), # 类型面一漂移 tsc 就非零退出,install 本身失败,什么也测不到。构建由下面的步骤显式驱动。 - name: Install run: npm install --ignore-scripts --no-audit --no-fund # 换包没生效就必须红:旧版本装在那里会让后面每一步都绿。 - name: Verify the swap actually took effect id: verify run: node scripts/compat-swap.mjs verify alpha - name: Typecheck id: typecheck continue-on-error: true run: npm run typecheck # 跑的是现有套件,一条新断言都不加。 - name: Run the existing suite id: suite continue-on-error: true run: npm test # 只在套件红了之后跑,用来分流「类型面」与「行为面」: # npm test = build && vitest,tsc 一挂 vitest 根本不跑,会把「只有类型动了」误报成 # 「整套挂了」。tsc 默认 noEmitOnError=false,报错仍会产出 lib/,所以这里能重建产物 # 并真的跑到测试。 - name: Diagnose — separate the type surface from the behaviour surface id: diagnose if: steps.suite.outcome == 'failure' continue-on-error: true run: | npx tsc || true npx tsc -p tsconfig.client.json || true node scripts/build-client.mjs npx vitest run --reporter=basic - name: Summarize if: always() run: | { echo "## DSH 兼容性 · alpha 线(已低于声明下限,只记录)" echo "" echo "| 步骤 | 结果 |" echo "| --- | --- |" echo "| 换包 + 核对装出来的版本 | ${{ steps.verify.outcome }} |" echo "| 类型面(npm run typecheck) | ${{ steps.typecheck.outcome }} |" echo "| 全量测试(npm test) | ${{ steps.suite.outcome }} |" if [ -n "${{ steps.diagnose.outcome }}" ] && [ "${{ steps.diagnose.outcome }}" != "skipped" ]; then echo "| 分流诊断(跳过 tsc 的类型面) | ${{ steps.diagnose.outcome }} |" fi echo "" echo "**红了怎么办**:先确认这不是「本来就该红」—— 本 job 跑的那条线**低于我们对宿主的下限**," echo "红了通常只说明旧线装不出新接缝,不构成缺陷。真要看的是 next job 与 declaration job。" echo "" echo "(历史:2026-09-22 之前这条线是承诺线,红了必须修;角色对调后它降级为记录。)" echo "" echo "**若确实要查**(顺序不可颠倒):" echo "" echo "1. 先判类别 —— 三类红的处理完全不同:" echo " - **换包那一步或核对失败** → 树根本没换成,先解决安装问题再看别的(多半是上游包之间的 peer 冲突)。" echo " - **类型面红** → 上游 API 签名变了。定位到具体包与符号,改调用点使其**新旧都能编译**" echo " (2026-09-16 实测例子:agent/created 监听器显式 return undefined 即可同时满足" echo " alpha 线与 next 线)。两版不可兼得时说明下限必须抬高,那是 Q2 是 → **major,先问人类**。" echo " - **全量测试红** → **行为差异**,最重。按 test/README.md 的分层定位:" echo " L3b/L6a(native-web-tools / client-bundle / dist)说明平台语义变了;L1/L2 红则先怀疑" echo " 换包装错了 —— 那几层对宿主版本不敏感。" echo "2. 本地复现:clone 之后照本 job 的步骤跑一遍(swap → npm install --ignore-scripts → verify)。" echo "3. 修 → 按 docs/PUBLISHING.md 定档 → 发版 → 等下一轮。" } >> "$GITHUB_STEP_SUMMARY" # 各步都是 continue-on-error(为了拿到全部信号并让摘要一定跑得到), # 所以「有没有红」必须在这里显式汇总成 job 的成败。 - name: Verdict if: always() run: | if [ "${{ steps.verify.outcome }}" != "success" ] || [ "${{ steps.typecheck.outcome }}" != "success" ] || [ "${{ steps.suite.outcome }}" != "success" ]; then echo "::error::alpha 线漂移(换包=${{ steps.verify.outcome }},类型面=${{ steps.typecheck.outcome }},全量测试=${{ steps.suite.outcome }})。这是**记录**不是告警:这条线已低于声明下限。处理链见本次运行的 Summary。" exit 1 fi # 承诺线(next):红了**必须修**,会发通知。 # # 2026-09-24 之前它是前瞻线(continue-on-error);声明面落到 next 之后角色对调 —— # 使用者按我们的区间装到的就是这条线,它红了就是真出事。 committed: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version-file: .node-version cache: npm # 与 alpha 线(旧线)**同一套机制**,只差 install 不加 --force。 # # 为什么这里不加 --force:2026-09-24 实测两条线都装得干净 —— swap 之后各自 # `npm install --ignore-scripts --no-audit --no-fund --dry-run` 都 rc=0、解析出同样的 109 个包; # 当时留 force 的理由(旧线上包**内部** peer 图未定型、install 直接 ERESOLVE 退出) # 在这两个版本上已不成立,而 --force 会把**真实**的 peer 冲突一起吞掉,正好造出「假绿」。 # 将来若有线真的 ERESOLVE:把 --force 加回那一条,并在本条注释里写明是哪条线、哪个包。 # # ⚠ 绝不要退回 npm install <包>@next: # - 不点名的那个包的目录会被清空(实测 dsh-client-locale / dsh-client-ui-primitives 都中过), # 随后 typecheck 报「找不到模块」,是与上游无关的假红; # - 加 --legacy-peer-deps 更糟:它连 npm 的 **peer 自动安装**一起关掉,dsh-tools 自己的 # peer(dsh-sandbox / dsh-sandbox-policy / dsh-ptc-runtime)一个都不装, # 6 个测试文件以「Cannot find package @deepseek-ai/dsh-sandbox」假红(2026-09-16 实测)。 - name: Swap the declared ranges onto the next line id: verify continue-on-error: true run: | node scripts/compat-swap.mjs swap next npm install --ignore-scripts --no-audit --no-fund node scripts/compat-swap.mjs verify next - name: Typecheck id: typecheck continue-on-error: true run: npm run typecheck - name: Run the existing suite id: suite continue-on-error: true run: npm test - name: Diagnose — separate the type surface from the behaviour surface id: diagnose if: steps.suite.outcome == 'failure' continue-on-error: true run: | npx tsc || true npx tsc -p tsconfig.client.json || true node scripts/build-client.mjs npx vitest run --reporter=basic - name: Summarize if: always() run: | { echo "## DSH 兼容性 · next 线(承诺线 = 我们声明的与使用者装到的那条线)" echo "" echo "| 步骤 | 结果 |" echo "| --- | --- |" echo "| 换包 + 核对装出来的版本 | ${{ steps.verify.outcome }} |" echo "| 类型面 | ${{ steps.typecheck.outcome }} |" echo "| 全量测试 | ${{ steps.suite.outcome }} |" if [ -n "${{ steps.diagnose.outcome }}" ] && [ "${{ steps.diagnose.outcome }}" != "skipped" ]; then echo "| 分流诊断 | ${{ steps.diagnose.outcome }} |" fi echo "" echo "**红了怎么办**(顺序不可颠倒):" echo "" echo "1. 先判类别 —— 三类红的处理完全不同:" echo " - **换包那一步或核对失败** → 树根本没换成,先解决安装问题再看别的(多半是上游包之间的 peer 冲突)。" echo " - **类型面红** → 上游 API 签名变了。定位到具体包与符号,先看能不能改调用点使其新旧都能编译" echo " (compat.yml 的历史里记过一个例子:显式 return undefined 同时满足两条线)。" echo " 两版不可兼得时说明下限必须抬高,那是 Q2 是 → **major,先问人类**。" echo " - **全量测试红** → **行为差异**,最重。按 test/README.md 的分层定位:" echo " L3b/L6a(native-web-tools / client-bundle / dist)说明平台语义变了;L1/L2 红则先怀疑" echo " 换包装错了 —— 那几层对宿主版本不敏感。" echo "2. 本地复现:clone 之后照本 job 的步骤跑一遍(swap → npm install --ignore-scripts → verify)。" echo "3. 修 → 按 docs/PUBLISHING.md 定档 → 发版 → 等下一轮。" } >> "$GITHUB_STEP_SUMMARY" - name: Verdict if: always() run: | if [ "${{ steps.verify.outcome }}" != "success" ] || [ "${{ steps.typecheck.outcome }}" != "success" ] || [ "${{ steps.suite.outcome }}" != "success" ]; then echo "::error::next 线漂移(承诺线):换包=${{ steps.verify.outcome }},类型面=${{ steps.typecheck.outcome }},全量测试=${{ steps.suite.outcome }}。处理链见本次运行的 Summary。" exit 1 fi # 失败可见:定时任务的失败邮件 GitHub 自带,但它只发给最后改过 cron 的人,**不能当作主通知**。 # 这里把「哪条线、哪一步红」写进一个**固定标题**的跟踪 issue:已存在就追加评论,不刷屏。 # # 只收会红到使用者身上的两个 job:declaration(承诺还成不成立)与 next(代码能不能跑)。 # alpha 刻意不进:它已低于声明下限,红是**记录**不是告警(见文件头与 record job)。 report: needs: [declaration, committed] if: failure() runs-on: ubuntu-latest permissions: contents: read # checkout issues: write # 建 / 改跟踪 issue steps: - uses: actions/checkout@v7 # 正文用英文(issue 是仓库对外的门面);标题与标签固定,靠它们幂等。 - name: Open or update the tracking issue env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} DECLARATION: ${{ needs.declaration.result }} COMMITTED: ${{ needs.committed.result }} run: | set -euo pipefail title='DSH compatibility patrol failed' label='compat' gh label create "$label" --description 'DSH compatibility patrol (see docs/PUBLISHING.md)' --color BFD4F2 --force >/dev/null body=$(cat <