plane-migrate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint-and-test:
11
+ name: Lint & Test (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.11", "3.12"]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -e ".[dev]"
31
+
32
+ - name: Lint — ruff check
33
+ run: ruff check src/ tests/
34
+
35
+ - name: Lint — ruff format check
36
+ run: ruff format --check src/ tests/
37
+
38
+ - name: Run tests
39
+ run: pytest --cov=planemigrate --cov-report=term-missing --cov-report=xml
40
+
41
+ - name: Upload coverage report
42
+ uses: actions/upload-artifact@v4
43
+ if: matrix.python-version == '3.11'
44
+ with:
45
+ name: coverage-report
46
+ path: coverage.xml
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ *.egg-info/
8
+ .env
9
+ *.log
10
+ migration-plan-*.yaml
11
+ # migration state files (<src-proj-id>-<dst-proj-id>.json)
12
+ ????????-????-????-????-????????????-????????-????-????-????-????????????.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Manish Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,464 @@
1
+ Metadata-Version: 2.4
2
+ Name: plane-migrate
3
+ Version: 0.1.0
4
+ Summary: Migrate projects between Plane instances or workspaces
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 Manish Gupta
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE
27
+ Keywords: migration,plane,project-management
28
+ Classifier: License :: OSI Approved :: MIT License
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Programming Language :: Python :: 3.11
31
+ Classifier: Programming Language :: Python :: 3.12
32
+ Requires-Python: >=3.11
33
+ Requires-Dist: plane-sdk<1.0.0,>=0.2.8
34
+ Requires-Dist: pyyaml>=6.0
35
+ Requires-Dist: rich>=13.0.0
36
+ Requires-Dist: typer>=0.9.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
39
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
40
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # plane-migrate
44
+
45
+ A CLI tool to migrate projects between [Plane](https://plane.so) instances or between workspaces on the same instance — with full fidelity, resumable runs, and delta re-runs.
46
+
47
+ > **Works with**: Plane Cloud (`api.plane.so`) and self-hosted Plane instances (v0.23+)
48
+
49
+ ---
50
+
51
+ ## What gets migrated
52
+
53
+ | Entity | Details |
54
+ |:-------|:--------|
55
+ | **Work Item Types** | Custom types defined in the source project |
56
+ | **States** | All workflow states; matched by name to avoid duplicates |
57
+ | **Labels** | All labels; matched by name |
58
+ | **Members** | Matched by email across instances; unmatched members flagged in plan |
59
+ | **Estimates** | Estimate schema + all points |
60
+ | **Work Items** | Title, description, state, priority, assignee, labels, type, dates, estimate point, parent — roots first, then children |
61
+ | **Relations** | `blocked_by`, `blocking`, `duplicate`, `relates_to` |
62
+ | **Links** | External URLs attached to work items |
63
+ | **Comments** | Full comment body with author attribution byline |
64
+ | **Modules** | With work item membership |
65
+ | **Cycles** | With work item membership |
66
+ | **Intake** | Intake work items |
67
+ | **Pages** | Requires `--session-cookie` (see [Pages](#pages-migration)) |
68
+
69
+ ### Known limitations
70
+
71
+ | Gap | Reason |
72
+ |:----|:-------|
73
+ | Activity history | Plane API has no write endpoint for activity entries |
74
+ | File attachments | Plane API has no attachment upload endpoint |
75
+ | Page listing via API | Pages require a browser session cookie |
76
+
77
+ ---
78
+
79
+ ## Installation
80
+
81
+ **Requires Python 3.11+**
82
+
83
+ ### Recommended — pipx (for CLI use)
84
+
85
+ [pipx](https://pipx.pypa.io) installs CLI tools in isolated environments and makes them available globally. This is the recommended approach on any system.
86
+
87
+ ```bash
88
+ # Install pipx if you don't have it
89
+ # macOS
90
+ brew install pipx && pipx ensurepath
91
+
92
+ # Debian / Ubuntu
93
+ sudo apt install pipx && pipx ensurepath
94
+
95
+ # Install plane-migrate
96
+ pipx install git+https://github.com/mguptahub/plane-migrate.git
97
+ ```
98
+
99
+ ### pip (inside a virtualenv)
100
+
101
+ ```bash
102
+ python -m venv venv && source venv/bin/activate
103
+ pip install git+https://github.com/mguptahub/plane-migrate.git
104
+ ```
105
+
106
+ ### Local development
107
+
108
+ ```bash
109
+ git clone https://github.com/mguptahub/plane-migrate.git
110
+ cd plane-migrate
111
+ python -m venv venv && source venv/bin/activate
112
+ pip install -e ".[dev]"
113
+ ```
114
+
115
+ Verify:
116
+
117
+ ```bash
118
+ plane-migrate --help
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Quick Start
124
+
125
+ ### Step 1 — Dry run
126
+
127
+ Always start with a dry run. It fetches everything from source, previews what will be created, and writes a **plan YAML** — without touching the destination.
128
+
129
+ ```bash
130
+ plane-migrate \
131
+ --src-url https://api.plane.so \
132
+ --src-token <source-api-token> \
133
+ --src-workspace <source-workspace-slug> \
134
+ --src-project <source-project-id-or-key> \
135
+ --dst-url https://api.plane.so \
136
+ --dst-token <destination-api-token> \
137
+ --dst-workspace <destination-workspace-slug> \
138
+ --dst-project <destination-project-id-or-key> \
139
+ --dry-run
140
+ ```
141
+
142
+ This writes a `migration-plan-<timestamp>.yaml` in the current directory.
143
+
144
+ ### Step 2 — Review the plan
145
+
146
+ ```yaml
147
+ members:
148
+ mapped:
149
+ - alice@old.com: alice@new.com # auto-matched by email
150
+ unmatched:
151
+ bob@old.com: "" # ← fill in destination email
152
+ _hint: Fill in destination emails for unmatched members, then pass this file via --members-map.
153
+
154
+ states:
155
+ create: [Todo, In Progress, Done]
156
+ skip: [Backlog] # already exist on destination
157
+
158
+ work_items:
159
+ total: 142
160
+ roots: 98
161
+ children: 44
162
+
163
+ modules: [Sprint 1, Sprint 2, Backlog]
164
+ cycles: [Q1 2025, Q2 2025]
165
+ ```
166
+
167
+ Fill in any blank destination emails under `members.unmatched`.
168
+
169
+ ### Step 3 — Real run
170
+
171
+ ```bash
172
+ plane-migrate \
173
+ --src-url https://api.plane.so \
174
+ --src-token <source-api-token> \
175
+ --src-workspace <source-workspace-slug> \
176
+ --src-project <source-project-id-or-key> \
177
+ --dst-url https://api.plane.so \
178
+ --dst-token <destination-api-token> \
179
+ --dst-workspace <destination-workspace-slug> \
180
+ --dst-project <destination-project-id-or-key> \
181
+ --members-map migration-plan-<timestamp>.yaml
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Environment Variables
187
+
188
+ All flags can be set via environment variables — useful for CI or repeated runs:
189
+
190
+ | Variable | Flag equivalent |
191
+ |:---------|:----------------|
192
+ | `PLANE_SRC_URL` | `--src-url` |
193
+ | `PLANE_SRC_TOKEN` | `--src-token` |
194
+ | `PLANE_SRC_WORKSPACE` | `--src-workspace` |
195
+ | `PLANE_DST_URL` | `--dst-url` |
196
+ | `PLANE_DST_TOKEN` | `--dst-token` |
197
+ | `PLANE_DST_WORKSPACE` | `--dst-workspace` |
198
+ | `PLANE_SESSION_COOKIE` | `--session-cookie` |
199
+ | `PLANE_MIGRATE_LOG` | `--log-file` |
200
+
201
+ ```bash
202
+ export PLANE_SRC_URL=https://api.plane.so
203
+ export PLANE_SRC_TOKEN=plane_api_...
204
+ export PLANE_SRC_WORKSPACE=my-workspace
205
+ export PLANE_DST_URL=https://self-hosted.example.com
206
+ export PLANE_DST_TOKEN=plane_api_...
207
+ export PLANE_DST_WORKSPACE=my-workspace
208
+
209
+ plane-migrate \
210
+ --src-project <src-project-id> \
211
+ --dst-project <dst-project-id>
212
+ ```
213
+
214
+ ---
215
+
216
+ ## All CLI Flags
217
+
218
+ ```
219
+ plane-migrate [OPTIONS]
220
+
221
+ Source:
222
+ --src-url TEXT Source Plane instance URL
223
+ --src-token TEXT Source API token
224
+ --src-workspace TEXT Source workspace slug
225
+ --src-project TEXT Source project UUID or key
226
+
227
+ Destination:
228
+ --dst-url TEXT Destination Plane instance URL
229
+ --dst-token TEXT Destination API token
230
+ --dst-workspace TEXT Destination workspace slug
231
+ --dst-project TEXT Destination project UUID or key
232
+
233
+ Options:
234
+ --dry-run / -n Preview what would be migrated — writes a plan YAML
235
+ --only TEXT Migrate only specific entities (repeatable)
236
+ --members-map TEXT YAML file mapping src emails → dst emails
237
+ --plan-file TEXT Where to write the dry-run plan (default: migration-plan-<ts>.yaml)
238
+ --state-file TEXT State file path for resumable runs (default: <src>-<dst>.json)
239
+ --session-cookie TEXT Browser session cookie for page migration
240
+ --log-file TEXT Log file path
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Selective Migration — `--only`
246
+
247
+ Migrate only specific entity types. Can be repeated.
248
+
249
+ ```bash
250
+ # Re-run only links and comments (fill gaps)
251
+ plane-migrate ... --only links --only comments
252
+
253
+ # Migrate only work items and relations
254
+ plane-migrate ... --only work-items --only relations
255
+
256
+ # Migrate pages (requires session cookie)
257
+ plane-migrate ... --only pages --session-cookie "..."
258
+ ```
259
+
260
+ **Valid `--only` values:**
261
+
262
+ | Value | What it migrates |
263
+ |:------|:-----------------|
264
+ | `types` | Work item types |
265
+ | `states` | Workflow states |
266
+ | `labels` | Labels |
267
+ | `members` | Member matching (no destination writes) |
268
+ | `estimates` | Estimate schema + points |
269
+ | `work-items` | All work items (roots + children) |
270
+ | `relations` | Work item relations |
271
+ | `links` | External URL links on work items |
272
+ | `comments` | Work item comments |
273
+ | `modules` | Modules + work item membership |
274
+ | `cycles` | Cycles + work item membership |
275
+ | `intake` | Intake work items |
276
+ | `pages` | Pages (requires `--session-cookie`) |
277
+
278
+ ---
279
+
280
+ ## Resumable Runs & Gap Filling
281
+
282
+ Every successful write is tracked in a **state file** (`<src-project-id>-<dst-project-id>.json`). If a run is interrupted — network error, rate limit, crash — just rerun the same command. The migrator will:
283
+
284
+ - **Skip** entities already created (tracked by source ID → destination ID mapping)
285
+ - **Fill gaps** for links and comments — only creates the ones missing per work item
286
+ - **Save state incrementally** — progress is never lost mid-run
287
+
288
+ ```bash
289
+ # Interrupted run — just rerun, it picks up where it left off
290
+ plane-migrate ... --state-file my-migration.json
291
+ ```
292
+
293
+ ### State file structure
294
+
295
+ ```json
296
+ {
297
+ "src_project": "...",
298
+ "dst_project": "...",
299
+ "states": { "<src-state-id>": "<dst-state-id>", ... },
300
+ "labels": { "<src-label-id>": "<dst-label-id>", ... },
301
+ "work_items": { "<src-item-id>": "<dst-item-id>", ... },
302
+ "modules": [{ "src": "...", "dst": "...", "workitems": [...] }],
303
+ "cycles": [{ "src": "...", "dst": "...", "workitems": [...] }],
304
+ "links_done": { "<src-item-id>": ["<src-link-id>", ...] },
305
+ "comments_done": { "<src-item-id>": ["<src-comment-id>", ...] },
306
+ "relations": ["<src-item-id>:relation_type:<src-related-id>", ...]
307
+ }
308
+ ```
309
+
310
+ > Keep the state file between runs. Deleting it causes the migrator to treat everything as new — resulting in duplicates in the destination.
311
+
312
+ ---
313
+
314
+ ## Members Mapping
315
+
316
+ Members are matched **by email** between source and destination. When the source and destination share the same Plane instance (workspace-to-workspace migration), members are automatically matched.
317
+
318
+ When migrating across instances, emails may differ. Unmatched members appear in the dry-run plan:
319
+
320
+ ```yaml
321
+ members:
322
+ unmatched:
323
+ old-email@company.com: "" # ← fill in new email
324
+ ```
325
+
326
+ Pass the edited plan file (or any YAML with `src_email: dst_email` pairs) via `--members-map`:
327
+
328
+ ```bash
329
+ plane-migrate ... --members-map migration-plan.yaml
330
+ ```
331
+
332
+ Work items assigned to unmatched members will still be migrated — but without an assignee.
333
+
334
+ ---
335
+
336
+ ## Pages Migration
337
+
338
+ Pages require a **browser session cookie** because the public API does not expose a page listing endpoint. The session cookie is used only to fetch page content — no writes happen with it.
339
+
340
+ ### Getting the session cookie
341
+
342
+ 1. Open your Plane instance in a browser and log in
343
+ 2. Open DevTools → Application → Cookies
344
+ 3. Copy the values of `csrftoken` and `session-id`
345
+ 4. Pass them as a single string:
346
+
347
+ ```bash
348
+ plane-migrate ... \
349
+ --session-cookie "csrftoken=abc123; session-id=xyz789"
350
+ ```
351
+
352
+ If `--session-cookie` is not provided, pages are skipped and noted in the summary.
353
+
354
+ ---
355
+
356
+ ## Logging
357
+
358
+ Every run writes a structured JSON log to `plane-migrate-<timestamp>.log` (or the path from `--log-file` / `PLANE_MIGRATE_LOG`).
359
+
360
+ Each line is a JSON event:
361
+
362
+ ```json
363
+ {"ts": "2025-04-01T10:23:45Z", "kind": "created", "entity": "work_item", "name": "Fix login bug", "src_id": "abc", "dst_id": "xyz"}
364
+ {"ts": "2025-04-01T10:23:46Z", "kind": "skipped", "entity": "label", "name": "Bug", "detail": "already exists"}
365
+ {"ts": "2025-04-01T10:23:47Z", "kind": "failed", "entity": "comment", "name": "on abc", "detail": "429 Too Many Requests"}
366
+ ```
367
+
368
+ Tail in real time:
369
+
370
+ ```bash
371
+ tail -f plane-migrate-*.log | python3 -c "
372
+ import sys, json
373
+ for line in sys.stdin:
374
+ e = json.loads(line)
375
+ print(f\"{e['ts'][11:19]} [{e['kind']:8}] {e['entity']:12} {e['name']}\")
376
+ "
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Rate Limiting
382
+
383
+ The migrator writes at ~3 requests/second (`300ms` delay between writes). On HTTP `429 Too Many Requests`, it backs off automatically:
384
+
385
+ - Wait 30s → retry
386
+ - Wait 45s → retry
387
+ - Wait 60s → retry
388
+ - Fail and log the error
389
+
390
+ Failures are logged and counted but do not abort the run. Use `--only` to re-run specific phases after fixing rate limit issues.
391
+
392
+ ---
393
+
394
+ ## Cross-Instance Migration
395
+
396
+ Migrating from one Plane deployment to another (e.g. Plane Cloud → self-hosted):
397
+
398
+ ```bash
399
+ plane-migrate \
400
+ --src-url https://api.plane.so \
401
+ --src-token plane_api_<cloud-token> \
402
+ --src-workspace my-cloud-workspace \
403
+ --src-project <project-id> \
404
+ --dst-url https://plane.mycompany.com \
405
+ --dst-token plane_api_<selfhosted-token> \
406
+ --dst-workspace my-internal-workspace \
407
+ --dst-project <project-id> \
408
+ --members-map members.yaml
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Same-Instance Workspace Migration
414
+
415
+ Migrating between two workspaces on the same Plane instance (same token works for both):
416
+
417
+ ```bash
418
+ plane-migrate \
419
+ --src-url https://api.plane.so \
420
+ --src-token plane_api_<token> \
421
+ --src-workspace old-workspace \
422
+ --src-project <src-project-id> \
423
+ --dst-url https://api.plane.so \
424
+ --dst-token plane_api_<token> \
425
+ --dst-workspace new-workspace \
426
+ --dst-project <dst-project-id>
427
+ ```
428
+
429
+ Members are auto-matched by email — no `--members-map` needed.
430
+
431
+ ---
432
+
433
+ ## Comparison with Plane's Native Export
434
+
435
+ | Capability | CSV Export/Import | JSON Export | plane-migrate |
436
+ |:-----------|:-----------------:|:-----------:|:-------------:|
437
+ | Work items | ✓ (flat) | ✓ | ✓ |
438
+ | States / Labels | names only | names only | ✓ full remap |
439
+ | Comments | ✗ | ✓ | ✓ with attribution |
440
+ | Relations | ✗ | ✓ | ✓ |
441
+ | Links | ✗ | ✓ | ✓ |
442
+ | Modules / Cycles | ✗ | names only | ✓ with membership |
443
+ | Estimates | ✗ | ✗ | ✓ |
444
+ | Pages | ✗ | ✗ | ✓ (session cookie) |
445
+ | Intake | ✗ | ✗ | ✓ |
446
+ | Member remapping | ✗ | ✗ | ✓ |
447
+ | Resumable | ✗ | ✗ | ✓ |
448
+ | Re-importable | ✓ | ✗ | ✓ |
449
+
450
+ ---
451
+
452
+ ## Contributing
453
+
454
+ Pull requests are welcome. When contributing:
455
+
456
+ - Keep the migration sequence order (types → states → labels → members → estimates → work-items → relations → links → comments → modules → cycles → intake → pages)
457
+ - All new entity types should follow the same pattern: fetch from source → remap IDs → create on destination → track in state file
458
+ - State must be saved incrementally (per entity, not at the end of the run)
459
+
460
+ ---
461
+
462
+ ## License
463
+
464
+ MIT