Skip to content

ci(check-links): 恢复 pull_request 断链门,仅检仓内链接、advisory-first (#6028) - #6304

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-6028-restore-check-links-gate
Aug 7, 2026
Merged

ci(check-links): 恢复 pull_request 断链门,仅检仓内链接、advisory-first (#6028)#6304
hotlong merged 6 commits into
mainfrom
claude/issue-6028-restore-check-links-gate

Conversation

@hotlong

@hotlonghotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes#6028

按维护者 2026-08-07 裁决恢复 Check Links 断链门。裁决原文与本 PR 的逐条对应见下。

一、裁决 → 实现的逐条对应

裁决要求本 PR 的落点
"re-enable the pull_request trigger".github/workflows/check-links.yml 恢复 pull_request (branches: main),workflow_dispatch 保留
"with external URLs excluded … checks repo-relative links deterministically"调用处加 --offline(理由见第三节),外链一律 EXCLUDED、零网络请求
"Option ② (keep it dormant, document why) is explicitly rejected"被注释掉的 push: 残骸按「declared = enforced」直接删除,不留空壳
"Land it advisory-first: keep it out of the required set"⛔ 未碰 required 集;⛔ 未加 merge_group
"If the internal-only run turns out noisy in practice, come back here"实跑不吵:PR 上 Check Documentation Links 绿,1697 条链接 0 error(第六节)

fail: true 原样保留。

二、触发器 diff(before / after)

before:

on:
workflow_dispatch:
# push:# branches:# - main# pull_request:# branches:# - main

after:

on:
workflow_dispatch:
pull_request:
branches:
- main

push: 两行注释残骸没有被"恢复",而是被删除 —— push 触发不在裁决范围内,如需恢复请另议。裁决只点名了 pull_request,而方向②被明确否决之后,把一段注释掉的触发器继续留在文件里就是它反对的那种空壳。

三、仅检仓内链接的机制:选 --offline,且写在调用处

裁决允许机制自选。选 --offline 而不是 exclude = ["^https?://"],并且写在 workflow 的 args: 里而不是 lychee.toml,两个选择都由实测决定:

为什么是 --offline 而不是 exclude pattern —— exclude 要逐个枚举远程 scheme,漏一个(ftp:、协议相对 //host/path、将来任何新 scheme)就重新引入外网依赖;--offline 是"不发任何网络请求"的硬保证,结构上做不到漏。

为什么写在调用处而不是配置文件 —— 实测 offline = true 这个配置键:

  • lychee 0.19.1:静默忽略,照常发网络请求(实测 16 个 network error);
  • lychee 0.24.2(lycheeverse/lychee-action@v2 当前 lycheeVersion 默认值):生效。

一个"确定性"保证不能取决于 action 恰好装了哪个版本 —— 一旦 action 改了默认版本,配置键式的写法会静默退回联网。写在调用处则两个版本都生效。

四、门休眠半年,配置已经漂移到跑不起来(裁决没预料到的部分)

这是本 PR 比"取消两行注释"多出来的工作量,也是必需的:只恢复触发器的话,门会以 exit 3 直接死掉,一条链接都不检lychee.toml 里有两个键在 action 钉住的 0.24.2 上是硬解析错误:

  • follow_redirects —— 在 0.24.2 上根本不是合法键;
  • include_fragments = false —— 已从 bool 改成枚举 none | anchor-only | text-only | full,裸 false 同样硬报错。改为 "none",保持原语义(不检 fragment)。

半年没人跑,所以半年没人发现。这正是"看起来在守而实际不守"的代价。旁证:该 workflow 最后几次真实运行(2026-01-28,即触发器被注释掉的当天)全部 failure;此后唯一一次 pull_request 运行是 2026-07-17 的 docs/generator-fixes 分支,4 次全红。

另外,content/** 里的主力内部链接写法是 root-relative 站点路由(/docs/permissions 这种),而 lychee 在没有 --root-dir 时对它们直接报错("Cannot resolve root-relative link ... provide a root dir")。补上 --root-dir ${{ github.workspace }}/content + --fallback-extensions mdx,md 之后:

检查面
修复前(假设配置能解析)134 条被检,1286 条 root-relative 链接完全没检
修复后1475 条被检,0 error

原来的 remap 块声称把 /docs/* 映射到 content/docs/*.mdx,一次都没生效过(root-relative 链接在 remap 之前就 URI 构造失败)。按 declared = enforced 删除,能力由真正生效的 --root-dir 承接。同时删掉一条 file://**/content/docs/** 的 exclude:它实测匹配不到任何东西 —— 而这恰恰是门还剩一点覆盖的唯一原因,它要是真按字面生效,会把整个扫描面排除掉

⚠️ 另一条尝试过并被实测否掉的路线:--exclude '^/'(把 root-relative 链接整体排除)。在 0.24.2 上无效 —— 这类链接在 exclude 匹配之前就已解析失败,实测仍报 1348 个 error。所以 --root-dir 不是"顺手加的",而是这道门能跑绿的唯一路径。

五、既有断链修复清单(22 条,仅改链接目标,不改散文)

⚠️ 超出派单的 ≤15 条阈值(22 大于 15),已请 PM 在验收时裁定范围;每一条都有仓内可查的证据,没有一条是猜的:

ADR 链接归一(14 条)/adr/* 站点路由根本不存在:apps/docs 只有 /[lang]/docs/[[...slug]]/[lang]/blog/[[...slug]] 两条路由,apps/docs/redirects.mjs 里也没有任何 /adr 重定向 ⇒ 这些链接在线上就是 404。仓内既有约定是 GitHub blob URL(29 处在用 vs 坏写法 13 处),故归一到多数约定;12 个目标文件已逐一验证存在于 docs/adr/

  • content/docs/permissions/authorization.mdx × 11
  • content/docs/permissions/attachments-access.mdx × 1
  • content/docs/ai/connect-mcp.mdx × 1
  • state-machine 参考页 × 1 —— 修在生产者:content/docs/references/**packages/spec 的生成物(文件头有 AUTO-GENERATED 标记),第一版误改了生成物,被 os-regen pre-commit 钩子当场拦下;已按 contract-first 改到源头 packages/spec/src/automation/state-machine.zod.ts 的 JSDoc。该链接原写作 ../../../docs/adr/0020-...md,从 packages/spec/src/automation/ 出发解析到 packages/docs/adr/... —— 源头本身就是断的,生成物只是忠实复制。

quick-reference.mdx(6 条)

README.md(2 条)

content/blog(1 条)

  • /docs/specifications 页面不存在 ⇒ 改指 /docs/references(参考索引页)。⚠️这 22 条里唯一带主观判断的一条,如果 reviewer 认为该指别处或干脆去链接,请直接改。

⛔ 未触碰 content/docs/kernel/runtime-services/data-service.mdx(#6002 面锁),该文件也未出现在任何断链里。

六、反向验证(三段预期均在执行前写下)

声明 A(触发面) — 预期:PR 开出后 Check Links 必须作为 check 出现并跑完。
结果:成立。Check Links run #554(31183937132),event: pull_request,关联 PR #6304

⚠️ 过程中的一个真实教训,记下来供后来者:PR 刚开时一个 workflow 都没触发。原因不是触发器没写对,而是 mergeable_state = "dirty" —— GitHub 建不出 merge ref,而 pull_request 事件的 workflow 正是跑在 merge ref 上,于是一个 run 都不产生(不是 queued,是根本不存在)。git merge origin/main 解掉之后,6 个 workflow 立刻全部入队。"PR 上没有 check"和"触发器没生效"是两件事,别把前者读成后者。

声明 B(咬合面) — 预期:探针 commit 把 content/docs/kernel/services-checklist.mdx:188 的相对链接 ./services.mdx 改指不存在路径 ⇒ 且报错点名该链接;git revert 后 ⇒ 绿;最终该文件对 origin/main 净零 diff。
结果:三段全部成立。

commitrun结论
34b5d9f(探针)31183937132failure,exit code 2
绿6a25d48(revert)31184038710success

红段报错正是探针那一条,别无其他:

### Errors in content/docs/kernel/services-checklist.mdx
* [ERROR] file:///home/runner/work/objectstack/objectstack/content/docs/kernel/services-probe-6028-does-not-exist.mdx (at 188:16) | File not found. Check if file exists and path is correct

净零证明:git diff origin/main -- content/docs/kernel/services-checklist.mdx 输出 0 行

声明 C(确定性面) — 预期:B 的失败输出来自相对路径解析,而非任何 HTTP 请求。
结果:成立。 红段 run 的统计表:

StatusCount
🔍 Total1697
🔗 Unique744
✅ Successful1475
⏳ Timeouts0
🔀 Redirected0
👻 Excluded221
🚫 Errors1

报错是 file:// URI 的 File not found(本地路径解析),Timeouts 与 Redirected 双 0,而 221 条外链无一例外都是 [EXCLUDED] ... | This is due to your 'exclude' values —— 从未发出请求。另一项确定性证据:CI 的这组数字与本地用同版本 lychee(0.24.2)跑出来的完全一致(1697 / 744 / 1475 / 221 / 1),即同一份配置在本地与 CI 得到同一结论,可复现。整个 job 耗时 1.4 秒。

七、不在本 PR 里

按维护者 2026-08-07 裁决恢复 `Check Links`:重新启用 `pull_request`
触发(branches: main),保留 `workflow_dispatch`,`fail: true` 不变;
方向②(留空壳写注释)已被否决,故被注释掉的 `push:` 残骸按
「declared = enforced」直接删除,而不是留在文件里继续误导读者。
⛔ advisory-first:不进 required 集,也不加 `merge_group` —— advisory
车道不该消耗合并队列容量。日后若晋级 required,必须按 #6121 的教训在
同一次改动里补上 `merge_group`,否则队列会卡在一个永不上报的必需检查上。
仅检仓内链接的机制选 `--offline`,写在调用处而不是 lychee.toml:
实测 `offline = true` 这个配置键在 lychee 0.19.1 上被静默忽略、在
action 当前钉住的 0.24.2 上才生效,确定性保证不能取决于 action 恰好
装了哪个版本。`^https?://` 这类 exclude 也不够 —— 它要逐个枚举远程
scheme,漏一个就重新引入外网依赖。
门休眠半年期间配置已经漂移到跑不起来,这次一并修好(否则恢复触发器
只会让门以 exit 3 直接死掉,连链接都不检):
- `follow_redirects` 在 0.24.2 上根本不是合法键 —— 硬解析错误;
- `include_fragments` 已从 bool 变成枚举,裸 `false` 同样硬报错,
改为 `"none"` 保持原语义(不检 fragment)。
root-relative 链接是 content/** 的主力写法,而 lychee 在没有 root-dir
时对它们直接报错。补 `--root-dir <workspace>/content` +
`--fallback-extensions mdx,md` 之后,1286 条此前完全没被检查的
`/docs/*` 链接首次纳入检查面(134 → 1476 条被检)。原来的 `remap`
块一次都没生效过(URI 构造在 remap 之前就失败),已按 declared =
enforced 删除,能力由真正生效的 root-dir 承接。
修掉门首次真正运行后暴露的 22 条既有真断链(仅改链接目标,不改散文):
- 14 条 ADR 链接归一到仓内既有约定(29 处在用的 GitHub blob URL):
`/adr/*` 站点路由并不存在,apps/docs 只有 /[lang]/docs 与
/[lang]/blog 两条路由,也没有任何 /adr 重定向 —— 这些链接在线上
就是 404;
- quick-reference 三行指向已退役 schema 的表项删除(audit.zod 按
ADR-0056 移除、registry.zod 按 #4939 退役、graphql.zod 全仓无踪),
并同步修正两处小节计数;connector-auth 有 schema 无页面,改为不带
链接保留信息;plugin-security 页面在 references/kernel 而非 cloud;
- README 两处:`@objectstack/account` 已迁到 packages/apps/account;
`service-feed` 已在 #1955 删除,整行移除。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 7, 2026 1:43pm

Request Review

@hotlonghotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
`content/docs/references/**` 是 `packages/spec` 的生成物(文件头写着
AUTO-GENERATED — DO NOT EDIT),上一个 commit 只改了生成出来的
state-machine.mdx,下一次 `gen:docs` 就会把它冲掉 —— 由 os-regen
pre-commit 钩子当场拦下。
按 contract-first 修在生产者:`packages/spec/src/automation/state-machine.zod.ts`
的 JSDoc 里那条 ADR 链接原本写作 `../../../docs/adr/0020-...md`,从
`packages/spec/src/automation/` 出发解析到 `packages/docs/adr/...` —— 这个
目录并不存在,所以源头本身就是断的,生成物只是忠实地把它复制了出来。
改为仓内既有约定的 GitHub blob URL(全仓 29 处在用),它与文件位置无关,
生成到任何深度的目录都成立。
重新生成后 `content/docs/references/automation/state-machine.mdx` 与本次
生成输出完全一致(regen 后工作树对该文件零 diff),生成物与源头就此对齐。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
声明 B(咬合面)的红段:预期本次 push 让 Check Links 变红,且报错点名
content/docs/kernel/services-checklist.mdx:188 指向的
./services-probe-6028-does-not-exist.mdx。下一个 commit 会 git revert
本提交,预期转绿;最终该文件相对 origin/main 净零 diff。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
main 带来了 docs 生成器改动(changeset
`docs-gen-description-line-layout-and-nested-links`:去掉逐行空行布局、
不再把行内代码路径转成嵌套链接)。该参考页是生成物,两边都改过,
文本合并的结果并不等于生成器的输出 —— os-regen 钩子说的正是这种
「被合并但没有真正重新生成」的状态。
`gen:schema && gen:docs` 后仅此一个文件有 diff(232 个生成文件里的
1 个),内容即新生成器的输出;本单在源头 JSDoc 里改的 ADR-0020 链接
原样保留。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation ci/cd labels Aug 7, 2026
@hotlong
hotlong marked this pull request as ready for review August 7, 2026 14:31
@hotlong
hotlong added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit 5b103d6Aug 7, 2026
27 checks passed
@hotlong
hotlong deleted the claude/issue-6028-restore-check-links-gate branch August 7, 2026 14:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@hotlong@claude