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.
- plane_migrate-0.1.0/.github/workflows/ci.yml +46 -0
- plane_migrate-0.1.0/.gitignore +12 -0
- plane_migrate-0.1.0/LICENSE +21 -0
- plane_migrate-0.1.0/PKG-INFO +464 -0
- plane_migrate-0.1.0/README.md +422 -0
- plane_migrate-0.1.0/pyproject.toml +62 -0
- plane_migrate-0.1.0/src/planemigrate/__init__.py +1 -0
- plane_migrate-0.1.0/src/planemigrate/client.py +12 -0
- plane_migrate-0.1.0/src/planemigrate/estimates.py +83 -0
- plane_migrate-0.1.0/src/planemigrate/logger.py +111 -0
- plane_migrate-0.1.0/src/planemigrate/main.py +293 -0
- plane_migrate-0.1.0/src/planemigrate/mapper.py +191 -0
- plane_migrate-0.1.0/src/planemigrate/migrate.py +1441 -0
- plane_migrate-0.1.0/src/planemigrate/plan.py +117 -0
- plane_migrate-0.1.0/tests/__init__.py +0 -0
- plane_migrate-0.1.0/tests/test_logger.py +157 -0
- plane_migrate-0.1.0/tests/test_mapper.py +295 -0
- plane_migrate-0.1.0/tests/test_plan.py +135 -0
- plane_migrate-0.1.0/tests/test_utils.py +140 -0
|
@@ -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,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
|