feat: support team workspace via zeabur workspace (PLA-1590)#244
Conversation
Adds a workspace concept to the CLI so team members can list, create, and deploy projects under a team they belong to — mirroring the dashboard's workspace switcher. Commands: zeabur workspace list list personal + all teams with role zeabur workspace current show the active workspace zeabur workspace switch <name|id> switch to a team workspace zeabur workspace clear return to personal workspace Plus a global `--workspace <name|id>` flag for one-shot overrides without touching the persisted state. Resolution rules: - 24-char hex → treated as a team ObjectID; must be in the caller's memberships - non-hex → name match against memberships; 0 matches errors, 2+ matches errors with the concrete `zeabur workspace switch <id>` invocation per candidate (team names are unconstrained and may collide) - `switch personal` is NOT a fast path back to personal; it always means "find a team literally named 'personal'". Use `workspace clear` instead. Switching workspaces clears the persisted project / environment / service context because resource IDs do not overlap between workspaces. The clear-on-switch is surfaced in every `switch` / `clear` output line. Only directory-level operations (`project list`, `project create`, `deploy` without a linked project) consult the workspace. Operations that take a specific service or deployment ID stay workspace-independent — the resource's own owner already locates the team, and backend RBAC gates writes. Threading: a `Factory.CurrentOwnerID()` helper folds the --workspace flag override (resolved during PersistentPreRunE) with the persisted workspace, and selector.New takes a closure so the same Selector instance reflects mid-process workspace switches. Lazy startup verify: PersistentPreRunE calls `teams` once and clears the persisted workspace if the caller is no longer a member (team deleted, caller removed). Best-effort: transport errors leave the workspace alone so an offline blip doesn't switch the user out silently. Depends on backend PLA-1589 (Team.myRole field) for the per-team role shown in `workspace list` and in disambiguation errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds workspace model and ephemeral context, Team API and team models, owner-scoped project/service APIs, Factory workspace override and EffectiveContext, ChangesWorkspace Management & Owner Scoping
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/cmd/deploy/deploy.go (1)
197-203:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStop the spinner on the
ListAllProjectserror path.Line 201 returns before
s.Stop(), so a failed fetch can leave the spinner active in the terminal.Suggested fix
s.Start() ownerID := f.CurrentOwnerID() projects, err := f.ApiClient.ListAllProjects(context.Background(), ownerID) +s.Stop() if err != nil { return nil, nil, err } -s.Stop()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cmd/deploy/deploy.go` around lines 197 - 203, After calling s.Start() in deploy.go, ensure the spinner is stopped on all code paths: either add defer s.Stop() immediately after s.Start() or explicitly call s.Stop() before returning on the error path from f.ApiClient.ListAllProjects(context.Background(), ownerID); update the block around s.Start(), ownerID := f.CurrentOwnerID(), and the ListAllProjects error branch so the spinner is always stopped when ListAllProjects returns an error.
🧹 Nitpick comments (1)
internal/cmdutil/workspace.go (1)
19-22: ⚡ Quick winAlign resolver comments with actual membership enforcement.
The comment says the flag path trusts raw ID even when not in memberships, but the implementation rejects non-member IDs. Please update the comment to match behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cmdutil/workspace.go` around lines 19 - 22, Update the comment in the workspace resolver to reflect actual behavior: instead of saying the flag path trusts a 24‑char hex team ID even if the caller is not a member, document that the implementation enforces membership (non-members are rejected) and that the flag path only accepts an ID if the caller is a member; adjust the note around the team resolution logic (the comment block describing hex vs non-hex handling) to state that membership is verified for both paths and backend RBAC still applies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cmdutil/workspace_test.go`:
- Line 1: Change the test package from package cmdutil to package cmdutil_test
and update all test references to exported symbols by prefixing them with
cmdutil. (For example, replace ResolveWorkspaceArg(...) calls with
cmdutil.ResolveWorkspaceArg(...)). For TestIsObjectIDHex, stop calling the
unexported isObjectIDHex directly—either assert its behavior indirectly via
cmdutil.ResolveWorkspaceArg (or another exported API that uses isObjectIDHex)
or, if your project policy permits, add a very narrow linter exception for this
single test; ensure all updated tests import the cmdutil package and compile
against exported APIs only.
In `@internal/cmdutil/workspace.go`:
- Around line 42-45: The comparison of ObjectIDs is case-sensitive:
isObjectIDHex accepts uppercase hex but the lookup compares teams[i].ID == arg
and may fail for uppercase input; normalize both sides (e.g., lower-case arg and
teams[i].ID or use strings.EqualFold) before comparing so ID resolution is
case-insensitive — update the code in the block that uses isObjectIDHex and the
teams slice lookup to perform a case-insensitive comparison (reference:
isObjectIDHex, teams[i].ID, arg).
In `@pkg/api/project.go`:
- Line 53: The call to c.ListProjects uses context.Background(), which drops the
caller's cancellation/deadline; change it to propagate the caller context by
passing the incoming context (e.g., ctx) instead of context.Background() to
c.ListProjects (or, if this function lacks a context parameter, add a
context.Context parameter to the enclosing function and thread it through), so
the call to ListProjects (and the resulting projectCon/err handling) respects
cancellation and deadlines.
In `@README.md`:
- Line 129: The README line for the workspace switch example currently implies
24-char hex IDs are only used when names conflict; update the text around the
command `npx zeabur workspace switch` to state that a 24‑character hex string is
always interpreted as a team ID (so users may pass either a team name or a
24‑char ID), and add a short note clarifying that if multiple teams share the
same name the CLI will error and recommend using the team ID; update the
single-line example and its parenthetical/notes accordingly.
---
Outside diff comments:
In `@internal/cmd/deploy/deploy.go`:
- Around line 197-203: After calling s.Start() in deploy.go, ensure the spinner
is stopped on all code paths: either add defer s.Stop() immediately after
s.Start() or explicitly call s.Stop() before returning on the error path from
f.ApiClient.ListAllProjects(context.Background(), ownerID); update the block
around s.Start(), ownerID := f.CurrentOwnerID(), and the ListAllProjects error
branch so the spinner is always stopped when ListAllProjects returns an error.
---
Nitpick comments:
In `@internal/cmdutil/workspace.go`:
- Around line 19-22: Update the comment in the workspace resolver to reflect
actual behavior: instead of saying the flag path trusts a 24‑char hex team ID
even if the caller is not a member, document that the implementation enforces
membership (non-members are rejected) and that the flag path only accepts an ID
if the caller is a member; adjust the note around the team resolution logic (the
comment block describing hex vs non-hex handling) to state that membership is
verified for both paths and backend RBAC still applies.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1565b3d6-301a-4acf-8ece-acc6c81f4daf
📒 Files selected for processing (22)
README.mdinternal/cmd/deploy/deploy.gointernal/cmd/project/create/create.gointernal/cmd/project/list/list.gointernal/cmd/root/root.gointernal/cmd/template/deploy/deploy.gointernal/cmd/workspace/clear/clear.gointernal/cmd/workspace/current/current.gointernal/cmd/workspace/list/list.gointernal/cmd/workspace/switch/switch.gointernal/cmd/workspace/workspace.gointernal/cmdutil/factory.gointernal/cmdutil/workspace.gointernal/cmdutil/workspace_test.gopkg/api/interface.gopkg/api/project.gopkg/api/team.gopkg/model/team.gopkg/selector/selector.gopkg/zcontext/context.gopkg/zcontext/interface.gopkg/zcontext/workspace.go
Panel Review — zeabur/cli PR #244 + zeabur/backend PR #21311. What problem does this PR solve?Team members cannot use the CLI to operate on team-owned projects — 2. How does it solve it?Backend #2131: Adds a nullable CLI #244: Adds 3. What are the tradeoffs?
4. VerdictConsensus: request changes 🔴 SUGGESTED CHANGES
⚖️ Unresolved disagreements
🟡 NIT
🟢 INFO
|
Panel review: - F1 (deploy hint vs. flag override): The "Creating new project in team workspace X" banner read from the persisted workspace, but the actual create call used CurrentOwnerID, which honors the --workspace flag override. With `--workspace team-b` overriding a persisted team-a the banner named the wrong team; overriding personal printed nothing. Snapshot a single CurrentWorkspace() up front and use it for both the banner and the ownerID — they now resolve from the same source. - F2 (ListTeams fanout): A single CLI invocation could hit `teams` up to three times (flag resolution, lazy verify, the command itself). Memoize the reply on Factory.ListTeams; resolveWorkspaceFlag, verifyPersistedWorkspace, and the workspace list / current / switch commands now all share one fetch with sticky-error caching. - N3 (current.go silent error): `workspace current` swallowed ListTeams errors and just omitted the role. Log at debug so it isn't completely silent without making the command fail. CodeRabbit: - workspace.go: factor invocationName() out of the ambiguous-name error so users invoking via `npx zeabur` (or any wrapper) get a copy-pasteable command in the error, not a hardcoded "zeabur". Normalize hex comparison to lower-case so an ID pasted in uppercase still resolves (isObjectIDHex already accepts both). - workspace.go (comment): Rewrite the ID-vs-name docstring to match behavior — both paths enforce membership; the hex path doesn't "trust" raw input as the old comment implied. Backend RBAC is still the authoritative gate. - deploy.go: spinner left running when ListAllProjects errors — s.Stop() happens before the err check so it runs on every path. - pkg/api/project.go: ListAllProjects' inner loop passed context.Background() to each page request, dropping the caller's cancellation. Propagate ctx. - workspace_test.go: convert to the `cmdutil_test` external package so the tests only exercise exported APIs. The previous direct test of the unexported isObjectIDHex is replaced by TestResolveWorkspaceArg_NonHex_NotMistakenForID (24-char non-hex must fall into the name branch) and TestResolveWorkspaceArg_ ByID_CaseInsensitive (uppercase hex must still resolve), which cover the same predicate via the public surface. - README: clarify that a 24-char hex value is always interpreted as an ID, not "only when the name is not unique". Names go to the name branch regardless; duplicates trip the ambiguous-name error. Factory: SetWorkspaceOverride now takes `*zcontext.Workspace` instead of a bare ID so CurrentWorkspace() can return the resolved name to display sites (the deploy hint). Added CurrentWorkspace() helper alongside CurrentOwnerID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
review 反馈已处理(commit Panel — Suggested changes (F)
Panel — Nits
CodeRabbit
附带: 全部测试通过(8 个 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cmdutil/workspace_test.go`:
- Line 50: The membership-error assertion uses a negated OR which triggers
staticcheck QF1001; change the condition in the test (where err is checked) from
"if err == nil || !(strings.Contains(err.Error(), \"not a team\") ||
strings.Contains(err.Error(), \"no team\"))" to the equivalent non-negated form
using AND of negations: check "if err == nil || (!strings.Contains(err.Error(),
\"not a team\") && !strings.Contains(err.Error(), \"no team\"))" so behavior
stays the same but avoids the negated OR pattern.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 31178dfc-a73c-458e-a460-18aa3ab44e26
📒 Files selected for processing (10)
README.mdinternal/cmd/deploy/deploy.gointernal/cmd/root/root.gointernal/cmd/workspace/current/current.gointernal/cmd/workspace/list/list.gointernal/cmd/workspace/switch/switch.gointernal/cmdutil/factory.gointernal/cmdutil/workspace.gointernal/cmdutil/workspace_test.gopkg/api/project.go
✅ Files skipped from review due to trivial changes (1)
- README.md
Panel Review — zeabur/cli PR #244 + zeabur/backend PR #2131 (Fix Round)1. What problem does this PR solve?Team members cannot use the CLI to operate on team-owned projects. The CLI needs a workspace concept (mirroring the dashboard) so users can switch between personal and team contexts. 2. How does it solve it?Backend #2131: Adds a nullable CLI #244: Adds 3. What are the tradeoffs?
4. VerdictConsensus: approve (pending trivial CI lint fix) 🔴 SUGGESTED CHANGES
⚖️ Unresolved disagreements
🟡 NIT
🟢 INFO
|
Codex flagged `context set project --name` falling back to a personal-
account lookup when the active workspace is a team. The same pattern
lives in every CLI command that resolves a project / service by name —
they all route through util.GetProjectByName / util.GetServiceByName,
which under the hood key on the caller's personal username via the
backend `project(owner, name)` / `service(owner, projectName, name)`
queries.
So in a team workspace, today, `--name` will either:
- silently miss a team-only project / service ("not found"), or
- worse, find a same-named personal one and pin to it — the next
command would then deploy / delete / restart under the wrong owner.
Fix at the choke point: route the name path through the workspace-aware
helpers. Personal path (ownerID == "") preserves the original
`project(owner, name)` / `service(owner, projectName, name)` query
exactly as before — no behavior change for the non-team caller. Team
path walks `ListAllProjects(ownerID)` / `ListAllServices(projectID)`
and filters by name locally. Both helpers' signatures grow new owner /
project arguments; ~20 call sites updated mechanically. ID-based paths
(`--id`) are workspace-agnostic and stay on the ApiClient.GetProject /
GetService direct queries.
Service lookups in a team workspace require a pinned project context
because services are project-scoped and a service name is only unique
within a project — the helper surfaces this with an actionable error
pointing at `zeabur context set project --id` rather than silently
falling through to the personal account.
Tests cover both paths plus the failure-propagation invariant (a team
list error must not silently fall through to a personal lookup):
TestGetProjectByName_Personal
TestGetProjectByName_TeamFound
TestGetProjectByName_TeamNotFound
TestGetProjectByName_TeamListErr
TestGetServiceByName_Personal
TestGetServiceByName_TeamFound
TestGetServiceByName_TeamWithoutProjectContext
TestGetServiceByName_TeamNotFound
TestGetServiceByName_TeamListErr
All pass; vet clean; full test suite clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cmd/context/set/set.go`:
- Around line 120-134: When resolving a project by ID (using
f.ApiClient.GetProject) the local variable name may remain empty causing success
logs to print blank names; after successfully obtaining project (the variable
project) in the ID branch assign name = project.Name so logs downstream use the
resolved name. Apply the same fix in the other occurrence where project is
fetched by ID (the block around the util.GetProjectByName /
f.ApiClient.GetProject pairs) so both code paths always set name from
project.Name when project != nil and err == nil.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0d356dbc-c440-4a32-aa32-e992aaa0e22f
📒 Files selected for processing (31)
internal/cmd/context/set/set.gointernal/cmd/deployment/get/get.gointernal/cmd/deployment/list/list.gointernal/cmd/deployment/log/log.gointernal/cmd/domain/create/create.gointernal/cmd/domain/delete/delete.gointernal/cmd/domain/list/list.gointernal/cmd/project/clone/clone.gointernal/cmd/project/delete/delete.gointernal/cmd/project/export/export.gointernal/cmd/project/get/get.gointernal/cmd/service/delete/delete.gointernal/cmd/service/exec/exec.gointernal/cmd/service/get/get.gointernal/cmd/service/instruction/instruction.gointernal/cmd/service/metric/metric.gointernal/cmd/service/network/network.gointernal/cmd/service/port-forward/port_forward.gointernal/cmd/service/redeploy/redeploy.gointernal/cmd/service/restart/restart.gointernal/cmd/service/suspend/suspend.gointernal/cmd/service/update/tag/tag.gointernal/cmd/variable/create/create.gointernal/cmd/variable/delete/delete.gointernal/cmd/variable/env/env.gointernal/cmd/variable/list/list.gointernal/cmd/variable/update/update.gointernal/util/project.gointernal/util/project_test.gointernal/util/service.gointernal/util/service_test.go
CI lint: - workspace_test.go:50 — staticcheck QF1001 (De Morgan): rewrite the membership-error assertion from `!(X || Y)` to `(!X && !Y)`. Same truth table, no longer trips the negated-OR rule. Verified locally with `staticcheck -checks QF1001`. CodeRabbit on the F2 commit: - set.go setProject / setService — when the user passed only `--id`, the local `name` variable stayed empty and the success log read "Project context is set to <>". Backfill `name` from the resolved `project.Name` / `service.Name` after the lookup succeeds, so both ID-only and name-only invocations log the resolved name. Backward-compat regression coverage (per Bruce's red-line review of the util signature changes) — Factory now has dedicated tests: - TestFactory_PersonalUserInvariant — a zero Factory (the brand-new user shape) reports CurrentOwnerID() == "" and IsPersonal(). This is the single most important invariant of PLA-1590: every owner-aware util helper checks for the empty string before deciding between the legacy personal query path and the new team-aware one. A regression here silently routes personal users through the team branch. - TestFactory_CurrentOwnerID_PersistedPersonal — Config present but no persisted workspace must still report personal. - TestFactory_CurrentOwnerID_PersistedTeam — sanity on the persisted team path. - TestFactory_CurrentOwnerID_OverrideBeatsPersisted — `--workspace` override takes precedence and does NOT leak into the persisted file. - TestFactory_CurrentOwnerID_OverrideNilClears — passing nil to SetWorkspaceOverride drops the override. - TestFactory_ListTeams_Memoizes — one process, one backend hit, no matter how many sites ask. Guards the F2 fanout fix. - TestFactory_ListTeams_StickyError — a failed fetch caches the error so subsequent callers don't retry against an already-broken backend. All tests pass; full vet + suite clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round 3 fixes(commit CI lint blocker (Panel F1 / CodeRabbit)
CodeRabbit on
Bruce 要求的"全路径向后兼容测试" — 新加 7 个 Factory 测试
加上之前的 9 个
🤖 Generated with Claude Code |
Both commands read the persisted workspace directly via `f.Config.GetContext().GetWorkspace()`, so a `--workspace foo` override was silently ignored — `workspace current` reported the persisted workspace and `workspace list` put `*` on the persisted row, not on the override. Caught running the Bucket 6 dev-2 E2E: $ /tmp/zeabur-dev2 workspace switch team-A $ /tmp/zeabur-dev2 --workspace team-B workspace current team-A [...] team ... # wrong — override was team-B This is the same class of bug as the deploy.go F1 finding from the panel review: display source must match the resolved source the rest of the command sees. Switch both commands to `f.CurrentWorkspace()` / `f.CurrentOwnerID()` so they reflect the effective workspace for the invocation. Existing `TestFactory_CurrentOwnerID_OverrideBeatsPersisted` covers the underlying helper; this fix is the consumer side. Verified on dev-2 against api-2.zeabur.dev: $ /tmp/zeabur-dev2 --workspace pla1230 workspace current pla1230-probe-... [69e72943...] team Administrator # ✓ $ /tmp/zeabur-dev2 --workspace pla1230 workspace list * 69e72943... pla1230-probe-... team Administrator # ✓ marker on override Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dev-2 E2E 测试报告部署 backend 测试 setup
Bucket 矩阵
关键证据1. F2 fix(Codex round 2 的核心问题)verified end-to-end跟旧版(PR 2. Project create owner verification确认 ownerID 写到了 team,不是 personal。 3. Lazy verify测试中修复的 2 个 bug(commit
|
| 文件 | Bug | Fix |
|---|---|---|
workspace/current/current.go |
读 f.Config.GetContext().GetWorkspace() 而非 f.CurrentWorkspace(),--workspace flag override 被忽略 |
改用 f.CurrentWorkspace() |
workspace/list/list.go |
同上 —— * marker 标 persisted 而非 override |
改用 f.CurrentOwnerID() |
重现(修复前 b97244f)
$ /tmp/zeabur-dev2 workspace switch team-A
$ /tmp/zeabur-dev2 --workspace team-B workspace current
team-A [...] team ... # 错 —— override 应该是 team-B
修复后(736851b)
$ /tmp/zeabur-dev2 --workspace pla1230 workspace current
pla1230-probe-1776760608745 [69e72943b0f22737a455dded] team Administrator # ✓
$ /tmp/zeabur-dev2 --workspace pla1230 workspace list
* 69e72943... pla1230-probe-... team Administrator # ✓ marker 在 override
这跟 panel round 1 的 F1(deploy hint)是同一类型 bug —— display source 必须跟 ownerID resolved source 一致。已有 TestFactory_CurrentOwnerID_OverrideBeatsPersisted 测试守 helper 侧,但 consumer 侧(workspace current / list)漏接。
回归保护
24 个 unit test 已经覆盖 helper / Factory / util 全路径;这次 dev-2 跑出来的 bug 是 consumer 侧而非 helper 侧,后续 follow-up 可以加 command-level integration test(cobra runE mock),不阻塞本 PR。
Backend deploy 配套
backend PR #2131 dev-2 deploy 成功 —— schema probe 显示 Team.myRole 字段已暴露,role 值与 team_members 一致。Backend 侧零 bug 发现。
测试收尾
- Binary、隔离 config、临时文件全部 cleanup
pkg/constant/const.gorevert 回api.zeabur.com- CLI repo 干净,HEAD =
736851b
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cmd/workspace/list/list.go`:
- Around line 34-38: The code uses currentID (set via f.CurrentOwnerID()) both
as the effective owner for display markers and for checking/presenting the
persisted-workspace warning; separate these concerns by renaming the existing
currentID to effectiveOwnerID (use f.CurrentOwnerID() for marker logic) and
introduce a persistedOwnerID (call the API that returns the persisted owner
ID—e.g., f.PersistedOwnerID() or the equivalent persisted-state accessor) for
the warning path so the warning reflects the saved workspace rather than any
--workspace override; update all references accordingly (marker logic ->
effectiveOwnerID, warning checks -> persistedOwnerID).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8381b9e2-8a04-4b1c-b129-865a85fe2226
📒 Files selected for processing (2)
internal/cmd/workspace/current/current.gointernal/cmd/workspace/list/list.go
Dev-2 E2E Round 2 测试报告完整跑了上一轮 Codex 提的 2 条 smoke + Bruce 加的 3 条 RBAC/free-team 测试。Backend 用 main-merged 版本(含 测试矩阵
关键证据Codex 1:service by-name in team workspace20+ caller 共用一个 helper —— helper 端有专门测试守住 personal/team 分支,consumer 端这次端到端确认通过。 Codex 2:
|
…1590) Codex's round-4 review surfaced a real cross-workspace contamination bug that the previous test coverage happened to walk around. Path 1 (read stale context under override): $ zeabur workspace switch team-A $ zeabur context set project --id <team-A-foo> $ zeabur --workspace team-B service delete --name redis --yes The team branch of util.GetServiceByName trusted the persisted project ID, so the delete landed on team-A's redis even though the user typed --workspace team-B. Same risk on every service / variable / deployment / domain consumer that resolves by name. Path 2 (write persisted context under override): $ zeabur workspace switch team-A $ zeabur --workspace team-B project create --name x --region y # non-JSON The non-JSON branch of project create called setProject, pinning the team-B project under persisted workspace team-A. Subsequent commands without --workspace then used the wrong combination. Both paths are regressions introduced by PLA-1590 — pre-PR, name lookups always went through the personal `service(owner=username, ...)` query which simply couldn't find team-owned projects, so the cross-team risk was structurally impossible. The team branch added by F2 is what opened the door. The fix codifies `--workspace` as a stateless one-shot override: 1. Factory.HasWorkspaceOverride() — predicate. Every override-gated behaviour goes through this. 2. Factory.CurrentProjectID() / CurrentProjectName() / CurrentServiceID() / CurrentEnvironmentID() — return "" when an override is active, so name-based downstream lookups fail-closed with the existing "cannot resolve service by name in a team workspace without a project context" error. The non-override path is byte-equivalent to reading the persisted context directly, so vanilla users see no change. 3. project create + deploy interactive create / select paths skip the setProject write when an override is active, and log a one-line hint telling the user how to make it the default (`workspace switch ...`). 4. context set rejects up front when an override is active — writing persistent state under a one-shot override is incoherent. The error tells the user to run `workspace switch` first. All 24 service/domain/variable/deployment consumers route through the new Factory helpers instead of reading the persisted project context directly, so there's one chokepoint, not 24 places to keep in sync. Tests: TestFactory_HasWorkspaceOverride TestFactory_CurrentInnerContext_OverrideHides TestFactory_CurrentInnerContext_NilConfigSafe Plus dev-2 E2E (this round): - C1: persisted=pla1230 + pinned for-clone-test, `--workspace pla1277-presub service delete --name redis --yes` → "cannot resolve service by name..." error; for-clone-test redis NOT deleted (verified via service list). - C2: persisted=pla1230, `--workspace pla1230 project create --name pla1590-bplus-c2-test --region ...` (non-JSON) → succeeds, prints the one-shot hint, persisted context.project still for-clone-test. - C3: `--workspace ... context set project ...` → rejected with actionable hint pointing at `workspace switch`. - C5: no override → `context set service --name redis` still pins correctly (back-compat). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
采纳 Codex 的 B+ 设计,commit 改动总览
Unit test(新增 3 条)加上前 19 条 → 共 22 个 unit test,覆盖 Factory + util + workspace helper 全路径。 Dev-2 E2E(本轮新跑)
关于 Codex 推荐 vs 我原推荐 A承认我对方案 A 的 cost 估错了 —— Project model 没 ownerID 字段( B+ 的语义更干净( A("给 context 加 workspace tag"的长期模型)作为 follow-up 单开 issue。 现状
🤖 Generated with Claude Code |
… context (PLA-1590) Codex's round-5 review showed B+ (commit b5667e2) only covered half the surface: my new helpers (CurrentProjectID etc.) plus a few skip-write guards in project create / deploy. Two whole entry classes still walked straight past them. Path 1 — interactive ParamFiller bypass: $ zeabur workspace switch team-A $ zeabur context set project --id <team-A-project> $ zeabur --workspace team-B service restart # interactive (default) `runRestartInteractive` passed the raw `f.Config.GetContext()` to ParamFiller.ServiceByNameWithEnvironment. The filler read team-A's pinned project ID as `projectCtx.GetProject().GetID()`, used it as the project scope for SelectService, and quietly restarted team-A's service while the user thought they were operating on team-B. Worse, when the persisted context was empty the filler called `projectCtx.SetProject(picked)` and *wrote* the team-B project under the persisted team-A workspace — cross-team contamination that survived the command. Same shape across service/{restart,delete,suspend,exec,network,port-forward,redeploy, instruction,metric,get,update/tag}, variable/{create,delete,env,list,update}, deployment/{get,log,list}, domain/{create,delete,list}. Path 2 — `project get/delete` PreRunE auto-fill: $ zeabur --workspace team-B project delete --yes -i=false # no --id `util.DefaultIDNameByContext(f.Config.GetContext().GetProject(), ...)` was bound at Cobra command-construction time and unconditionally copied team-A's persisted project ID into opts.id. The runE then deleted team-A's project, even though every flag on the line said team-B. Fix: ephemeral context + EffectiveContext audit. 1. New `zcontext.NewEphemeralContext(workspace)` — an in-memory Context implementation that starts empty, writes to memory only, and reports the supplied workspace via GetWorkspace() (so consumers that ask "what workspace is this context for?" get the override answer rather than personal — a second-order trap Codex flagged). 2. New `Factory.EffectiveContext() zcontext.Context`: - Without override: returns the persisted config context, byte- equivalent to today. - Under override: lazy-initialises a per-Factory ephemeral context and returns it for every subsequent call (so ParamFiller's `Set → later Get` cycle still works in-process; nothing leaks to disk). 3. 24 interactive callers swap `f.Config.GetContext()` for `f.EffectiveContext()`: every service/, variable/, deployment/, domain/ command that ran ParamFiller. 4. project get/delete PreRunE swap to a lazy closure so EffectiveContext is resolved at PreRunE time (after PersistentPreRunE parses `--workspace`), not at command construction. The signature of util.DefaultIDNameByContext changes from `BasicInfo` to `func() BasicInfo` to make that lazy evaluation explicit. 5. `context get` reads EffectiveContext and, under override, prints an extra "Note: --workspace is one-shot; persisted ... is not used" line for human-readable output. JSON output stays structurally identical so scripts keep parsing. 6. `context clear` rejects under override — clearing belongs to the persisted state, not a one-shot override. 7. Deleted internal/util.NeedProjectContextWhenNonInteractive and DefaultIDByContext — both had zero callers and would re-introduce the same pattern if revived from copy/paste later. What stays on `f.Config.GetContext()` (intentional): workspace/{switch,clear}, auth/logout, root.go's lazy workspace verify, context/{set,clear} command bodies (set already rejects override; clear now does too), project create / deploy interactive flows (already wrapped in `if !HasWorkspaceOverride` from b5667e2 with a user-visible hint). Tests: zcontext/ephemeral_test.go (new): TestEphemeralContext_WorkspaceFromConstructor TestEphemeralContext_NilWorkspaceIsPersonal TestEphemeralContext_ReadEmptyByDefault TestEphemeralContext_SetReadCycleWorksInMemory TestEphemeralContext_ClearAll cmdutil/factory_test.go (added): TestFactory_EffectiveContext_NoOverride TestFactory_EffectiveContext_OverrideReturnsEphemeral_WithWorkspace TestFactory_EffectiveContext_OverrideCachedWithinCommand TestFactory_EffectiveContext_OverridePersistedUnpolluted Dev-2 E2E (this round): C1: --workspace team-B service delete --name redis (interactive, persisted=team-A + pinned project) → opens Select project from team-B; for-clone-test redis NOT deleted (verified via list). C3: --workspace team-B project delete --yes -i=false (no --id) → "please specify project by --name or --id"; for-clone-test project NOT deleted. C4: --workspace team-B context get → all inner context shows "<not set>" + the Note line; --json stays structurally clean. C5: --workspace team-B context clear → rejected with actionable hint pointing at workspace switch. C6: no override → context get reads persisted normally (back-compat red line). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
B++ 落地,commit 改动总览
保留 persisted 读写的位置(明确)按你的"按读写语义审计"要求:
新加测试Dev-2 E2E按你列的 7 条 + back-compat 跑完: C2/C7 在前一轮 b5667e2 已验过:
现状
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cmdutil/factory.go`:
- Around line 192-195: EffectiveContext() currently caches f.ephemeralCtx and
never refreshes it, so if SetWorkspaceOverride(...) changes the workspace within
the same invocation the cached context remains bound to the old workspace;
update EffectiveContext to detect when the current workspace
(f.CurrentWorkspace()) differs from the workspace inside f.ephemeralCtx and
recreate f.ephemeralCtx via zcontext.NewEphemeralContext(...) when they differ
(or when ephemeralCtx is nil), ensuring the cached ephemeralCtx always reflects
the latest workspace override.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8f8881cf-c9fc-49df-ac61-aee79a65b492
📒 Files selected for processing (31)
internal/cmd/context/clear/clear.gointernal/cmd/context/get/get.gointernal/cmd/deployment/get/get.gointernal/cmd/deployment/list/list.gointernal/cmd/deployment/log/log.gointernal/cmd/domain/create/create.gointernal/cmd/domain/delete/delete.gointernal/cmd/domain/list/list.gointernal/cmd/project/delete/delete.gointernal/cmd/project/get/get.gointernal/cmd/service/delete/delete.gointernal/cmd/service/exec/exec.gointernal/cmd/service/get/get.gointernal/cmd/service/instruction/instruction.gointernal/cmd/service/metric/metric.gointernal/cmd/service/network/network.gointernal/cmd/service/port-forward/port_forward.gointernal/cmd/service/redeploy/redeploy.gointernal/cmd/service/restart/restart.gointernal/cmd/service/suspend/suspend.gointernal/cmd/service/update/tag/tag.gointernal/cmd/variable/create/create.gointernal/cmd/variable/delete/delete.gointernal/cmd/variable/env/env.gointernal/cmd/variable/list/list.gointernal/cmd/variable/update/update.gointernal/cmdutil/factory.gointernal/cmdutil/factory_test.gointernal/util/runE.gopkg/zcontext/ephemeral.gopkg/zcontext/ephemeral_test.go
Codex's round-5 review noted that the `project get/delete` PreRunE fix from 7a1be0e was only proven by E2E — no committed regression test guarded the lazy-closure behaviour that makes the fix work. This adds three unit tests against `util.DefaultIDNameByContext`: TestDefaultIDNameByContext_LazyEvaluation Source BasicInfo is swapped AFTER the helper is constructed but BEFORE PreRunE runs. Eager capture (the pre-fix behaviour) would leak the old value; lazy resolution must pick up the new one. This is the exact mechanism that lets PersistentPreRunE parse `--workspace` and then have project get/delete see the override. TestDefaultIDNameByContext_EmptyBasicInfoSkipsFill The override path: EffectiveContext returns an empty BasicInfo, so no auto-fill happens and the caller's runE produces the "please specify project by --name or --id" actionable error rather than silently using the persisted team-A project. TestDefaultIDNameByContext_RespectsUserFlags Back-compat: explicit --id / --name from the user is never overwritten by the context default. Unchanged from pre-PLA-1590 but worth pinning down so a future refactor doesn't regress it. No production code change in this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
补完你提的两个 nit。Commit C7 实际跑了(之前漏的)Setup: persisted = pla1230 (Admin) + pinned ParamFiller 在 override 下走进 ephemeral,没用 persisted 的 for-clone-test/redis。两条共用同一个
|
| 类 | 文件 | 数量 |
|---|---|---|
| Unit | pkg/zcontext/ephemeral_test.go |
5 |
| Unit | internal/cmdutil/factory_test.go |
11 |
| Unit | internal/util/runE_test.go(新) |
3+3=5 sub |
| Unit | internal/util/project_test.go |
4 |
| Unit | internal/util/service_test.go |
5 |
| Unit | internal/cmdutil/workspace_test.go |
8 |
| E2E | dev-2 — C1/C2/C3/C4/C5/C6/C7a/C7b + RBAC.A/B/C + free-team + helper 矩阵 | 跨 4 轮 |
🤖 Generated with Claude Code
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/util/runE_test.go`:
- Line 21: The test declares source with an explicit type causing staticcheck
ST1023; remove the redundant type by changing the declaration of source to use
type inference (e.g., use short variable declaration for symbol source assigned
from zcontext.NewBasicInfo("old-id", "old-name") or use var source =
zcontext.NewBasicInfo(...)); update the declaration so it no longer explicitly
names zcontext.BasicInfo.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3eb1d2af-18d3-4269-b56d-04f261baafb8
📒 Files selected for processing (1)
internal/util/runE_test.go
…am mode (PLA-1590)
Codex's round-6 review surfaced the last residual cross-workspace
contamination path that the B+ / B++ work missed: `context set project
--id <other-team-project>` while in a team workspace.
Repro:
$ zeabur workspace switch team-A
$ zeabur context set project --id <team-B-project-id> # SUCCEEDS today
$ zeabur service delete --name redis --yes -i=false # deletes team-B's redis
The team branch of util.GetServiceByName then resolves under team-A
ownerID + team-B projectID and hands the resulting service-ID to the
destructive call. Backend RBAC doesn't catch it (caller is a member of
both teams), so the user — believing they're in team-A — silently
mutates team-B resources.
Pre-fix `setProject` simply called `f.ApiClient.GetProject(id, "",
"")` (owner-agnostic) and pinned whatever came back. The comment even
called it out as "ID path is workspace-agnostic" — accurate description
of the bug.
The fix is the minimal guard Codex recommended:
- ID path in a team workspace: after fetching the project by ID, call
`ListAllProjects(currentOwnerID)` and verify the project is in the
team's set. Reject otherwise with an actionable error pointing at
`workspace switch` or `--name`.
- Personal workspace: unchanged. Collaborator workflows depend on
pinning by-ID a project owned by someone else, and that's still
fine because personal-pin doesn't have a "current workspace" the
pinned project could conflict with.
Implementation lives in `internal/cmd/context/set/set.go`. Adds one
extra round-trip per ID-only team-workspace pin (acceptable: only
fires on the explicit `context set` action, not on hot paths).
Tests:
TestSetProject_ID_TeamWorkspace_AllowsOwnProject
legitimate same-team pin succeeds + ListAllProjects called with
the correct ownerID.
TestSetProject_ID_TeamWorkspace_RejectsForeignProject
the exact Codex attack: cross-team --id rejected + context not
contaminated.
TestSetProject_ID_PersonalWorkspace_BypassesCheck
back-compat: personal --id NEVER calls ListAllProjects (else
collaborator pin-by-ID breaks).
TestSetProject_ID_TeamWorkspace_ListErr
backend list call failure propagates — must not silently
"trust the user" and re-open the gap when the API flakes.
All pass; full `go test ./...` + `go vet ./...` clean.
Dev-2 E2E (Codex's 4 verifications):
- F1.1 team-A + --id of a personal project Bruce has access to →
ERROR "project ... does not belong to workspace ..."; project
context still empty.
- F1.2 team-A + --id of a team-A own project (for-clone-test) →
pin succeeds.
- F1.3 personal workspace + --id of personal project (dev-test) →
back-compat: pin succeeds, no ListAllProjects call.
- F1.4 after F1.1's rejection, follow-up `service delete --name redis
-i=false` fails with "cannot resolve service by name ... no project
context", so the destructive call never reaches the other team's
redis. End-to-end verified: dev-test in personal is still present
(untouched).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
采纳,commit 修法(按你的最小修法建议)
project, err = f.ApiClient.GetProject(ctx, id, "", "")
if err != nil { return ... }
// Team workspace: verify the project actually belongs to this team
// via the owner-scoped ListAllProjects. Personal workspace stays
// unchanged to preserve collaborator pin-by-ID workflows.
if ownerID := f.CurrentOwnerID(); ownerID != "" {
teamProjects, listErr := f.ApiClient.ListAllProjects(ctx, ownerID)
if listErr != nil {
return fmt.Errorf("verify project workspace membership: %w", listErr)
}
belongs := false
for _, p := range teamProjects {
if p.ID == id { belongs = true; break }
}
if !belongs {
return fmt.Errorf(
"project %q does not belong to workspace %q; either run `zeabur workspace switch <team>` first, or pin by --name",
project.Name, f.CurrentWorkspace().Name,
)
}
}不动 personal 单测(
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/cmd/context/set/set_test.go (1)
1-1: 💤 Low valueConsider using
set_testpackage for black-box testing.The linter flags that the test package should be
set_testinstead ofset. However, since these tests directly call the unexportedsetProjectfunction (white-box testing), keeping the package assetis intentional and necessary. If this lint rule is enforced project-wide, you could either:
- Add a
//nolint:testpackagedirective- Refactor to test via the public
NewCmdSetinterfaceGiven the tests are validating internal security logic, white-box testing is reasonable here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cmd/context/set/set_test.go` at line 1, The tests intentionally use white-box testing by referencing the unexported setProject function, so change nothing functionally; either add a package-level linter suppression by placing "//nolint:testpackage" immediately above the "package set" declaration in internal/cmd/context/set/set_test.go to silence the testpackage lint, or refactor tests to exercise the public NewCmdSet command/interface instead of calling setProject directly (update test helpers to invoke NewCmdSet and assert behavior) — reference setProject and NewCmdSet when locating the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/cmd/context/set/set_test.go`:
- Line 1: The tests intentionally use white-box testing by referencing the
unexported setProject function, so change nothing functionally; either add a
package-level linter suppression by placing "//nolint:testpackage" immediately
above the "package set" declaration in internal/cmd/context/set/set_test.go to
silence the testpackage lint, or refactor tests to exercise the public NewCmdSet
command/interface instead of calling setProject directly (update test helpers to
invoke NewCmdSet and assert behavior) — reference setProject and NewCmdSet when
locating the code to change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f21669ff-7e8d-48bb-bebf-1378264d621d
📒 Files selected for processing (2)
internal/cmd/context/set/set.gointernal/cmd/context/set/set_test.go
…(PLA-1590)
Three CodeRabbit findings on the PR, two of which were CI lint blockers:
1. [CI BLOCKER] runE_test.go ST1023 — drop redundant explicit type on
`var source zcontext.BasicInfo = ...`; the type is inferred from
NewBasicInfo's return. staticcheck failed the lint job on it.
2. [CI BLOCKER] set_test.go testpackage — the test was `package set`
(internal/white-box) so it could call unexported setProject. Lint
requires `package set_test`. Reworked as black-box: drive the real
`context set project --id <id>` flow through set.NewCmdSet + Cobra
Execute, which is also a more faithful test of the actual entry
point. Same 4 cases (legit same-team pin / cross-team reject /
personal bypass / list-error propagation).
3. workspace/list/list.go — separate effective-owner from persisted
for the two concerns that were sharing one variable:
- `*` marker uses effectiveID (f.CurrentOwnerID) so a `--workspace`
override is reflected.
- stale-workspace warning uses persistedID (config workspace) so a
transient override can't suppress a warning about a genuinely
stale persisted workspace.
4. factory.go EffectiveContext — invalidate the cached ephemeral
context when the override workspace changes mid-process. Today only
resolveWorkspaceFlag sets the override (once per invocation), so this
is defensive, but it stops a future mid-run team switch from handing
back a stale in-memory project/service.
All checks pass locally: build, vet, full suite, and staticcheck
(the ST1023 + testpackage failures are gone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
让 Zeabur CLI 跟 dashboard 一样可以切到 team workspace —— 操作 team 拥有的 project / service / deployment。
Closes PLA-1590。
依赖:PLA-1589 (
Team.myRolebackend field — backend PR zeabur/backend#2131)。客户场景
Discord ives.0629 (2026-05-15):team 成员 alice 想用 CLI deploy team 的 project,但
zeabur project list永远只看到她个人的项目。表象是"team workspace 建的 API key 指向个人",真实诉求 = "让 CLI 能操作我所属 team 的 project"。取代 PLA-1542 / zeabur/backend#2115("给 API key 加 team scope"误判方向,详见两个 issue 的 cancellation 说明)。
新增命令
加 global
--workspace <name|id>flag,一次性 override 当前 workspace(不改 persistent state)。解析规则
switch personal不是回到个人的快捷方式 —— 它永远是去找名叫personal的 team(team 名字无限制)。回 personal 用workspace clear。--workspaceflag 用同样解析。Switch / clear 副作用
切换 workspace 会清 project / environment / service context(不同 workspace 的 resource ID 不重叠,留旧 context 必然 stale)。每次 switch / clear 输出都会明示清了什么。
哪些命令受 workspace 影响
只有"目录级":
project listproject createdeploy(没 link project)→ Creating new project in team workspace "xxx"提示)service list -p <pid>service deploy -s <sid>variable create ...deployment log ...绝大多数命令对 workspace 透明 —— 跟 dashboard 一致。
Startup lazy verify
每次进程启动调一次
teamsquery 验证 persisted workspace 还在不在 caller 的 memberships 里。被删 / 被踢出 → warning + auto fallback personal。Transport error 不动 workspace(offline 不应该静默切人)。兼容性
--workspaceflag 默认 empty(= personal,等同今天)ListProjects(ownerID, ...)/CreateProject(ownerID, ...)API 签名改了 —— 内部 caller 全部更新,外部没有人 import 这两个签名projects(ownerID)/createProject(ownerID)已经在 prod 跑了一段(team plan PLA-1160 系列),CLI 这边走的是已经存在的 schema 路径Team.myRole是 PLA-1589(zeabur/backend#2131)提供。一起 review、merge 后 deploy。本 PR 的 GraphQL queryteams { _id name myRole }在 backend 部署前会报字段不存在 —— prod 用户拿到这个 CLI 的时间一定晚于 backend deploy(CLI 发版需要打 v* tag),所以发版前 backend merge 即可。Tests
internal/cmdutil/workspace_test.go覆盖:isObjectIDHex各种 edge case全套
go test ./...通过。文件清单
新增:
pkg/zcontext/workspace.go— Workspace 结构 + Personal/Team 判断pkg/model/team.go— Team / TeamMemberRole modelspkg/api/team.go— ListTeams APIinternal/cmdutil/workspace.go—ResolveWorkspaceArg(核心解析逻辑,flag + switch 共用)internal/cmdutil/workspace_test.go— 7 个 unit testinternal/cmd/workspace/{workspace,list,current,switch,clear}/— 4 个子命令 + 父命令修改:
pkg/zcontext/{interface,context}.go— Context 接口加 Workspacepkg/api/{interface,project}.go— ProjectAPI 加 ownerID 参数pkg/selector/selector.go—New加 ownerIDFn 闭包internal/cmdutil/factory.go— Factory 加Workspaceflag +CurrentOwnerID()/SetWorkspaceOverride()internal/cmd/root/root.go— 注册 workspace 命令 +--workspaceglobal flag + PreRunE 里解析 flag + lazy verify + 用闭包构造 selectorproject list/project create/deploy/template deploy传 ownerID(deploy多加 team workspace 提示)Out of Scope(备查)
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need.