Skip to content

refactor(products): migrate product command ownership - #101

Merged
Ali1213 merged 97 commits into
ucloud:masterfrom
Episkey-G:refactor-product-migration
Jul 6, 2026
Merged

Ali1213 merged 97 commits into
ucloud:masterfrom
Episkey-G:refactor-product-migration

Conversation

@Episkey-G

@Episkey-G Episkey-G commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Refactor product command ownership/layout for the product migration work.
  • Preserve upstream CLI command compatibility while adding migrated product command surfaces.
  • Keep validation gates around product boundaries, Cobra command tree/completion, and command snapshots.

Validation

  • go test ./... -skip TestUhost
  • go vet ./...
  • go test ./hack/snapshot/...
  • go run ./hack/check-product
  • Cobra-focused verification:
    • go test ./cmd ./pkg/command ./hack/snapshot -skip TestUhost
    • ucloud completion bash|zsh|fish|powershell generated shell completion scripts successfully
    • ucloud __complete "" and ucloud __complete uhost "" returned Cobra dynamic-completion candidates successfully
  • Built current fork and upstream binaries, then compared command surfaces: no upstream commands removed; the new visible command subtree is completion.
  • Live read-only comparison with --profile ucloud: 19/19 semantic JSON cases passed across migrated products (uhost, uphost, udisk, eip, firewall, image, mysql db) and legacy/platform commands (project, region, subnet, vpc, bw pkg, bw shared, udpn, ulb, gssh, pathx, redis, memcache).
  • Real billable lifecycle validation with --profile ucloud:
    • migrated: firewall create/update/add-rule/delete, eip allocate/modify-bw/release, udisk create/expand/delete, uhost create/stop/start/restart/delete, image create-image/delete, mysql create/stop/start/restart/delete
    • legacy/unmigrated: vpc create/delete, subnet create/delete, ulb inner create/delete
  • Cleanup scan found no codex-e2e temporary resources remaining.

Notes

  • Image create/delete was validated successfully. The immediate image list assertion was timing-sensitive/eventually consistent, so cleanup verification was used as the final state check.

Episkey-G and others added 30 commits June 22, 2026 14:25
Adds hack/snapshot/snapshot.go (Render), snapshot_test.go (TDD unit +
baseline golden test), cmd/snapshot_export.go (AddChildrenForSnapshot
stub for test-only tree construction without InitConfig/network), and
the generated baseline hack/snapshot/testdata/cmdtree.golden (58 KB,
full ucloud command tree: path/use/short/flags/defaults/required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add OutputFormat enum (OutputTable/OutputJSON/OutputYAML) with OutputTable as iota-zero
- Add Context struct with in/out/err/format/biz/config fields, NewContext(Deps) constructor,
  and Out()/In()/Format() accessors
- Add Product interface with Metadata() and NewCommand(*Context) contract
- Add Metadata struct (Name, Owners, Commands []string, Version)
- Relocate base.PickResourceID, base.ParseError, and cmd/ulb.go readFile (as ReadFile)
  verbatim into pkg/cli/util.go; originals kept in place for existing callers
- Add context_test.go (external package cli_test) covering TestPickResourceID and
  TestOutputFormatDefault

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iceClient reuses same auth path for oauth & AK/SK (no §9 regression)

Extract buildCredential/attachHandlers (param-based) shared by NewClient and
the parameterless exported wrappers BuildCredential/AttachHandlers (package-var
based). NewClient keeps using its own ac/credConfig params so the oauth reactive
retry target stays the construction-source profile, not package ConfigIns.
Add cli.NewServiceClient[T] reusing base.BuildCredential + base.AttachHandlers
so oauth and AK/SK profiles share one credential+handler code path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds pkg/ui as a standalone presentation layer (no base/pkg/cli imports).
Printer.PrintList renders to any io.Writer in Table, JSON, or YAML format;
Format iota order (Table=0,JSON=1,YAML=2) matches pkg/cli.OutputFormat for
lossless numeric conversion in B5. Table rendering is relocated verbatim from
base/util.go with fmt.Printf→fmt.Fprintf(out) threading. IsTTY checks *os.File
terminal status via go-isatty. Confirm mirrors base.Confirm+ux.Prompt with
injected io.Reader/io.Writer streams for testability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ux/spinner.go: replace fmt.Printf with fmt.Fprintf(s.out, ...) in
  Spinner.reset() and Spinner.render(); add NewRefreshTo(out io.Writer)
  constructor alongside NewRefresh()
- ux/spinnerv2.go: replace fmt.Printf with fmt.Fprintf(s.out, ...) in
  Spin.reset() and Spin.renderToScreen(); renderToString() untouched
- ux/document.go: replace fmt.Printf with fmt.Fprintf(d.out, ...) in
  document.reset()
- base/util.go: add pkg/ui import; add non-TTY branch in Spoll() that
  drains the done channel and prints a single terminal-state line
  ("<pollText>...done\n" or "<pollText>...timeout\n") without any
  spinner animation when p.Out is not a TTY; Poll() left unchanged
- base/spoll_test.go: two focused tests asserting non-TTY output
  contains the done line and no spinner frame runes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…) + bind helpers; wire Context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…u format)

Relocates GenDoukuTree/GenDoukuTreeCustom from the cobra fork's
vendor/github.com/spf13/cobra/doc/douku_docs.go into cmd/douku.go as
unexported genDoukuTree/genDoukuTreeCustom. Updates cmd/doc_md.go to call
the local genDoukuTree instead of doc.GenDoukuTree. Uses only stable
upstream cobra/doc API (doc.GenMarkdownCustom), so the build remains green
on the fork now and after Task C2 drops the fork for upstream cobra.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d (drop fork pflag completion from cmd)

The 9 completion-bearing bind helpers in cmd/util.go (bindRegion,
bindRegionS, bindZone, bindZoneEmpty, bindZoneEmptyS, bindZoneS,
bindProjectID, bindProjectIDS, bindChargeType) now accept *cobra.Command
instead of *pflag.FlagSet. Fork-only SetFlagValuesFunc/SetFlagValues calls
are replaced with command.SetCompletion/command.SetFlagValues wrappers.

All 241 call sites across 13 cmd/*.go files updated accordingly. The 4
non-completion helpers (bindGroup, bindLimit, bindOffset, bindQuantity)
remain unchanged on *pflag.FlagSet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…etPersistentCompletion)

Add SetPersistentCompletion to pkg/command/completion.go that targets
cmd.PersistentFlags() instead of cmd.Flags(). The cobra fork's
SetFlagValuesFunc silently no-ops when the flag is not in the given FlagSet,
so calling SetCompletion (which uses cmd.Flags()) for a flag registered on
PersistentFlags() drops tab completion silently.

Wire cmd/root.go:61 to use SetPersistentCompletion for --profile, and add
TestProfileCompletionRegistered to guard against this regression. Also update
stale comment in cmd/udb.go to reference the current API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Episkey-G and others added 28 commits June 25, 2026 15:43
The release detector previously used only `svu next` (feat/fix/breaking), so a
refactor-only merge produced no release (e.g. PR #4's platform cleanup was
skipped). Add a git-log grep for refactor/perf commit types alongside svu next
so platform refactors also cut a patch release; pure docs/chore/test/ci/style/
build merges still skip. The bump stays PATCH-ONLY (svu patch) — version policy
unchanged.

Verified locally: new detector matches all 11 refactor commits since v0.3.4
(=> would release v0.3.5) and correctly ignores chore/docs/ci/test/style; YAML
validates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-aware

Two fixes from PR #5 review:
- BLOCKER (self-review): 'git log | grep -q' under bash -o pipefail (the GitHub
  Actions default shell) makes git log take SIGPIPE when grep matches early,
  flipping the whole pipeline non-zero, so the release is WRONGLY SKIPPED —
  reproduced on the real v0.3.4..HEAD range (printed NO-MATCH despite 11
  matching refactor commits). Capture git log to a var and match via here-string
  (no pipe -> no SIGPIPE).
- P2 (Codex): scan %B (full commit message) instead of %s, so a refactor/perf
  type that only appears in a merge-commit body / PR title is also detected.

Verified under bash -eo pipefail: real range -> release; merge-body-only type
-> release; chore/docs/ci/test/style -> skip; 5M-line input -> no SIGPIPE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci(release): also release on refactor/perf commits (keep patch-only)
…nds consistency

rule7: no two enabled products may declare the same top-level command name (cobra AddCommand silently shadows duplicates) — 设计 §6.3 一级命令无冲突. rule8: each enabled product's product.go Metadata().Commands must match its products.yaml commands (order-independent, AST-extracted) — 设计 §6.3 命令声明与 products.yaml 一致.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uct.yaml

gen-products/check-product 改为扫描 products/*/product.yaml 聚合;归属真源随产品目录走(product.go Owners 删,归属只在 product.yaml)。gen.go byte-identical、命令树 golden 不变,纯重构。对应 ROADMAP P2a-0。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
新建 hack/owner-gate:判定纯函数 decide —— 改动落在单一 products/X 且 PR
author ∈ base 版 owners → 产品自治可 auto-merge;越界(改平台/跨产品/非
owner/增删产品)→ 平台 PR。资格一律用 git show <base>:products/X/product.yaml
判,防 PR 内把自己加进 owners 自证提权。含 11 个单测:8 场景 + name-status
解析 + 临时 git 仓库证明读 base 非 head。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
owner-gate job 跑判定并把裁决以 sticky comment 贴到 PR;auto-merge job 对
自治 PR 跑 gh pr merge --auto --squash(GitHub 原生,fail-closed);hold-gate
在 do-not-merge/hold 标签存在时变红,经分支保护挡住合并。scope-gate 瘦身为仅
conventional 标题,产品/平台/跨产品/owner 路由全交 owner-gate(消除旧 scope-gate
对跨产品 exit 1 硬失败与 spec「跨产品→平台 PR」的冲突)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…el change

