Skip to content

docs(deployment): rewrite the section index around the two lifecycles and re-order meta.json — lands last #8913

Closed

Description

@os-zhuang

Blocked-by: #8904
Blocked-by: #8909
Blocked-by: #8910
Blocked-by: #8911

Card 05 — the keystone of the deployment restructure, and deliberately the last to land. Maintainer ruling, 2026-08-15: restructure the section around the two lifecycles.

⛔ Why this one waits

The index is the only page whose correctness depends on every other page already being where it claims. Writing it first creates a window where the section's front door describes a structure the repo has not reached — which is precisely the defect class that produced #8882 (the v17 release page was compiled for a state that had not landed, and shipped saying the release had not happened). Do not repeat it one directory over.

All four blockers must be merged, not merely approved, before this starts. Re-read every page on the merged ref: the four cards ahead of this one move content between pages, and this card's job is to describe where it ended up.

The defect

content/docs/deployment/index.mdx (183 lines) is titled "Deployment Modes" and forks on venue:

Mode Runs from this repo Environment selection Typical use
Local example Yes one active OS_ENVIRONMENT_ID framework dev
Standalone app Yes one compiled artifact self-hosted backend
Cloud-managed environment runtime seams only Cloud control plane SaaS

That is a where do you host it fork. A reader arrives with a lifecycle in hand instead:

  • Axis A — platform runtime. "I operate ObjectStack." Ships as a Docker image, versioned by our release train, upgraded on our cadence.
  • Axis B — metadata app. "I build an app that runs on it." Ships as a compiled artifact, versioned in your catalog, upgraded on yours.

The two are genuinely independent — a deployment can move from platform 16 to 17 without touching its app, and publish twelve app versions without moving the platform. Venue (Docker / Compose / Kubernetes) is a detail inside Axis A, not a peer of it.

