Skip to content

feat: support team workspace via zeabur workspace (PLA-1590)#244

Open
BruceDu521 wants to merge 5 commits into
mainfrom
bruce/pla-1590-workspace
Open

feat: support team workspace via zeabur workspace (PLA-1590)#244
BruceDu521 wants to merge 5 commits into
mainfrom
bruce/pla-1590-workspace

Conversation

@BruceDu521
Copy link
Copy Markdown
Contributor

@BruceDu521 BruceDu521 commented May 20, 2026

Summary

让 Zeabur CLI 跟 dashboard 一样可以切到 team workspace —— 操作 team 拥有的 project / service / deployment。

Closes PLA-1590
依赖:PLA-1589 (Team.myRole backend 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 说明)。

新增命令

zeabur workspace list                # 列 personal + 所有 team(带 role)
zeabur workspace current             # 显示当前 workspace
zeabur workspace switch <name|id>    # 切到 team workspace
zeabur workspace clear               # 切回 personal(唯一切回个人的方式)

加 global --workspace <name|id> flag,一次性 override 当前 workspace(不改 persistent state)。

解析规则

workspace switch <arg>:
  arg 是 24-char hex → 当 team ID(必须在 caller 的 memberships 里)
  arg 不是 hex      → 当 name 查 memberships:
    0 个匹配  → "no workspace named ..."
    1 个匹配  → 切
    ≥2 个匹配 → 拒绝并列每个候选的 `zeabur workspace switch <id>` 命令

switch personal 不是回到个人的快捷方式 —— 它永远是去找名叫 personal 的 team(team 名字无限制)。回 personal 用 workspace clear

--workspace flag 用同样解析。

Switch / clear 副作用

切换 workspace 会 project / environment / service context(不同 workspace 的 resource ID 不重叠,留旧 context 必然 stale)。每次 switch / clear 输出都会明示清了什么。

哪些命令受 workspace 影响

只有"目录级":

命令 用 workspace
project list
project create
deploy(没 link project) ✓(带 → Creating new project in team workspace "xxx" 提示)
service list -p <pid> ✗ projectID 自带 owner
service deploy -s <sid> ✗ serviceID 自带 owner
variable create ...
deployment log ...

绝大多数命令对 workspace 透明 —— 跟 dashboard 一致。

Startup lazy verify

每次进程启动调一次 teams query 验证 persisted workspace 还在不在 caller 的 memberships 里。被删 / 被踢出 → warning + auto fallback personal。Transport error 不动 workspace(offline 不应该静默切人)。

兼容性

  • 新 commands 都是 additive
  • --workspace flag 默认 empty(= personal,等同今天)
  • ListProjects(ownerID, ...) / CreateProject(ownerID, ...) API 签名改了 —— 内部 caller 全部更新,外部没有人 import 这两个签名
  • Backend projects(ownerID) / createProject(ownerID) 已经在 prod 跑了一段(team plan PLA-1160 系列),CLI 这边走的是已经存在的 schema 路径
  • Team.myRole 是 PLA-1589(zeabur/backend#2131)提供。一起 review、merge 后 deploy。本 PR 的 GraphQL query teams { _id name myRole } 在 backend 部署前会报字段不存在 —— prod 用户拿到这个 CLI 的时间一定晚于 backend deploy(CLI 发版需要打 v* tag),所以发版前 backend merge 即可。

Tests

internal/cmdutil/workspace_test.go 覆盖:

  • 空参数 → required error
  • ID 命中 / ID 不在 memberships
  • Name 唯一命中
  • Name 无匹配
  • Name 重名 → ambiguous 错误,含每个候选的 switch 命令 + role(Bruce 明确要求)
  • isObjectIDHex 各种 edge case
$ go test -run TestResolveWorkspaceArg|TestIsObjectIDHex ./internal/cmdutil/
=== RUN   TestResolveWorkspaceArg_EmptyArg                       --- PASS
=== RUN   TestResolveWorkspaceArg_ByID_Match                     --- PASS
=== RUN   TestResolveWorkspaceArg_ByID_NotAMember                --- PASS
=== RUN   TestResolveWorkspaceArg_ByName_Unique                  --- PASS
=== RUN   TestResolveWorkspaceArg_ByName_NotFound                --- PASS
=== RUN   TestResolveWorkspaceArg_ByName_Ambiguous               --- PASS
=== RUN   TestIsObjectIDHex                                      --- PASS
PASS

全套 go test ./... 通过。

文件清单

新增:

  • pkg/zcontext/workspace.go — Workspace 结构 + Personal/Team 判断
  • pkg/model/team.go — Team / TeamMemberRole models
  • pkg/api/team.go — ListTeams API
  • internal/cmdutil/workspace.goResolveWorkspaceArg(核心解析逻辑,flag + switch 共用)
  • internal/cmdutil/workspace_test.go — 7 个 unit test
  • internal/cmd/workspace/{workspace,list,current,switch,clear}/ — 4 个子命令 + 父命令

修改:

  • pkg/zcontext/{interface,context}.go — Context 接口加 Workspace
  • pkg/api/{interface,project}.go — ProjectAPI 加 ownerID 参数
  • pkg/selector/selector.goNew 加 ownerIDFn 闭包
  • internal/cmdutil/factory.go — Factory 加 Workspace flag + CurrentOwnerID() / SetWorkspaceOverride()
  • internal/cmd/root/root.go — 注册 workspace 命令 + --workspace global flag + PreRunE 里解析 flag + lazy verify + 用闭包构造 selector
  • 4 个 caller 更新:project list / project create / deploy / template deploy 传 ownerID(deploy 多加 team workspace 提示)

Out of Scope(备查)

  • ❌ Team-scoped API key("team service account")—— 真有需求时单开 issue 做完整 IAM 系统设计,参考 AWS IAM / GCP Service Account / GitHub fine-grained PAT
  • ❌ 跨 team 聚合 query("列我所有 team 的 service")—— 没真实需求
  • ❌ Team 命名限制(用户起名自由,"personal" 也允许)

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Workspace management: workspace group with list (ls), current, switch, and clear commands, plus a one-shot --workspace flag.
    • Project/service creation and name-based lookups now respect the active workspace (personal vs team).
  • Behavior Changes

    • Switching workspaces clears pinned project/environment/service context.
    • Workspace display shows team role when available.
  • Documentation

    • README updated with workspace guidance.

Walkthrough

Adds persistent workspace model and CLI workspace commands, Team API and owner-scoped project APIs, factory workspace override and --workspace resolution, selector owner integration, workspace-aware util lookups, and tests; integrates owner scoping across project/service/variable/domain commands.

Changes

Workspace Management & Owner Scoping

Layer / File(s) Summary
Workspace Data Model & Persistence
pkg/zcontext/workspace.go, pkg/zcontext/context.go, pkg/zcontext/interface.go
New Workspace type with IsPersonal()/IsTeam(); viper-backed GetWorkspace, SetWorkspace, ClearWorkspace; interface docs updated.
API Team Support & Project Scoping
pkg/model/team.go, pkg/api/interface.go, pkg/api/team.go, pkg/api/project.go
Adds TeamAPI/ListTeams and Team model; ProjectAPI methods updated to accept ownerID and switch GraphQL queries/mutations for personal vs team scoping.
Factory, Root Flag Resolution & Verification
internal/cmdutil/factory.go, internal/cmd/root/root.go
Factory adds workspace override, CurrentOwnerID(), CurrentWorkspace(), SetWorkspaceOverride(), memoized ListTeams, and persistent --workspace flag handling; root resolves one-shot overrides and verifies persisted workspace.
Workspace Subcommand Group
internal/cmd/workspace/*
Adds workspace parent and list, current, `switch <name
Selector & Project Integration
pkg/selector/selector.go, internal/cmd/project/*, internal/cmd/deploy/*, internal/cmd/template/deploy/deploy.go
Selector accepts ownerID callback; project listing/creation and deploy/template auto-create use resolved owner ID to scope or create projects in the active workspace.
Workspace-aware util lookups & tests
internal/util/project.go, internal/util/service.go, internal/util/*_test.go, internal/cmdutil/workspace.go, internal/cmdutil/workspace_test.go
Refactors GetProjectByName and GetServiceByName to accept explicit owner/project context and implement personal vs team lookup paths; adds resolver ResolveWorkspaceArg and unit tests covering ID/name/ambiguity/edge cases.
Commands: workspace-aware lookups usage
internal/cmd/* (project, service, deployment, domain, variable, etc.)
Updates many commands to use the new util signatures and f.CurrentOwnerID()/context values when resolving names to IDs; keeps existing behavior for ID-based commands unchanged.
User Documentation
README.md
Adds "Workspaces (personal / team)" section documenting defaults, commands, context-clearing side effects, ID command independence, and --workspace one-off override.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature being added: support for team workspaces via the zeabur workspace command, with a reference to the related issue PLA-1590.
Description check ✅ Passed The description comprehensively documents the feature, including use case, new commands, parsing rules, switching behavior, affected commands, startup verification, compatibility notes, testing, and file manifest.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bruce/pla-1590-workspace

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Stop the spinner on the ListAllProjects error 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 win

Align 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

📥 Commits

Reviewing files that changed from the base of the PR and between 18268b9 and c593186.

📒 Files selected for processing (22)
  • README.md
  • internal/cmd/deploy/deploy.go
  • internal/cmd/project/create/create.go
  • internal/cmd/project/list/list.go
  • internal/cmd/root/root.go
  • internal/cmd/template/deploy/deploy.go
  • internal/cmd/workspace/clear/clear.go
  • internal/cmd/workspace/current/current.go
  • internal/cmd/workspace/list/list.go
  • internal/cmd/workspace/switch/switch.go
  • internal/cmd/workspace/workspace.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/workspace.go
  • internal/cmdutil/workspace_test.go
  • pkg/api/interface.go
  • pkg/api/project.go
  • pkg/api/team.go
  • pkg/model/team.go
  • pkg/selector/selector.go
  • pkg/zcontext/context.go
  • pkg/zcontext/interface.go
  • pkg/zcontext/workspace.go

Comment thread internal/cmdutil/workspace_test.go Outdated
Comment thread internal/cmdutil/workspace.go
Comment thread pkg/api/project.go Outdated
Comment thread README.md
@canyugs
Copy link
Copy Markdown
Contributor

canyugs commented May 20, 2026

Panel Review — zeabur/cli PR #244 + zeabur/backend PR #2131

1. What problem does this PR solve?

Team members cannot use the CLI to operate on team-owned projects — zeabur project list only shows personal 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 Team.myRole GraphQL field that returns the caller's own role in a team via a direct team_members lookup (hitting the existing partial compound index). Avoids the N+1 teamMembers(teamID) fan-out.

CLI #244: Adds zeabur workspace {list, current, switch, clear} commands and a global --workspace flag. Resolution logic (ResolveWorkspaceArg) is shared between the flag and the switch command. Switching clears project/environment/service context (resource IDs don't overlap between workspaces). A lazy startup verify detects stale memberships and falls back to personal.

3. What are the tradeoffs?

  • Adds a network call (ListTeams) to every CLI invocation when a team workspace is persisted (lazy verify). Multiple reviewers flagged that this can compound to 2–3 calls in a single invocation.
  • Team names are unconstrained (duplicates allowed), requiring ID-based disambiguation — good UX but the error message hardcodes zeabur workspace switch rather than using the actual binary name.
  • switch personal is NOT a shortcut back to personal (by design) — requires workspace clear. Correct but potentially surprising.

4. Verdict

Consensus: request changes
Reviewers: Aragorn: approve (pending dedupe), Legolas: request changes, Gimli: request changes, Boromir: request changes, Frodo: approve


🔴 SUGGESTED CHANGES

  • F1. internal/cmd/deploy/deploy.go:214 — The "Creating new project in team workspace" message reads from the persisted workspace (f.Config.GetContext().GetWorkspace()) but the actual project creation uses f.CurrentOwnerID() which may come from --workspace flag. When --workspace team-b overrides a persisted team-a, the message names the wrong team; when overriding personal, the message is missing entirely. Fix: derive the display name from the same resolved source as CurrentOwnerID. (raised by: Gimli, Legolas, Boromir)
  • F2. internal/cmd/root/root.go:105-112 — A single CLI invocation can call ListTeams up to 3 times: once in resolveWorkspaceFlag, once in verifyPersistedWorkspace, and once in the command itself (e.g. workspace current). Cache the result from the first call on the Factory and reuse it for verify + downstream commands. (raised by: Aragorn, Gimli, Legolas, Boromir)

⚖️ Unresolved disagreements

  • None — reviewers agree on all findings

🟡 NIT

  • N1. internal/gateway/graphql/team.graphqls:209-215 — Schema docstring says "Null only when the caller is not a member" but the resolver returns a GraphQL error (not null) when the caller is unauthenticated. Suggest adding "Errors when the caller is not authenticated." (raised by: Aragorn)
  • N2. internal/cmdutil/workspace.go:70 — Ambiguous error hardcodes zeabur workspace switch; users invoking via npx zeabur or aliases get a non-copy-pasteable command. Consider using os.Args[0]. (raised by: Aragorn)
  • N3. internal/cmd/workspace/current/current.go:43ListTeams error is silently swallowed; the role just doesn't display. Consider at least a debug-level log. (raised by: Aragorn)

🟢 INFO

  • I1. Backend #2131 is clean: nullable additive field, sub-ms index lookup, privacy-narrowing vs teamMembers, test covers 5 membership states. (raised by: Frodo, Gimli, Legolas)
  • I2. Config persistence confirmed safe — PersistentPostRunE calls viper.WriteConfig(), new workspace keys fall through automatically. (raised by: Aragorn)
  • I3. ResolveWorkspaceArg ambiguous-name UX (roles + concrete switch commands per candidate) meets Bruce's explicit requirement. (raised by: Frodo)
  • I4. Closure-based ownerIDFn in Selector prevents stale-owner footgun across mid-process workspace switches. (raised by: Frodo, Legolas)

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>
@BruceDu521
Copy link
Copy Markdown
Contributor Author

review 反馈已处理(commit c515cee):

Panel — Suggested changes (F)

  • F1 (deploy.go): "Creating new project in team workspace X" 现在用 f.CurrentWorkspace() 取,跟 CurrentOwnerID() 来自同一个 resolved 源。--workspace team-b 覆盖 persisted team-a 时不再显示错的 team 名;override 到 personal 时直接不显示。snapshot 一次顶部用到底,避免 race。
  • F2 (Factory.ListTeams 缓存): 单进程内 teams 只调一次。resolveWorkspaceFlag / verifyPersistedWorkspace / workspace list / workspace current / workspace switch 全部走 f.ListTeams(ctx),sticky error caching(出错不重试同一进程)。

Panel — Nits

  • N3 (current.go 吞 error): 现在 f.Log.Debugf 一行,命令依然不 fail。
  • N2 (ambiguous error 硬编码命令名): 抽 invocationName()filepath.Base(os.Args[0])npx zeabur 之类 wrapper 也能 copy-paste 错误信息里的命令。

CodeRabbit

  • ✅ 测试包改 cmdutil_test,只测公开 API。移掉对私有 isObjectIDHex 的直接测,加了两个新 case 通过公开接口覆盖同一谓词:
    • TestResolveWorkspaceArg_ByID_CaseInsensitive(大写 hex 也能解析)
    • TestResolveWorkspaceArg_NonHex_NotMistakenForID(24-char 非 hex 必须走 name 分支)
  • ✅ ID 比较大小写不敏感化(isObjectIDHex 本来就接受大小写,下游 == 不该 case-sensitive)
  • ListAllProjects 内部循环 propagate ctx,不再 context.Background() 丢 caller cancel
  • deploy.go spinner s.Stop() 移到 err check 前,所有 path 都正确停
  • workspace.go 注释改写:两条 path 都执行 membership 校验(旧注释暗示 hex path "trust" raw input,跟代码不符)
  • ✅ README clarify:24-char hex 永远当 ID,不只是 name conflict 时

附带:Factory.SetWorkspaceOverride 签名从 string 改成 *zcontext.Workspace(要带 name 给 deploy hint),新增 Factory.CurrentWorkspace() 配套 CurrentOwnerID

全部测试通过(8 个 TestResolveWorkspaceArg),go build ./... + go vet ./... 都 clean。

🤖 Generated with Claude Code

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c593186 and c515cee.

📒 Files selected for processing (10)
  • README.md
  • internal/cmd/deploy/deploy.go
  • internal/cmd/root/root.go
  • internal/cmd/workspace/current/current.go
  • internal/cmd/workspace/list/list.go
  • internal/cmd/workspace/switch/switch.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/workspace.go
  • internal/cmdutil/workspace_test.go
  • pkg/api/project.go
✅ Files skipped from review due to trivial changes (1)
  • README.md

Comment thread internal/cmdutil/workspace_test.go Outdated
@canyugs
Copy link
Copy Markdown
Contributor

canyugs commented May 20, 2026

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 Team.myRole GraphQL field returning the caller's own role via direct team_members index lookup.

CLI #244: Adds zeabur workspace {list, current, switch, clear} commands and a global --workspace flag with per-process ListTeams memoization and consistent workspace resolution.

3. What are the tradeoffs?

  • Per-process cache means a workspace change by another process within the same CLI invocation won't be seen (acceptable — CLI is short-lived)
  • Team names remain unconstrained (duplicates require ID-based disambiguation)

4. Verdict

Consensus: approve (pending trivial CI lint fix)
Reviewers: Aragorn: approve, Legolas: approve, Gimli: request changes (CI lint only), Boromir: approve, Frodo: approve


🔴 SUGGESTED CHANGES

  • F1. internal/cmdutil/workspace_test.go:50 — CI lint QF1001 (De Morgan's law): rewrite !(X || Y) as (!X && !Y). 1-line style change, logic correct, blocks CI green. (raised by: Gimli, confirmed by: Frodo)

⚖️ Unresolved disagreements

  • None — all reviewers agree F1/F2 from round 1 are fixed. Only the lint nit remains.

🟡 NIT

  • None remaining (N1–N3 from round 1 all addressed in this fix)

🟢 INFO

  • I1. Factory.ListTeams() memoization is clean: sticky error, per-process lifetime, 5 call sites funneled through one fetch. (Aragorn, Frodo)
  • I2. ResolveWorkspaceArg refactored to accept []model.Team directly — better testability, no API client mock needed. (Aragorn, Frodo)
  • I3. Case-insensitive ID matching added with dedicated test. (Legolas, Frodo)

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>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c515cee and 61e8378.

📒 Files selected for processing (31)
  • internal/cmd/context/set/set.go
  • internal/cmd/deployment/get/get.go
  • internal/cmd/deployment/list/list.go
  • internal/cmd/deployment/log/log.go
  • internal/cmd/domain/create/create.go
  • internal/cmd/domain/delete/delete.go
  • internal/cmd/domain/list/list.go
  • internal/cmd/project/clone/clone.go
  • internal/cmd/project/delete/delete.go
  • internal/cmd/project/export/export.go
  • internal/cmd/project/get/get.go
  • internal/cmd/service/delete/delete.go
  • internal/cmd/service/exec/exec.go
  • internal/cmd/service/get/get.go
  • internal/cmd/service/instruction/instruction.go
  • internal/cmd/service/metric/metric.go
  • internal/cmd/service/network/network.go
  • internal/cmd/service/port-forward/port_forward.go
  • internal/cmd/service/redeploy/redeploy.go
  • internal/cmd/service/restart/restart.go
  • internal/cmd/service/suspend/suspend.go
  • internal/cmd/service/update/tag/tag.go
  • internal/cmd/variable/create/create.go
  • internal/cmd/variable/delete/delete.go
  • internal/cmd/variable/env/env.go
  • internal/cmd/variable/list/list.go
  • internal/cmd/variable/update/update.go
  • internal/util/project.go
  • internal/util/project_test.go
  • internal/util/service.go
  • internal/util/service_test.go

Comment thread internal/cmd/context/set/set.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>
@BruceDu521
Copy link
Copy Markdown
Contributor Author

Round 3 fixes(commit b97244f):

CI lint blocker (Panel F1 / CodeRabbit)

  • workspace_test.go:50 De Morgan QF1001:!(X || Y)(!X && !Y),本地 staticcheck -checks QF1001 已 clean

CodeRabbit on 61e8378

  • set.go setProject + setService:用户只传 --id 时 local name 是空,success log 现在打 <>。修:resolve 完之后 name = project.Name / service.Name 回填

Bruce 要求的"全路径向后兼容测试" — 新加 7 个 Factory 测试

测试 守护的不变量
TestFactory_PersonalUserInvariant 核心:zero Factory → CurrentOwnerID() == ""IsPersonal() == true。每个 owner-aware util helper 都靠这个空字符串走 legacy personal 路径。这条挂了 = 普通用户被静默路由到 team 分支
TestFactory_CurrentOwnerID_PersistedPersonal Config 存在但无持久化 workspace → 仍然 personal
TestFactory_CurrentOwnerID_PersistedTeam 持久化 team 路径返回 team ID
TestFactory_CurrentOwnerID_OverrideBeatsPersisted --workspace flag 覆盖 persisted,且不污染配置文件
TestFactory_CurrentOwnerID_OverrideNilClears SetWorkspaceOverride(nil) 正确清除
TestFactory_ListTeams_Memoizes 单进程多次调用 = 1 次后端 hit
TestFactory_ListTeams_StickyError 失败 fetch sticky,不重试

加上之前的 9 个 TestGetProjectByName_* / TestGetServiceByName_*(其中包括 _Personal 明确断言 client call 字节等同旧实现)+ 8 个 TestResolveWorkspaceArg_* = 总共 24 个 unit test 覆盖本 PR 全路径

go build ./... + go vet ./... + 全套 go test + staticcheck 全部 clean。

🤖 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>
@BruceDu521
Copy link
Copy Markdown
Contributor Author

Dev-2 E2E 测试报告

部署 backend 3a824c0 到 dev-2 之后跑完 11 个 bucket。测试中发现 2 个 CLI bug 并已修复(commit 736851b)。

测试 setup

  • Binary:本地 build CLI 把 pkg/constant/const.go 临时改成 api-2.zeabur.dev(不 commit),strings 验过 binary embedded URL 只有 api-2.zeabur.dev
  • Config 隔离:HOME=/tmp/zeabur-test 避免污染真实 ~/.config/zeabur/cli.yaml
  • 账号:airfreedom5@gmail.com 主号(pla1230 admin / 3 个 viewer team)

Bucket 矩阵

Bucket Result
Schema probe workspace list 实际命中 api-2 teams { myRole }
B10 Help text 4 个子命令 + --workspace flag 描述
B1 Personal baseline project list / context set --name(personal path 字节等同旧逻辑)
B2 list/current * marker、role 列、persisted vs team
B3 Switch happy by name / by ID / UPPERCASE ID 大小写不敏感 / config 写入 / project context 清空
B4 Switch edge 不存在 name / switch personal(拒绝当快捷方式)/ 有效 hex 非 member
B5 Clear from team / already-personal
B6 --workspace flag list / persisted-不被污染 / 无效 hex ⚠️ 发现 2 bug,已修
B7 project list/create team 下 list 看到 team projects / personal 看不到 team projects / project create owner=team / cleanup delete
B8 F2 fix(critical) team --name → team project / team 找 personal-only name → 拒绝(不 silently fall personal)/ personal --name 字节等同旧逻辑
B9 Lazy verify 注入假 stale workspace → warn + auto-clear

关键证据

1. F2 fix(Codex round 2 的核心问题)verified end-to-end

# 在 team workspace 下,用 --name 解析 team-owned project
$ /tmp/zeabur-dev2 workspace switch pla1230-probe-1776760608745
$ /tmp/zeabur-dev2 context set project -i=false --name for-clone-test
INFO  Project context is set to <for-clone-test>

$ grep -A2 "project:" ~/.config/zeabur/cli.yaml
    project:
        id: 6a0733237fda5660beee1d47   # ← team 的 project ID
        name: for-clone-test

# 在 team workspace 下找 personal-only 的 name → 必须拒绝,不能 silently fall personal
$ /tmp/zeabur-dev2 context set project -i=false --name dev-test
ERROR  failed to get project: no project named "dev-test" in this workspace

跟旧版(PR c515cee)行为对比:旧版会 fall 到 personal 找到同名 project 静默 pin 错。Fix verified。

2. Project create owner verification

$ /tmp/zeabur-dev2 workspace switch pla1230-probe-1776760608745  # admin team
$ /tmp/zeabur-dev2 project create -i=false --name pla1590-e2e-test --region server-69bbfce115f1d12e55d131a3
{"id":"6a0d64c1be57ba3ed0f7579c","name":"pla1590-e2e-test","status":"success"}

# 在 team workspace 下 list:能看到
$ /tmp/zeabur-dev2 project list | grep pla1590
6a0d64c1be57ba3ed0f7579c  pla1590-e2e-test  ...

# 切到 personal:看不到
$ /tmp/zeabur-dev2 workspace clear
$ /tmp/zeabur-dev2 project list | grep pla1590
(empty)

确认 ownerID 写到了 team,不是 personal。

3. Lazy verify

# 手动 inject 一个不存在的 team 到 cli.yaml
$ sed -i '' 's/id: ""/id: 69aabbccddeeffaabbccddee/; ...' ~/.config/zeabur/cli.yaml

$ /tmp/zeabur-dev2 workspace current
WARN  Persisted workspace "fake-stale-team" [69aabbccddeeffaabbccddee] is no longer in your memberships; falling back to personal.
personal  (Bruce Du)

# config 里 workspace 字段被清回 ""

测试中修复的 2 个 bug(commit 736851b

文件 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.go revert 回 api.zeabur.com
  • CLI repo 干净,HEAD = 736851b

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b97244f and 736851b.

📒 Files selected for processing (2)
  • internal/cmd/workspace/current/current.go
  • internal/cmd/workspace/list/list.go

Comment on lines +34 to +38
// 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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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).

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants