Repository navigation
Commit d5fd948
feat(cli): os deploy and the os dev d key deploy to a self-hosted runtime remembered by os environments use-runtime (#31137)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiUfC8G7ZWNaCdELoQcXkaCC)_
Fixes #30888
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:新增 `os environments use-runtime 地址 --token
密钥`,记住一个私有部署的运行时(Docker 社区版或企业版)并设为部署目标;之后 `os deploy` 和 `os dev` 里按 `d`
不带任何参数就部署到它:先构建,再请运行时做一次预演(打印升级计划,含 `renamedFrom`
声明的改名),然后把构建产物直接装进去,按运行时的 `hotLoaded` 回答报告是否已生效。`os package install` 不写
`-u`/`-t` 时也用它。
- **为什么改**:私有部署现在每次都要手敲地址和令牌,默认地址 3000 还和官方镜像的 8080
对不上;维护者要求私有部署的发布流程和云端一样顺手(epic cloud#3652)。记住的目标让默认端口问题不再需要改默认值。
- **风险与回滚**:只动 CLI。地址和密钥存在
`~/.objectstack/cloud.json`(0600)里独立的一段,云端登录/登出都保留它,密钥只发给它自己的地址。Q3
的「默认拒绝」由 install-local 自己判(#30884 第二个 PR 尚未合入),本 PR
只负责把它的拒绝原样打印并以非零退出。回滚 = revert 本 PR。
-
**先看**:`packages/cli/src/utils/cloud-config.ts`(两半存储,登录/登出不丢目标)、`packages/cli/src/commands/deploy.ts`
的 `deployToRuntime`、`packages/cli/src/utils/runtime-target.ts` 的
`runtimeInstallTarget`(密钥只发给同一地址)。
- **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run --project
unit test/deploy.test.ts
test/environments-use-runtime.test.ts`,预期全部通过,其中「a self-hosted runtime
as the active target」一组 8 条。
- **需要你做的**:无,席位落地。
## What changes
**`os environments use-runtime URL --token KEY`** (new) records a
self-hosted runtime and an API key it issued, and makes it the deploy
target. The name is a verb beside `os environments switch`, and
"runtime" is the word `os package install` already uses for this server
(`OS_RUNTIME_URL` / `OS_RUNTIME_TOKEN`). Before writing, the key is
tried with one authenticated `GET /api/v1/marketplace/install-local`. A
rejected key (401/403), a runtime that does not serve the door (404), or
one that cannot be reached is refused and nothing is recorded;
`--no-verify` records anyway. `--forget` drops the record. `os
environments switch ID`, when it records into `cloud.json`, makes the
cloud the target again and keeps the runtime remembered. Whichever ran
last wins.
**Storage** (card item 1: "the same protected file the cloud token
uses"). `~/.objectstack/cloud.json` (0600) gains a deploy-target half,
`runtimeTarget: { url, token, recordedAt }` and `deployTarget: 'cloud' |
'runtime'`. It is never read by the cloud credential path.
`writeCloudConfig` carries it across every cloud write, so `os cloud
login` keeps it. `deleteCloudConfig` keeps it, so `os cloud logout`
clears only the session. A file that holds only the runtime half reads
as "not logged in" to the cloud. Only `writeDeployTargetRecord` changes
the half. `login.ts` and `logout.ts` are untouched.
**`os deploy`** — when a runtime is the active target and `--env` is not
given (card items 2–4):
1. It builds with `compileForDeploy`, as the cloud path does.
2. It asks install-local for a dry run (`dryRun: true`, from #30884 PR
1) and prints the upgrade plan with the same renderer as `os package
install --dry-run`. Declared `renamedFrom` renames are listed.
3. It installs the artifact inline. There is no catalog publish and no
`-dev` version. `--confirm-access-upgrade FINDING_SET` passes through to
the install. The flag is refused when the target is the cloud.
4. It reads the answer. install-local registers the package before it
answers, so `hotLoaded` means live, with exit 0. A package the kernel
could not load gives exit 1.
Steps 2–3 run `os package install` in-process, as the cloud path runs
`os package publish`. Every refusal is printed once, in one place, and
the retry lines name `os deploy`. If the dry run is refused, nothing
more is sent. If the runtime answers 404 (a runtime booted from a
composed artifact, or one without the door), the deploy names the cause
and exits 1. If the runtime predates the dry run, the deploy says so and
exits 1.
**The `d` key** names the remembered runtime in its offer line and runs
the same `os deploy` with no cloud login prompt.
**`os package install`** without `-u` / `$OS_RUNTIME_URL` uses the
remembered runtime. Without `-t` / `--email` it uses that runtime's key,
but only when the URL is the runtime's own (`isSameControlPlane`). The
`url` flag loses its oclif `default` so that an absent flag can be told
from a typed one. With nothing remembered, the URL is still
`http://localhost:3000`. The 404 sentence now names the
composed-artifact case.
**Item 5 (default port).** The remembered target makes it moot, so the
default stays `http://localhost:3000`.
## Tests
- `test/deploy.test.ts`, new top-level describe "a self-hosted runtime
as the active target" (8). Covers: no-flag deploy (dry run, then inline
install, plan printed, key sent, no `-dev` suffix, no cloud call); a
dry-run refusal stops before the install; 404 named; runtime predating
the dry run; access-upgrade refusal with retry `os deploy
--confirm-access-upgrade …`, and the token passed through on rerun;
`hotLoaded: false` gives exit 1; a failed build installs nothing; the
flag refused on the cloud target.
- `test/environments-use-runtime.test.ts` (new, 10). Covers: record
after probe (0600, cloud session kept, cloud reads never see the key,
still "not logged in" without a cloud login); 401 and 404 refused with
nothing recorded; `--no-verify`; URL validation; `--forget`. For
`cloud.json`: login keeps the runtime target, logout keeps it (and
deletes the file when nothing else is left), and `switch` activates the
cloud.
- `test/package-install-url-token-flags.test.ts`, new describe (5).
Covers: fallback to the remembered runtime and its key; `-u` to the same
runtime keeps the key; `-u` to another server never gets the key;
explicit `-t` wins; `--email` never mixes the two; nothing remembered
falls back to localhost:3000.
- `test/dev-deploy-key.test.ts`, new describe (2). Covers: the runtime
offer line, and `d` deploying with no login; the wiring in
`startDevDeployKey`.
- Ablation (one-shot): `isSameControlPlane(stored.url, url)` was
replaced with `true` in `runtime-target.ts`. The test "-u naming another
server never receives the remembered key" went red (1 failed / 13
passed), and the file was restored to its HEAD blob `900b6fe7`.
## Acceptance notes
- **Q3 is install-local's verdict, and its refusal half has not
landed.** On `origin/main`, install-local has the dry run (#30884 PR 1,
#30983) but not the Q3 refusal, `confirmSchemaUpgrade` or
`--confirm-upgrade` (#30884 PR 2, back in the queue per handoff
6688284693). This PR prints the plan and stops on any refusal the door
answers, from the dry run or the install. It adds no client-side copy of
the verdict and no `--confirm-upgrade` flag, because the door has no
body key for it to pass through. Until PR 2 lands, a deploy whose plan
lists a suspected rename installs just as `os package install` does
today, with the plan printed first. When PR 2 lands, its
`--confirm-upgrade` should be forwarded by `os deploy` the same way
`--confirm-access-upgrade` is.
- **Cloud-bound runtime through the catalog** (card item 2, second
half): not done. A follow-up.
- **The composed-artifact refusal shape** is inferred from the card
("declines" install-local), not measured. The cloud repo is not readable
from this session. A 404 gets the sentence above, and any other refusal
is printed with its code and message.
- `os environments create --activate` does not switch the deploy target
back to the cloud; only `switch` does.
- Docs (`content/docs/deployment/cli.mdx` for `os deploy`, the
environments table and `os package install`) and the platform skill's
`operations.md` row are not updated here. Both are outside the claim's
file surface and are listed for the seat.
## Verification
<details><summary>Local gates on head b57c03a (merge base 5f00374;
origin/main merged once, already up to date)</summary>
```
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 :: exit 0 (397 files / 5603 tests passed)
pnpm --filter @objectstack/cli typecheck :: exit 0 (tsc + check:test-typecheck, NODE_OPTIONS=--max-old-space-size=8192)
eslint packages/cli --no-inline-config :: exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack :: exit 0 (62 families)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-test-child-env :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-coverage :: exit 0
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vouch-for-reference-callers :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list :: exit 0 (62 derived, 62 run, 0 NOT-MEASURED)
```
First passes that exited 3 for a missing prerequisite were rebuilt and
rerun (the exit-0 lines above): `check-plugin-teardown-shape
--self-test` (shallow clone, so the pinned control commit was fetched),
`check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage`,
`check:i18n-walk-parity` (built output missing, so those packages were
built), and `check:type-check-debt` (lock queue timeout, rerun with a
slot). The CLI `integration` tier is left to CI: the diff touches no
spawn entry and no integration-tier file.
</details>
<details><summary>Manual check on the Community image</summary>
NOT RUN: docker is not available in this container. Steps: `docker run
-p 8080:8080 ghcr.io/objectstack-ai/objectstack` (`os start`); create an
API key in Setup → API Keys; in a fresh scaffold run `os environments
use-runtime http://localhost:8080 --token KEY`, then `os dev`, press
`d`, edit an object label, press `d` again, and confirm that `GET
http://localhost:8080/api/v1/meta/object/NAME` serves the second label.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent cbe20ab commit d5fd948
12 files changed
Lines changed: 1103 additions & 45 deletions
File tree
- .changeset
- packages/cli
- src
- commands
- environments
- package
- utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
48 | 73 | | |
49 | 74 | | |
50 | 75 | | |
| |||
56 | 81 | | |
57 | 82 | | |
58 | 83 | | |
| 84 | + | |
| 85 | + | |
59 | 86 | | |
| 87 | + | |
60 | 88 | | |
61 | 89 | | |
62 | 90 | | |
| |||
66 | 94 | | |
67 | 95 | | |
68 | 96 | | |
69 | | - | |
| 97 | + | |
| 98 | + | |
70 | 99 | | |
71 | 100 | | |
72 | 101 | | |
73 | 102 | | |
74 | 103 | | |
| 104 | + | |
75 | 105 | | |
76 | 106 | | |
77 | 107 | | |
| |||
105 | 135 | | |
106 | 136 | | |
107 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
108 | 143 | | |
109 | 144 | | |
110 | 145 | | |
111 | 146 | | |
112 | 147 | | |
113 | 148 | | |
114 | 149 | | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
115 | 166 | | |
116 | 167 | | |
117 | 168 | | |
| |||
229 | 280 | | |
230 | 281 | | |
231 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
232 | 336 | | |
233 | 337 | | |
234 | 338 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
27 | 33 | | |
28 | 34 | | |
29 | 35 | | |
| |||
70 | 76 | | |
71 | 77 | | |
72 | 78 | | |
| 79 | + | |
73 | 80 | | |
74 | 81 | | |
75 | 82 | | |
| |||
88 | 95 | | |
89 | 96 | | |
90 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
91 | 104 | | |
92 | 105 | | |
93 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
0 commit comments