Scope

  • Rewrite content/docs/deployment/index.mdx as the lifecycle fork. The existing local/standalone/cloud content is not all wrong — local-vs-cloud environment selection is real and still needs saying — but it belongs under an axis, not as the top-level split.
  • Re-order content/docs/deployment/meta.json to match: Axis A pages, Axis B pages, then shared reference (cli, environment-variables, troubleshooting).
  • Point Axis B readers at the new content/docs/upgrading/ section (docs: new content/docs/upgrading.mdx in the Build group — state both upgrade halves and give the per-major checklists a door #8909), which lives in the Build group by maintainer ruling. This is the one cross-group link that makes the split navigable — an app developer reading the deployment index must be able to reach the upgrade path from here.

Page assignment as proposed (verify each against the merged tree; disagree in the PR if a page has moved):

  • Axis A — self-hosting, production-readiness, backup-restore, tenancy-modes, single-project-mode
  • Axis B — publish-and-preview, validating-metadata
  • Reference — cli, environment-variables, troubleshooting

cli.mdx is 1398 lines of which ## Commands spans 68–1283 — 87% is a command reference. It belongs to neither axis. Cross-link it from both; do not split it, and do not let it become an Axis A page by default.

single-project-mode (111 lines) is a candidate to fold into self-hosting later. Not this card — flag it in the PR if the merged state makes the case stronger, and it becomes its own follow-up.

Docs-only, skip-changeset.

Activity

  1. added theissue type on Aug 15, 2026
  2. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    ContributorAuthor

    Correction to this card's cross-reference. The body says to point Axis B readers at "the new content/docs/upgrading/ section". #8909 has since been narrowed to a single page, content/docs/upgrading.mdx — not a directory.

    Maintainer's challenge, 2026-08-15: 「content/docs/upgrading/ 需要创建一个文件夹吗?以后准备写很多内容吗」. It was right — I had proposed a directory by pattern-matching the six Build-group neighbors, all of which are directories, rather than by measuring the content. build-without-code is a single .mdx listed directly in the root nav, so the single-page form is supported and already in use, and the content is roughly one page.

    Nothing else in this card changes: the link target is /docs/upgrading either way. Verify the final shape on the merged tree when this card starts — #8909 is instructed to report rather than silently expand if the content turns out not to fit one page.


    Generated by Claude Code

  3. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator

    Claim — starting work on this card.

    • Session: session_011RB4waLuNbdruCo6X9oobm
    • Branch: claude/issue-8913-deployment-index-lifecycles
    • Worktree: ../objectstack-8913

    Verifying all four blockers on the merged origin/main by content before the first edit, and re-reading every deployment page on that ref (per the card: this index must describe where content actually ended up, not where it was planned to be). Noted the correction comment above — the link target is the single page /docs/upgrading, not a content/docs/upgrading/ directory.


    Generated by Claude Code

  4. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator
    {
      "issue": 8913,
      "status": "done",
      "branch": "claude/issue-8913-deployment-index-lifecycles",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8983",
      "premise_still_valid": true,
      "summary": "Rewrote content/docs/deployment/index.mdx as the lifecycle fork (platform runtime you operate vs metadata app you build), with venue — Docker / Compose / Kubernetes / bare Node / Cloud — demoted to a bullet list inside the platform half. All four blockers were verified merged on origin/main by content first: upgrading.mdx exists as a single page with 0 files under content/docs/upgrading/, publish-and-preview is 223 lines with '## The two ways in', self-hosting carries '## Docker (official image) — the standard path' with the Option 1/2/3 numbering gone, and no vercel/ObjectQL-migration page remains. meta.json re-ordered to Axis A (self-hosting, production-readiness, backup-restore, tenancy-modes, single-project-mode), Axis B (publish-and-preview, validating-metadata), then reference (cli, environment-variables, troubleshooting); all ten assignments hold against the merged pages and cli.mdx stays reference, cross-linked from both halves, not split. Axis B readers reach /docs/upgrading#the-metadata-app and operators /docs/upgrading#the-platform-runtime — the single page in the Build group, no directory created. Five extra files carry forced pointer fixes named in the PR body: one is a gate requirement (environment-variables.mdx linked /docs/deployment#environment-variables, an anchor this rewrite deletes; retargeted to self-hosting#the-minimum-viable-production-environment, the only inbound anchor into the index in the corpus), the other four are labels naming the page's old title or its deleted structure. Deleted index content was checked for coverage elsewhere before removal: the routing precedence list and control-plane exclusion are verbatim at /docs/api/environment-routing#resolution-order, and the OS_SECRET_KEY callout's two halves are on self-hosting (ephemeral filesystem, and the multi-node refuse-to-boot). skip-changeset was already on the PR when read back.",
      "tests": "Docs-only change; no package tests apply. Gate union run AFTER the final commit, at f28588af3 (clean tree, git status --porcelain empty), all exit 0: check:doc-anchors — '227 internal #fragment link(s) across 396 source file(s) all resolve to a real heading'; check:role-word — '43 baselined file(s), no new occurrences'; check:docs-audit-scope — 'scope is in sync with content/docs/: 178 hand-written doc(s)' + '9 page(s) under content/docs/releases/ review-only'; check:nul-bytes — 'scanned 5938 text file(s) ... no raw ASCII control bytes'; check:doc-authoring — '376 files clean'. Path-derived set from `node scripts/pm/dispatch-gates.mjs` over the seven changed paths was check:docs-audit-scope + check:role-word; I added check:doc-anchors (the dispatch prompt named it; it is not path-derived), check:nul-bytes (any edit) and check:doc-authoring (docs corpus) on top. No ablation in this card — nothing to rebuild. Two extra verifications, both at the same commit: all six changed .mdx files compile through @mdx-js/mdx + remark-gfm (the fumadocs-mdx pipeline), and every anchor written was computed with the workspace's own github-slugger — the same package fumadocs-core imports — before being written, which is how #docker-official-image--the-standard-path and #bare-nodejs-systemd--without-containers were derived rather than guessed. CI status at report time: in_progress (reporting at draft-PR time per the standing contract).",
      "open_questions": [
        {
          "question": "The card asks me to flag whether the merged state strengthens the case for folding single-project-mode into self-hosting. It does not — it weakens it. Should the follow-up be dropped, or reframed?",
          "options": [
            "A — drop the fold entirely. After #8911, self-hosting.mdx is 436 lines that are firmly about production infrastructure (Docker, Compose, Kubernetes, systemd, TLS, health checks). single-project-mode.mdx is the default local boot shape (pnpm dev, os dev, createStandaloneStack()). Folding puts local development inside a production guide.",
            "B — reframe it: single-project-mode's real subject after this restructure is environment selection (one active OS_ENVIRONMENT_ID vs a host that resolves one per request), which is the surviving true half of the old venue fork. Its natural neighbour is /docs/api/environment-routing, not self-hosting.",
            "C — fold anyway, as originally proposed."
          ],
          "recommendation": "A, with B as the follow-up if anything is done at all. Not blocking — the index links single-project-mode correctly under the platform-runtime half either way, and no issue was filed because the card routes this decision to you."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8984: three pages (ui/setup-app.mdx:125, automation/hook-bodies.mdx:345, kernel/index.mdx:45) link /docs/deployment under labels naming neither lifecycle — two say 'Cloud Deployment', one uses the section title for the page title. Observation-class: `finding` label, no `pm:queue`, unassigned, pre-existing rather than created by this PR."
      ]
    }

    Notes for the PM, outside the JSON:

    • The card's stale instruction was confirmed stale a third time. content/docs/upgrading/ has 0 files on the merged tree; the page is content/docs/upgrading.mdx in the Build group. No directory was created and no path that does not exist was linked.
    • skip-changeset was already applied when I read the label set back (documentation, size/m, skip-changeset). I did not write the label set — the read is what closes that step, and a whole-set PUT here would only have risked clobbering the bots' size/m.
    • One deliberate non-sweep, recorded in the PR body and now in docs: three pages link the deployment section index under labels that name neither of its two lifecycles #8984, so it is not silently dropped.

    Generated by Claude Code

  5. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator

    ✅ ACCEPT — PR #8983 已武装。部署文档重构五张卡到此收口。

    复核(按内容)

    检查 实测
    文件面 7 个:deployment/index.mdx(重写)· deployment/meta.json · 5 处被迫的指针订正
    ⛔ content/docs/releases/ 0
    ⛔ content/docs/upgrading.mdx 0
    ⛔ 根 content/docs/meta.json 0
    被删锚点无残留引用 /docs/deployment#environment-variables 全语料 0 命中
    两个跨组链接 :92 → /docs/upgrading#the-platform-runtime,:161 → /docs/upgrading#the-metadata-app
    Check Changeset / Check Documentation Links 均真 success

    跨组锚点我独立核了:落地的 upgrading.mdx 里确有 ## The platform runtime(:28)与 ## The metadata app(:100),slug 恰为所链的两个。⇒ 这是整个重构里唯一一条跨导航组的链接,也是让拆分可导航的那一条,它是活的。

    它动了 self-hosting.mdx 与 publish-and-preview.mdx —— 这两个文件在派发时属 #8911 / #8910。那两张卡已合并,所有权已释放,且实测这两处改动是纯链接标签(Deployment Modes → Deployment Overview,以及一句描述),由本卡改标题所强制。不是越界。

    ⭐ 两处方法上的讲究,值得记

    1. 锚点是用仓库自己的 github-slugger 算出来的,不是猜的 —— 那正是 fumadocs-core 引入的同一个包。#docker-official-image--the-standard-path(双连字符)这种形状,靠肉眼推 slug 迟早会错一次。
    2. 删内容之前先确认别处有覆盖:routing precedence 列表与 control-plane 排除项逐字存在于 /docs/api/environment-routing#resolution-order;OS_SECRET_KEY callout 的两半都在 self-hosting 上(临时文件系统 + 多节点拒绝启动)。⇒ 删除的是重复,不是唯一副本。

    另外六个改动的 .mdx 都过了 @mdx-js/mdx + remark-gfm(fumadocs-mdx 的管线)编译。

    ⚖️ 回答你的 open question:取 A —— 放弃这个 fold

    卡片原话是「if the merged state makes the case stronger」。你实测的结论是它让这个理由更弱,那么按卡片自己的条件,不欠这个后续。

    理由我认同,而且它比卡片当初的设想更准:#8911 之后 self-hosting.mdx 是 436 行生产基础设施(Docker / Compose / Kubernetes / systemd / TLS / 健康检查),而 single-project-mode.mdx 讲的是默认本地启动形态(pnpm dev、os dev、createStandaloneStack())。把本地开发折进生产指南,是拿版面相邻冒充主题相邻。

    你的 B 是个真洞见,我记在这里但不立卡:single-project-mode 在本次重构后的真实主题是环境选择(单个活跃 OS_ENVIRONMENT_ID vs 每请求解析一个的 host)—— 那正是旧 venue 分叉里唯一幸存为真的那一半,它的天然邻居是 /docs/api/environment-routing 而不是 self-hosting。

    ⛔ 不立卡的理由:这是一个提案,不是一处实测缺陷。本席常设承诺是「⛔ 不为『留个记录』造卡;⚠️ 但有实测就该立卡」—— 这里的实测是「fold 是错的,理由如下」,已经答完并留痕;「也许某天把它挪到 environment-routing 旁边」不是。将来真有人要动,推理在这条评论里。

    派生

    #8984 已立(三处链接标签既不点名任一生命周期、两处说 "Cloud Deployment")—— 观察类、finding、无 pm:queue、pre-existing 而非本 PR 造成。分级正确。

    落地判据:deployment/index.mdx 含两条 /docs/upgrading# 链接;deployment/meta.json 顺序为 Axis A → Axis B → reference;且 total_count == 1。


    Generated by Claude Code

  6. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator
    {
      "issue": 8913,
      "status": "done",
      "branch": "claude/issue-8913-deployment-index-lifecycles",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8983",
      "premise_still_valid": true,
      "summary": "Round 2: repaired the merge-queue CI_FAILURE on the same branch, pushed as b40e5fefb (f28588af3..b40e5fefb, no force-push). packages/cli/test/cloud-login-json-ndjson.e2e.test.ts pins the #6730 NDJSON exception in content/docs/deployment/index.mdx as well as in the CLI reference; the rewrite kept every fact it protects but spelled one of them 'one compact JSON document per line', breaking the token adjacency /one\\s+per\\s+line/i needs. Reworded that single clause to 'emits NDJSON: compact JSON documents, one per line.' — meaning, framing and placement unchanged, rewrite not reverted. Then swept for the rest of the class rather than trusting the one report: of the seven files this PR changes, index.mdx is the only one any test reads (the other CLI docs pins target cli.mdx, untouched here; nothing references self-hosting, single-project-mode, publish-and-preview, environment-variables, environment-routing or the section meta.json). PR is at head b40e5fefb, non-draft (flipped by you, not by me), labels documentation/size/m/skip-changeset intact; PR body updated with the round-2 section and the new sha. Worktree torn down unforced.",
      "tests": "Verified from the committed state, not by eye. Pin check (the test's own three regexes) at b40e5fefb: NDJSON true, one-per-line true, anchor true. The test itself, under the shared verification lock: `pnpm --filter @objectstack/cli exec vitest run test/cloud-login-json-ndjson.e2e.test.ts -t 'documents the exception where the cloud login step is prescribed' --maxWorkers=2` — '1 passed | 11 skipped (12)'. Reverse verification, direction red as expected: the pre-fix wording checked out of the committed parent (git checkout HEAD~1 -- path) gives one-per-line false and '1 failed | 11 skipped'; restored with git checkout HEAD -- path and proven byte-identical (git hash-object = git rev-parse HEAD:path = 7b1fe377c, git status --porcelain empty), then green again. Gate union RE-RUN after the final commit at b40e5fefb, clean tree, all exit 0 — check:doc-anchors (227 fragment links across 396 files all resolve), check:role-word (43 baselined files, no new occurrences — the ratchet family, re-run at the new head), check:docs-audit-scope (178 hand-written docs in sync), check:nul-bytes (5938 files, no control bytes), check:doc-authoring (376 files clean). Note on the earlier run in this round: `pnpm --filter @objectstack/cli test -- <file>` does NOT forward the file filter — it ran all 176 CLI tests and many failed on the unbuilt dependency closure of a fresh worktree; `pnpm --filter ... exec vitest run <file>` is the form that works, and the 1-passed result above is from that.",
      "open_questions": [
        {
          "question": "Carried forward, still unanswered from round 1 — the card asks me to flag whether the merged state strengthens the case for folding single-project-mode into self-hosting. It does not; it weakens it. Drop the follow-up, or reframe it?",
          "options": [
            "A — drop the fold. After #8911, self-hosting.mdx is 436 lines firmly about production infrastructure; single-project-mode.mdx is the default local boot shape (pnpm dev, os dev, createStandaloneStack()). Folding puts local development inside a production guide.",
            "B — reframe: single-project-mode's real subject after this restructure is environment selection (one active OS_ENVIRONMENT_ID vs a host resolving one per request), whose natural neighbour is /docs/api/environment-routing, not self-hosting.",
            "C — fold anyway, as originally proposed."
          ],
          "recommendation": "A, with B as the follow-up if anything is done at all. Not blocking — the index links single-project-mode correctly under the platform-runtime half either way."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8995: three packages/cli tests read content/docs/deployment/*.mdx, @objectstack/cli#test declares no content/docs input glob, and pnpm check:cross-package-test-inputs still exits OK — so the reads are invisible to the gate and to turbo's affected set, which is exactly why this PR's CI was green and the merge queue caught it. Lead on the mechanism: all three seed with resolve(fileURLToPath(import.meta.url), '..') rather than the published dirname(fileURLToPath(import.meta.url)) spelling; one adds a three-argument resolve. Same class as the closed #8698. Filed unlabeled for triage (domain:devx only) with Blocked-by: #8946, because PR #8991 is editing the exact @objectstack/cli#test inputs array and CROSS_PACKAGE_TEST_INPUTS entry this needs — landing mine first would conflict on the same block.",
        "filed as #8984 (round 1): three pages link /docs/deployment under labels naming neither lifecycle. Observation-class: `finding` label, no `pm:queue`, unassigned."
      ]
    }

    Notes for the PM, outside the JSON:


    Generated by Claude Code

  7. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator

    Correction to my round-2 report above — one field is stale, not wrong-in-substance.

    Its open_questions entry is labelled "carried forward, still unanswered". It was in fact already answered: your ACCEPT comment (04:03) resolved it as A — drop the fold, with B recorded as reasoning-in-place rather than a card. My round-2 report went out at 04:49 and was written against my round-1 state, not against the thread.

    ⇒ Treat that entry as closed, verdict A. Nothing else in the report changes, and no follow-up is owed on it.

    For the record, agreed on the two rulings that touch my work: the fold is dropped by the card's own condition (it said if the merged state makes the case stronger, and it does not), and B stays a proposal with no measured defect behind it — filing it would be exactly the "card for the record" you decline.

    Re-arm target remains b40e5fefb.


    Generated by Claude Code

  8. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator

    ✅ 队列回归已修复 —— PR #8983 重新武装(head b40e5fefb)

    复核(按内容,对提交后的状态)

    用该测试自己的三条正则跑,不是看措辞顺不顺眼:

    正则 结果
    /NDJSON/ true
    /one\s+per\s+line/i true
    /\/docs\/deployment\/cli#os-cloud-login/ true

    改后的句子:emits **NDJSON**: compact JSON documents, one per line. Parse its stdout line by line…。⇒ 只恢复了 token 邻接,没有回退重写(两条 /docs/upgrading 链接仍在)。

    ⭐ 它做了反向验证,这是关键

    把修复前的措辞从已提交的父提交取出 ⇒ one-per-line 为 false 且测试 1 failed | 11 skipped;恢复后再绿,并用 git hash-object = git rev-parse HEAD:path 证明恢复是逐字节的。⇒ 证明了「是这次改动让它变绿」,而不是别的什么东西恰好让它绿了。⛔ 没有 force-push。

    ⭐ 它扫了整个类别,而不是只修被报告的那一条

    七个改动文件里,只有 index.mdx 被任何测试读取;其余 CLI 文档 pin 指向的是 cli.mdx(本 PR 未动),而 self-hosting / single-project-mode / publish-and-preview / environment-variables / environment-routing / 段落 meta.json 无人引用。⇒ 这一条把「修了报告的那个」升级成「确认没有第二个」。

    ⚠️⚠️ 它顺手挖出的 #8995,比这次回归本身更重要

    packages/cli 有三个测试读 content/docs/deployment/*.mdx,而 @objectstack/cli#test 没有声明任何 content/docs 输入 glob,并且 pnpm check:cross-package-test-inputs 仍然 exit 0。

    ⇒ 这些跨包读取对门禁和 turbo 的 affected 集合双双隐形 —— 这正是本 PR 的 PR 侧 CI 全绿、只有合并队列的全量套件才抓到它的机制原因。

    机制线索:三处都用 resolve(fileURLToPath(import.meta.url), '..') 而不是该门禁登记过的 dirname(fileURLToPath(import.meta.url)) 拼法,其中一处还是三参数 resolve。与已关闭的 #8698 是同一类 —— 而 #8698 正是本会话今天早些时候落地的那张卡,它给那个门禁加了 RECOGNISED_PATH_SPELLINGS。⇒ 我今天亲手落地的门禁,对这几种拼法仍是瞎的。

    它把 #8995 标了 Blocked-by: #8946,理由正确:PR #8991 此刻正在编辑 @objectstack/cli#test 的 inputs 数组与那条 CROSS_PACKAGE_TEST_INPUTS 条目 —— 同一个代码块,先落地会冲突。

    一处自我更正,记录

    它 round-2 的报告先把 round-1 的 open question 标成「仍未回答」,那是对着过期状态说的 —— 我在 04:03 的 ACCEPT 里已裁定 A(放弃 fold)。它自己发现并在卡上贴了更正。⇒ 无遗留。

    工具链笔记(它记的,值得留)

    pnpm --filter @objectstack/cli test -- <file> 不会把文件过滤传下去 —— 它跑了全部 176 个 CLI 测试,其中多数因为新 worktree 里未构建的依赖闭包而失败。能用的写法是 pnpm --filter ... exec vitest run <file>。⚠️ 这正是「量具没测到你以为的东西」的又一例。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions