Repository navigation
feat(cli): os deploy — the managed-package dev loop in one command; installs pick the only environment, keep it across login, and refuse before any write - #30933
Conversation
…y, next steps Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
… name Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
…d), waking the environment each poll Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 76 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 19 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4eb56e40abd3037f290a85e9858cc7df19ede4d0 && git checkout 4eb56e40abd3037f290a85e9858cc7df19ede4d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d56914aaa8c2cf388986309364eef23eea10282 c7aab69d877255d0a9b9aa34b641356e2c09f082 && git checkout -B drift-repro 4d56914aaa8c2cf388986309364eef23eea10282 && git merge --no-ff c7aab69d877255d0a9b9aa34b641356e2c09f082
node scripts/docs-audit/affected-docs.mjs --json 4d56914aaa8c2cf388986309364eef23eea10282
|
…e code, SERVICE_UNAVAILABLE Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2 Co-authored-by: Claude <noreply@anthropic.com>
|
repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2 席位核对 · ACCEPT · 2026-11-06T14:08ZHead-sha:
|
Refs #30871
Clause-②: no
Metadata-protocol: none
维护者速读
os deploy:不带任何参数就是完整开发循环——编译、发布MANIFEST_VERSION-dev.TIMESTAMP、装进组织唯一的环境、等到环境真正换成新版本。os dev起来后按d就部署(没登录先登录),保存文件不会部署;os build之后提示下一步,裸跑os先列日常命令;脚手架带npm run deploy。os package publish --install也改为自动选唯一环境、找不到环境时在写任何东西之前非零退出;os cloud login不再清掉已记下的环境。os package publish --install在没有可用环境时从「跳过安装、退出 0」变成「什么都不发布、退出 1」,依赖旧行为的脚本会变红;「等待生效」读取 cloud#3662(已合并)的data.served;旧版 cloud 不返回该字段时命令会明说无法确认、不会假装已生效。等待期间每轮会向环境自身发一个请求以触发重建(环境按请求才重建)。回滚即 revert 本 PR,无数据迁移。packages/cli/src/commands/deploy.ts(整体顺序:哪些拒绝发生在写之前)、packages/cli/src/utils/served-version.ts(interpretServedReport:live / failed / pending / 被覆盖 的判定)、packages/cli/src/commands/package/publish.ts(Step 0 前移到第一次写之前)。pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/deploy.test.ts test/publish-active-environment-store.test.ts,预期全部通过,其中「several environments … before any write」一项证明拒绝时没有任何 POST。What changes
Command name:
os deploy(notos push). It matches the scaffold script the amendment names (npm run deploy), it is the verb developers already know for "put this on the server and make it live" (vercel deploy,fly deploy,wrangler deploy), andpushreads asgit push— uploading source, not installing and waiting. Nodeployid existed; the retiredos publishid stays retired.Usage:
Flags:
--env($OS_ENVIRONMENT_ID),--no-wait,--wait-timeout SECONDS(default 600),-s/--server($OS_CLOUD_URL),-t/--token($OS_CLOUD_API_KEY).Card items 1–3, applied to every command that installs into a cloud environment (
os package publish --installandos deploy;os package installtargets a runtime URL and names no environment, so it has nothing to resolve):utils/install-target.ts, one resolver for both:--env/$OS_ENVIRONMENT_ID, then the active environmentcloud.jsonrecords for this control plane (the existing gate inactive-environment.ts, not duplicated), thenGET /api/v1/cloud/environments(the control plane already filters it to the caller's organizations — cloudenvironment-crud/environments.ts, authz rowcaller-own-org). Exactly one installable row (notarchived, notis_system) → used and printed.os cloud login—mergeCloudLoginkeepsactiveEnvironmentIdwhen the previous file names the same control plane and the same account (user ids agree, or one side recorded none); otherwise dropped as before.--envandos environments switch.publish.tsresolves the target as a new Step 0, ahead of Step 1.Amendment items:
os deploy(commands/deploy.ts). Credential and environment resolve first, then the build (a childos compile, as every other compile in this CLI; a failed build prints its errors, exits 1, nothing published), then the version (utils/dev-version.ts: UTC timestamp; an existing pre-release gains.dev.TS; build metadata dropped; non-semver refused), thenos package publishitself run in-process with that version and--install --env RESOLVED(so manifest-id, namespace andobjectstack.manifest.jsonrules stay in one place; it now records aPublishOutcome), then the wait.os devdeploy key (utils/dev-deploy-key.ts): attached when the first server listens, only on a TTY.druns a childos deploy(the terminal is handed back while it runs so a login prompt works; Ctrl-C, which raw mode swallows, is forwarded as SIGINT). Not logged in → the line offers login, anddrunsos cloud loginfirst. The child does not inheritOS_ENVIRONMENT_ID: underos devthat variable is the LOCAL environment id, andos deployreads the same name as its cloud--env. No save hook (cloud ADR-0024).create-objectstack's blank template ships"deploy": "objectstack deploy"; so doesos init'sapptemplate (the only one withdev).os buildends withNext: os deploy …(os compile, whichos devruns on every rebuild, does not); bareos/os --helpprints anEVERYDAY COMMANDSblock (dev, build, deploy, cloud login, validate) from a help class (src/help.ts,oclif.helpClass).The wait — the served-version contract (cloud#3651, declared in cloud#3662)
One client function,
waitUntilServed, with the shape confined tofetchServedReport(request) andinterpretServedReport(reading), as relayed by the seat:GET /api/v1/cloud/environments/{environmentId}/packages/{manifestId}with the publish credential →data.served{ state: live|pending|failed, desiredVersionId, versionId, version, observedStatus, observedAt, error }. ⛔ NotGET …/installations/:packageId(old shape, noserved) — a test asserts the wait never calls it.live→ exit 0 (observedStatus: incompleteadds a plan warning);failed→ printserror, exit 1;pending→ keep polling.desiredVersionId≠ the publish answer'sid→ another install replaced this deploy: said so, exit 1, never "Live".GET {environment origin}/api/v1/discovery(origin fromdata.environment.hostnameonGET /api/v1/cloud/environments/{id};/api/v1/discoveryis kernel-served, unlike/api/v1/health). The answer (a retryable 503SERVICE_UNAVAILABLEwhile it builds) is ignored. No hostname → a warning that the rebuild waits for real traffic, and the wait continues.--wait-timeoutis 600 s, and the timeout message says a failing build can read as still building for minutes.served(a control plane before flaky: lazy-deps 的两条 dist 探针用例在整仓并发下撞 5s 默认超时(兄弟用例已设 30s) #3662) → says it cannot confirm liveness, exit 0; never spins to the timeout, never prints "Live".version+install_env_idin the samePOST …/versions), so feat(analytics,spec): executeAggregate 桥携带 ExecutionContext —— ADR-0021 D-C 第二层带子 (#3602) #3651's new 409 for a version-less install of a pre-release-only package does not apply.Docs
content/docs/deployment/cli.mdx: newos deploysection, thedkey underos dev,os cloud loginkeeping the environment, and the publish--env/--installrows and install-target paragraph.Tests
All local readings are on a shared container; verdict lines quoted from the verify lock.
Unit and package suites
packages/cliunit tier,vitest run --project unit --maxWorkers=2, at4d010654: Test Files 1 failed | 392 passed (393), Tests 1 failed | 5477 passed (5478). The one red wastest/validate-build-gate-parity.test.ts("the command sources use no call shape this scan cannot read"):compile.tsimportsBUILD_NEXT_STEPas a value it never calls. Fixed in8df29def74by classifying it in that file's not-a-gate ledger (a fixed line, reads and judges nothing); the file then ran 33/33.7664160bmergedorigin/main(4 commits, none on apackages/cli,create-objectstackorcli.mdxpath); rebuilt what they touched in the CLI closure (spec,plugin-auth,service-automation,plugin-approvals) andcli— lock VERDICT command-exit 0. There:pnpm --filter create-objectstack test→ Test Files 24 passed (24), Tests 370 passed (370) (typecheckexit 0 at4d010654).b7ea5f21(the served-version contract wiring), lock VERDICT command-exit 0:pnpm --filter @objectstack/cli build→ exit 0;pnpm --filter @objectstack/cli typecheck→ exit 0 (check:test-typecheck: OK).package-publish-*file,publish-active-environment-store,init,validate-build-gate-parity,child-env-source-loader.pin) → Test Files 13 passed (13), Tests 210 passed (210).4d010654; what changed since is the gate-parity classification, the merge (no overlap) and the wait wiring, each covered by the targeted files above. NOT MEASURED atb7ea5f21: fullpackages/cliunit tier, reason: shared lock queue (~10–25 min per acquisition); CI'sTest Coreruns it.integrationtier ofpackages/cliis declared to CI: the diff touches no integration-tier file, nobin/entry and notest/helpers/serve-process.ts.What each acceptance line is pinned by
publish-active-environment-store.test.ts› "installs into the only environment and says which, skipping archived and system rows"os cloud loginkeepsactiveEnvironmentIdcloud-login-keeps-active-environment.test.ts(same plane + account kept; other plane / other account dropped)manifest.versiondeploy.test.ts› "builds, publishesMANIFEST_VERSION-dev.UTC_TIMESTAMP…" and "deploys twice in a row … each time a new version, with no flag"deploy.test.ts(stubbed control plane: readsdata.served, wakes the environment, never the installed-state probe) andwaitUntilServed› "polls through pending, waking the environment before every poll, until it is live"deploy.test.ts› "prints the environment's build-failure reason and exits 1"; local build failure case toowaitUntilServed› "times out naming what it last saw, and reportVerdict names what it waited for and why failures read late"deploy.test.ts› "reports a deploy another install replaced …";interpretServedReportsuperseded caseos dev: deploy key runs the deploy path; no login → offers logindev-deploy-key.test.tspackage.jsonhasdeploycreate-objectstack/src/template-consistency.test.ts› "bundled templates ship a deploy script";test/init.test.ts› "app scaffold ships a deploy script"os buildnext step; bareoseveryday firstnext-steps.test.tsAblations — one per behaviour, through scripts/ablation-replace.mjs (WRAP mode), at b7ea5f2
Each mutation was proven on disk by the tool (anchor 1 → 0, blob changed), the named test then ran RED, and the restore was proven by the blob at
HEAD(git diff HEADandgit status --porcelainempty after all twelve). The tests importsrc/directly (nodist/on the resolution path), so no rebuild leg applies.install-target.ts:candidates.length === 1→=== -1publish.tsStep 0: refusal branch disabledlogin.tsmergeCloudLoginreturnsnextdev-version.tsreturns the bare core versionserved-version.ts: thestate === 'live'branch disabledserved-version.ts: thestate === 'failed'branch disabledserved-version.ts: the per-poll wake deletedserved-version.ts: the superseded check disableddev-deploy-key.ts:runDeploy()not calleddev-deploy-key.ts: login branch disabledcompile.ts: next-step print disabledhelp.ts:formatRootreturns oclif's aloneScaffold
package.jsonscripts are configuration, not ablated.Gates
Derived families, local, at b7ea5f2
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(treeobjectstack-ai/objectstackatb7ea5f210d, change set vs merge base): 112 commands, each run with its exit code captured before any pipe → 112 × exit 0.--ranreconciliation:112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED (a DERIVED zero — all 112 recorded an exit code and none of them is 3).The first run (at
08361b30) caught one real finding of this diff:check:system-context-censusread a summary field namedisSystemas a system-context elevation read. Renamed toinfrastructure(it mirrors the row'sis_system); census OK.Every family CI runs (Lint & Repo Gates, TypeScript Type Check, Test Core, …) is read from this PR's check-runs, not re-run locally.
Acceptance notes
create-objectstackscaffold with a fixedmanifest.version,npm run dev, pressd, edit a file, pressdagain. Both deploy with no flag, and the environment serves the second build (Live:namesVERSION-dev.SECOND_TIMESTAMP). The card body'sos package publish --dev --installstaging line is superseded by the amendment.os package publish --install: with no resolvable environment it now exits 1 before publishing anything (it used to print "Skipping auto-install", publish, and exit 0). Declared in the changeset.os package publishotherwise keeps its meaning: a release version,--installoptional, no wait.GET {origin}/api/v1/discovery, origin fromdata.environment.hostname(the seat confirmed it)./api/v1/healthis the container liveness probe and answers without a kernel.os dev's line says "your ObjectOS Cloud environment" rather than naming it: naming it would cost a control-plane call on every dev boot.os deploynames the environment when the key runs it.test/next-steps.test.tsrunsos buildin-process once (about 8.8 s of test time on this shared box, the first load of the compile path); it declares no budget. If CI's shard timings put it over 10 s, that is this lane's card.packages/cliintegration tier is declared to CI (no integration-tier file,bin/entry orserve-process.tstouched).domain:skillscard with the name and usage above.Verification
Generated by Claude Code