UN-3479 [FEAT] CLI restructure with user-group cloning + share-state replication#19
Conversation
Replace the 'unstract-clone' console script with a top-level 'unstract'
Click group ('unstract clone' is now the canonical invocation), promote
click/rich from the 'clone' extra into core dependencies (the extra is
removed), and document the CLI — usage, env promotion use case, main
options and exit codes — in the README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New 'group' phase (runs first): clones org user groups by name with
idempotent merge — a like-named target group is always reused, never
renamed or errored — and records int-pk remaps for downstream phases.
Opt-in --clone-group-members matches members to target users by email,
bulk-adds the hits and reports the misses as warnings; service accounts
(@platform.internal) never migrate.
After each shareable resource lands (adapter, connector, workflow,
pipeline, api_deployment, custom_tool), its source share state is
mirrored via POST /{resource}/{id}/share/: shared_groups through the
group remap (axis omitted with a warning when the group phase is
excluded), shared_to_org as-is, shared_users via id -> email -> target
id (missing users skipped + warned). shared_users stays stripped from
create POSTs (SERVER_MANAGED). Users/groups listings are memoised per
run; dry-run never POSTs. Reports gain a non-fatal warnings channel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Empty Prompt Studio projects cannot be exported (backend guard), so they have no source registry entry and no workflow references. The phase previously republished unconditionally, turning these into counted failures on every run. Now the source registry lookup runs first and the export + remap are skipped when no entry exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| src/unstract/clone/phases/group.py | New phase cloning org groups by name with email-based member migration; a try/except that spans both the POST and the re-list can mask a successful create and leave the remap unrecorded. |
| src/unstract/clone/sharing.py | New module replicating share state post-create; race condition in _cached addressed, payload construction and dry-run handling look correct. |
| src/unstract/clone/client.py | New org users/groups/share API methods; list_group_members still lacks the or {} null guard present in list_users (prior thread). |
| src/unstract/clone/phases/base.py | Adds share_path_template class attribute and apply_share helper that delegates to sharing.apply_share_state; clean delegation pattern. |
| src/unstract/clone/phases/custom_tool.py | Adds share-state replication and moves source registry check before the export gate to skip republishing empty/never-exported tools; logic is sound. |
| src/unstract/clone/orchestrator.py | Registers GroupPhase as the first phase so group remaps are available for share-state replication in all later phases. |
| src/unstract/clone/report.py | Adds warnings list to PhaseResult and a _render_warnings_summary section to the rich/plain/JSON report renderers. |
| src/unstract/clone/context.py | Adds clone_group_members option, share_cache dict, and share_cache_lock to CloneContext; thread-safe per-run memoisation of user/group listings. |
| src/unstract/cli.py | New top-level unstract Click group that registers clone as a subcommand; keeps python -m unstract.clone working via the existing local group. |
| pyproject.toml | Promotes click and rich from optional [clone] extra to core dependencies and renames the console script from unstract-clone to unstract. |
| tests/clone/test_sharing.py | Comprehensive tests covering payload building, cache races, dry-run, group-phase exclusion, and POST failures; good coverage. |
| tests/clone/test_group_phase.py | Tests for group creation, idempotent adoption, dry-run, member cloning, and service-account skipping; all key paths covered. |
Sequence Diagram
sequenceDiagram
participant CLI as unstract clone
participant GP as GroupPhase
participant AP as Resource Phase
participant SH as sharing.py
participant SRC as Source API
participant TGT as Target API
CLI->>GP: run()
GP->>SRC: list_groups()
GP->>TGT: list_groups()
loop each source group
alt group exists on target
GP->>GP: adopt + record remap
else
GP->>TGT: create_group()
GP->>TGT: list_groups() re-list for id
GP->>GP: record remap(src_id to tgt_id)
end
opt --clone-group-members
GP->>SRC: list_group_members()
GP->>SH: target_user_id_by_email()
SH->>TGT: list_users() cached
GP->>TGT: add_group_members()
end
end
CLI->>AP: run()
loop each resource
AP->>TGT: create resource
AP->>SH: apply_share_state()
opt share axes missing
SH->>SRC: get_detail() src_detail_fn
end
SH->>SRC: list_users() cached
SH->>SH: remap.resolve group ids
SH->>TGT: share_resource(path, payload)
end
Reviews (2): Last reviewed commit: "UN-3479 [FIX] Address review: prefer suc..." | Re-trigger Greptile
…rd None group listing
- _cached: a raced _FETCH_FAILED no longer shadows a peer's successful
listing (would silently no-op all share replication for the run).
- list_groups: 'or {}' guard mirrors list_users so a None response can't
raise AttributeError.
- Add regression tests for both race directions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
unstractClick group withcloneas first subcommand;click/richmoved from optional extra to core (fixes broken-by-defaultImportError)groupphase (runs first) clones org user groups by name with reuse-on-match; optional--clone-group-membersflag (email-matched, missing users skipped and reported)is_service_accountfield with email-suffix fallback for older backendsTest Plan
uv run pytest→ 184 passed (167 pre-existing + 17 new)Dependencies
feat/org-migration-platform-api-gaps(service-account group management)README
unstract cloneusage and optional--clone-group-membersflag🤖 Generated with Claude Code