feat: support team workspace via zeabur workspace (PLA-1590)#244
feat: support team workspace via zeabur workspace (PLA-1590)#244BruceDu521 wants to merge 5 commits into
zeabur workspace (PLA-1590)#244Conversation
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>
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds persistent workspace model and CLI workspace commands, Team API and owner-scoped project APIs, factory workspace override and ChangesWorkspace Management & Owner Scoping
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
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
| // Use the effective workspace so the `*` marker tracks a `--workspace` | ||
| // flag override, not just the persisted state. Otherwise | ||
| // `--workspace foo workspace list` would print `*` on the persisted | ||
| // team rather than `foo`. | ||
| currentID := f.CurrentOwnerID() |
There was a problem hiding this comment.
Separate effective owner selection from persisted-workspace warning checks.
currentID now tracks the effective owner (including --workspace overrides), but the later warning path describes persisted workspace state. Using the same variable for both can hide a stale persisted workspace when an override is active.
💡 Proposed fix
- currentID := f.CurrentOwnerID()
+ currentID := f.CurrentOwnerID()
+ persistedWorkspaceID := f.Config.GetContext().GetWorkspace().ID
@@
- if currentID != "" {
+ if persistedWorkspaceID != "" {
@@
- if t.ID == currentID {
+ if t.ID == persistedWorkspaceID {
@@
- currentID,
+ persistedWorkspaceID,
)
}
}🤖 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/workspace/list/list.go` around lines 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).
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.