采纳 #6 Codex 评审两条:
- owner-gate 原用两点 `git diff BASE HEAD`,分支落后 master 时会冒出无关
  master 侧文件、误把自治 PR 降级为平台 PR;改三点 `BASE...HEAD`(merge-base
  ..head),与 GitHub「Files changed」一致。本地临时仓库实证:两点冒出 phantom
  D master-only.txt,三点只剩 PR 真改动。
- pull_request 默认不含 labeled/unlabeled,加标签后 hold-gate 不重跑、分支保护
  仍见旧绿 check,/hold 急停失效;补 types 让其重跑、经分支保护挡合并。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: P2a 准入 gate(owner-gate + 原生 auto-merge)+ rule7/8 + products.yaml 拆分
平台 PR 默认 fail-closed 红,除非作者是 admin 或有 admin(≠作者)在当前 head commit 上的 Approve;产品 owner 自治不变。修跨产品 rename 误路由 + rename-away 下线绕过。
P3 Part 0 — 平台地基(C2 ux 平台暴露)。纯平台改动,产品命令零改动;为 batch-1 产品迁移补齐平台契约。

- pkg/cli/result.go: OpResultRow / ProgressWriter / EmitResult(结构化写出,table no-op、json/yaml 出 stdout)
- pkg/cli/progress.go: 并发进度 facade(NewProgress/NewBlock/Refresh/Disable/Sspoll)+ ConcurrentAction(Block=ux.Block)
- base/util.go: Poller.Sspoll 非 TTY 抑制(镜像 Spoll)
- ux/document.go: 导出 NewDocument(writer) + 非 TTY 自动抑制(全局 Doc 不动)
- pkg/cli/forward.go, pkg/command/bind.go: BindGroup / RegionList·ZoneList·ProjectList getter / AllRegions(保 error) / Log*·LogFilePath forwarder
- base/{client,log}.go: 平台请求日志 handler(logToFile 只写 cli.log、不传 DAS)替代产品手拼

验证:build/vet/golden/check-product/test/6平台 全绿;真实 CLI 增删改查 + EIP allocate→release 计费生命周期对比 master 行为零回归。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rewall/udisk/eip/image/uhost)

Migrate the six compute-domain products out of the monolithic cmd/ package into self-contained products/ packages on the pkg/cli platform facade.

- Migrated: uphost, firewall, udisk, eip, image, uhost (behind ctx *cli.Context; no base.BizClient/ux singletons; cross-product compat shims kept for batch-2).
- Bug fixes surfaced during migration: uphost --uphost-id filter wiring, firewall copy --src-fw enforcement, udisk --snapshot-id (rename + binding).
- Machine-output contract (aws/gcloud model): stdout carries only the structured result (EmitResult); narration/progress/confirm prompts and errors/warnings go to stderr; failures exit non-zero without dumping usage. uhost list --output is the AWS-style format selector (legacy wide flag removed).
- OAuth: keep base.AuthCredential pointer stable so registration-time product clients see token refreshes.

Verified end-to-end against the live API and diffed against upstream master; build/vet/golden/check-product/test-suite green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…example (P3.5) (#10)

* refactor(products): own domain constants per product, drop model/status+model/cli imports

* feat(check-product): rule-9 enforces the §6.1 product import whitelist

* feat(snapshot): per-product goldens under products/<name>/testdata; platform golden prunes product subtrees

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(example): per-verb file layout for product_onboarding (spec §2 alignment)

* chore(hack): fix stale products.yaml references in doc comments (per-product product.yaml since P2a-0)

* feat(example): demonstrate the machine-output contract (EmitResult/ProgressWriter) in write verbs

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (P3.6 B7) (#19)

* feat(check-product): rule-10 bans grab-bag filenames and multi-constructor files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): grant auto-merge job contents:write (enablePullRequestAutoMerge requirement)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ts (#20)

* fix(cli): EmitResult emits [] not null for empty result set (F-6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): non-zero exit when a product command reports an error, excluding completion (F-3 core)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): robust completion detection in exit gate; comment/test cleanup (F-3 core review)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: route terminal command errors through ctx.HandleError for non-zero exit (F-3 Tier2)

* feat(cli): Confirm returns (bool,error) for non-interactive confirmation (F-5 platform)

* fix: handle Confirm (bool,error) at simple call sites (F-5 category A)

* fix: handle Confirm (bool,error) in batch loops, partial-success semantics (F-5 category B)

* fix: propagate Confirm three-state through uhost helper chains (F-5 category C)

* fix(example): update onboarding delete confirmation for Confirm error return

* fix(udisk): keep legacy snaphost-id snapshot flag alias

* Revert "fix(udisk): keep legacy snaphost-id snapshot flag alias"

This reverts commit ecabefd.

* chore: rerun PR gate after title fix

* fix(cli): address confirmation review edge cases

* fix(mysql): confirm restart before config apply

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@Episkey-G Episkey-G changed the title Refactor product command migration refactor(products): migrate product command ownership Jul 6, 2026
@Ali1213
Ali1213 merged commit 15e6c4d into ucloud:master Jul 6, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants