Skip to content

feat(migration): import from Planka v2 - #3552

Merged
kolaente merged 9 commits into
mainfrom
planka-import-research
Aug 20, 2026
Merged

feat(migration): import from Planka v2#3552
kolaente merged 9 commits into
mainfrom
planka-import-research

Conversation

@tink-bot

@tink-bot tink-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Vikunja had no way to import Planka boards. Planka has no export, only its REST API — this adds a planka migrator that pulls projects, boards, lists, cards, labels, checklists, custom fields, comments and attachments live from a Planka v2 instance and feeds them through InsertFromStructure.

Third migrator flavour next to OAuth-code and file upload: "credentials" (URL + API key, or URL + username/password). Credentials are verified synchronously (400 on bad url/credentials/TOTP, 10s cap, no retries) before the async migration is queued; they are never persisted or logged (they ride the in-process migration event only). Routes live on v2 only (pkg/routes/api/v2/migration_credentials.go, always enabled); the listener learns the migrator via a route-free RegisterMigratorForEvents.

Mapping decisions (per plans/feat-planka-migration.md):

  • Project with >1 board → parent project + child per board; single board → one project named after the Planka project.
  • Lists → kanban buckets. closed/archive list cards and isClosed cards → done; trash skipped. Archive cards paged via GET /api/lists/:id/cards.
  • Checklists → TipTap task list in description; custom fields → markdown table; link attachments → link list. File attachments downloaded (capped by files.maxsize, skipped on failure; object-storage redirects followed with the Planka credentials stripped), cover attachment kept.
  • Comments keep author name prefix only when author ≠ importing user. No assignees, memberships, emails or avatars imported.
  • Planka v1 (lists without type) → import fails with a clear message (async, reported by email).

Also fixes a pre-existing importer bug: done tasks placed in an imported bucket lost their done state (moving out of the default "Done" bucket flipped it back).

Uses richtext.MarkdownToHTML instead of moving trello's goldmark helper.

How to verify

  1. Open Settings → Import from other services, click Planka.
  2. Enter the URL of a Planka v2 instance and either an API key (created by a Planka admin via POST /api/users/:id/api-key) or username and password of a local account without 2FA. Click Start import.
  3. Expected: "Vikunja will now import…" message; after the migration email arrives, the Planka projects appear in the sidebar. A project with several boards is a parent project with one child project per board; the kanban view of each board has one bucket per Planka list (active, closed, archive; no trash), archived/closed cards are marked done, labels keep their Planka colours, checklists/custom fields/links show in the task description, comments and file attachments are present.
  4. Enter a wrong URL (e.g. the Vikunja URL itself) or a wrong API key and click Start import.
    Expected: an inline error ("Could not reach a Planka API at the given url…" / "Planka rejected the provided credentials…"), no migration queued.
  5. Use username/password of a Planka account with TOTP enabled (or one that never accepted Planka's end-user terms).
    Expected: error asking to log in to Planka once / use an API key instead (400, code 14007).
  6. API only (v2 only, there is no v1 route): curl -X POST /api/v2/migration/planka/migrate -H 'Authorization: Bearer <jwt>' -d '{"url":"https://planka.example.com","token":"<api key>"}'200 {"message":"Migration was started successfully."}; omitting url or credentials → 400; GET /api/v1/info lists planka in available_migrators.

Docs stub (for the website repo)

Planka: Import from a Planka v2 instance. Enter the instance URL and either an API key or your username and password. API keys are created by a Planka administrator for your account (POST /api/users/:id/api-key); browser session tokens do not work. Password login only works for local Planka accounts without two-factor authentication that have logged in to Planka at least once (Planka 2.2+ asks new users to accept its terms on first login) — SSO users need an API key. Vikunja imports all projects and boards you can see: lists become kanban buckets, closed and archived cards are marked done, checklists and custom fields are appended to the task description, and file attachments are copied. Assignees and board members are not imported. If your Planka runs on a private network, set outgoingrequests.allownonroutableips: true — note this lets every registered user reach non-routable addresses through the importer (and webhooks), so only enable it on trusted instances.

@tink-bot tink-bot added the pr-swarm/started PR Swarm run is currently active on this PR label Aug 18, 2026
@github-actions github-actions Bot added area/attachments File attachments on tasks: upload, preview, download area/import-export Importers (WeKan, Todoist, …) and data exports area/kanban Kanban view, buckets, bucket CRUD, done-bucket behavior integration/inbound Data coming IN from external systems (importers, email-in, MCP, ingest) labels Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-3552.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:pr-3552 latest
https://sha-d79747228a5395218b2ca5c7da96f9714d66fc68.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-d79747228a5395218b2ca5c7da96f9714d66fc68 d797472
https://sha-ecdfcd4ffb51adbb3ad383967c225a05e91c48c8.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-ecdfcd4ffb51adbb3ad383967c225a05e91c48c8 ecdfcd4
https://sha-edd46b7b47b06b2fb50962501b936c0c257f2798.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-edd46b7b47b06b2fb50962501b936c0c257f2798 edd46b7
https://sha-d66851b081788f4a225179db041c4930eace5eee.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-d66851b081788f4a225179db041c4930eace5eee d66851b
https://sha-a91c1f49f44d447c5e4f1da63221a7d874ab3a9e.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-a91c1f49f44d447c5e4f1da63221a7d874ab3a9e a91c1f4
https://sha-771d002edd59274d515d2631cf4c7171ca1f5a8b.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-771d002edd59274d515d2631cf4c7171ca1f5a8b 771d002
https://sha-d7265672d16ada0d757d32d561ed9a37b0f86423.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-d7265672d16ada0d757d32d561ed9a37b0f86423 d726567
https://sha-51d6a0bbec435d8d5694c8e4dc2b2cc190fb4a13.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-51d6a0bbec435d8d5694c8e4dc2b2cc190fb4a13 51d6a0b
https://sha-645c870bc0edf9ce8e8515d533f4bf309cddfc1c.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-645c870bc0edf9ce8e8515d533f4bf309cddfc1c 645c870
https://sha-0f109807e6c5221872fd5e42e7d9bf95bae5a3d3.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-0f109807e6c5221872fd5e42e7d9bf95bae5a3d3 0f10980
https://sha-bfa4080e8cbb2ba271edb8bcd59ad3f871c56a5c.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-bfa4080e8cbb2ba271edb8bcd59ad3f871c56a5c bfa4080

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-3552 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/go-vikunja/vikunja:pr-3552
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-3552

Last updated for commit d797472

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 1. See inline comments.

Comment thread pkg/routes/api/v2/migration_credentials.go Outdated
Comment thread pkg/routes/api/v2/migration_credentials.go Outdated
Comment thread frontend/src/services/migrator/abstractMigration.ts Outdated
Comment thread pkg/modules/migration/handler/listeners.go
Comment thread pkg/events/events.go
Comment thread frontend/src/views/migrate/MigrationHandler.vue
Comment thread frontend/src/views/migrate/MigrationHandler.vue
Comment thread frontend/src/i18n/lang/en.json Outdated
Comment thread frontend/src/views/migrate/migrators.ts
Comment thread pkg/modules/migration/handler/handler.go
@tink-bot

tink-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — automated review. Panel: bughunter · security · conventions · design · verifier — round 3 (final, round cap) @ d726567 (+270/−87 this round, PR +3328/−130)

Verdict: ✅ APPROVE → all actionable items fixed; open question answered (accepted as is)

Round-3 pre-fix: 1 MEDIUM (attachment downloads on S3-backed Planka), 1 convergent MEDIUM (partial comment order), rest LOW/NIT; security found nothing new. Round cap reached — the round-3 fixes (redirect split for downloads, comment order, session cleanup, dedupe) are covered by unit/webtests but were not re-driven at runtime.

❓ Needs your input (0) — answered

  • pkg/modules/migration/planka/planka.go:56 — sync CheckCredentials = outbound probe primitive (WithAnyPort, timing oracle); with outgoingrequests.allownonroutableips: true an internal port scanner for every registered user. Mitigated: 15s total cap, no retries. Same class as webhooks. Maintainer: "Fine as is" — accepted with the docs warning.

✅ Fixed this round (11)

  • downloads follow object-storage redirects with credential headers stripped; api redirects still refused, now without retry (utils.ErrDoNotRetry); default ports ignored in host compare; session deleted when password login fails late; 4xx bodies for callers capped at 1 MiB — 3217f8268
  • partial comments oldest-first (defer slices.Reverse), dead cursor branch — 313b374ad
  • generic registerMigrationMigrate[B], migration_shared.god8702497d
  • form: clearError on resubmit, no native requiredd7265672d

💤 Resolved as nit (1)

  • DownloadFileWithHeaders unlimited/default-client for trello/todoist — separate PR

🔎 Runtime verification (round 3, fake Planka stand-in — no real instance)

✅ cross-host 302 on /api/projects and on the attachment path refused, target got 0 requests; same-host followed, import complete · ✅ trello/todoist pages on /api/v2/migration/*/auth|status|migrate, zero v1 traffic, no console errors · ✅ empty submit shows both in-app errors; scheme-less url → inline error.14010; one error per field · ✅ credential check ≈15.0s for token and password path against filtered/blackholed hosts · ✅ planka UI happy path · ⚠️ round-3 fix (downloads now follow cross-host redirects credential-free) verified by TestDownloadFollowsCrossHostRedirectsWithoutCredentials only

oauth trello v2
empty submit

earlier rounds

username error
already migrated
kanban r1
task r1

Reviewer summaries

Reviewer Assessment
🐛 bughunter Round-2 fixes correct; download redirect refusal would break S3 Planka; partial order, retry waste, session leak, 4xx cap.
🛡 security All closures verified (client, policy, caps, deadline, v2 auth parity, API-token scoping); no new findings.
📏 conventions Lint clean; 4 restating comments cut; commits conventional.
📐 design Partial-comment order regression, dead branch, registrar dedupe, doc-instead-of-enforce (deferred), form required consistency.
🔎 verifier All round-2 fixes hold at runtime; one cosmetic banner nit.
Previous rounds (2) round 2 @ 51d6a0b (36m) — 💬 APPROVE WITH NITS → 17 fixed, 2 nits: redirect guard on data path, single deadline, half cursor, OAuth v2 event registration, frontend all-v2, novalidate. round 1 @ 645c870 (54m) — ⚠️ REQUEST CHANGES → 31 fixed, 9 nits, 1 deferred: v1 routes/dead v2 path, 93s probe, body/pagination caps, poison-log payload, base custom fields, form a11y/status/submit.

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 2. See inline comments.

Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/planka/client.go Outdated
Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/planka/client.go Outdated
Comment thread pkg/modules/migration/helpers.go
Comment thread pkg/webtests/huma_migration_planka_test.go
Comment thread frontend/src/services/migrator/abstractMigration.ts
Comment thread frontend/src/views/migrate/MigrationCredentialsForm.vue
Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/planka/client.go

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 PR Swarm — automated review, round 3 (final, round cap). See inline comments.

Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/planka/client.go
Comment thread pkg/modules/migration/helpers.go
Comment thread pkg/modules/migration/helpers.go
Comment thread pkg/modules/migration/planka/fetch.go
Comment thread pkg/routes/api/v2/migration_shared.go
Comment thread frontend/src/views/migrate/MigrationCredentialsForm.vue
Comment thread frontend/src/views/migrate/MigrationCredentialsForm.vue
Comment thread pkg/utils/retry.go
@tink-bot tink-bot added pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input and removed pr-swarm/started PR Swarm run is currently active on this PR labels Aug 18, 2026
@tink-bot

Copy link
Copy Markdown
Collaborator Author

Manual verification against a real Planka 2.2.1 (docker)

Setup: ghcr.io/plankanban/planka:2.2.1 + postgres, admin user, terms accepted in UI. Data created partly via Playwright (project, board, first cards) and partly via the Planka API: 3 lists (Backlog / In Progress / Done as closed), 6 cards incl. one archived, 3 labels, markdown description + due date, checklist 1/3, 2 comments, file attachment, link attachment, board custom field Estimate = 3 days, a second board and a second project.

Password path (UI form, admin / password): POST /api/v2/migration/planka/migrate → 200, Successfully done migration 1. Both Planka sessions Vikunja created (credential check + migration) are deleted_at != null afterwards.
API key path (POST /api/users/:id/api-key, second Vikunja user): 200, Successfully done migration 2, no session rows created.

Result: Website Relaunch → parent project with children Sprint 1 / Sprint 2; Personal (one board) → flat project. Sprint 1 kanban = Backlog / In Progress / Done / Archive (trash skipped), cards in the same order, Pick a hosting provider (closed list) and Old idea: forum (archive) done, labels with Planka colours, due date, checklist as task list, custom field table, links list, comments (no author prefix — same user), brief.txt attachment (35 B).

Planka Vikunja
planka board vikunja kanban
planka card vikunja task
more

form
comments + attachment

Note for docs: Planka 2.2 shows an end-user terms dialog on first login; an account that never accepted them gets a 403 accept-terms step from POST /api/access-tokens, which the importer reports as "rejected the provided credentials" — log in once in the Planka UI first.

@tink-bot tink-bot added pr-swarm/done PR Swarm finished, nothing deferred and removed pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input labels Aug 19, 2026
@tink-bot
tink-bot force-pushed the planka-import-research branch from 771d002 to a91c1f4 Compare August 19, 2026 14:58
Comment thread frontend/src/i18n/lang/en.json Outdated
Comment thread frontend/src/i18n/lang/en.json Outdated
Comment thread frontend/src/i18n/lang/en.json Outdated
Comment thread frontend/src/views/migrate/icons/planka.png
Comment thread pkg/modules/migration/planka/errors.go Outdated
}

// ErrCodeNoPlankaAtURL holds the unique world-error code of this error
const ErrCodeNoPlankaAtURL = 14010

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

where is the 14xxx error block first defined?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

pkg/modules/migration/errors.go — 14001 ErrNotAZipFile … 14005 ErrMigrationAlreadyRunning; 14006–14010 continue that block. Note: pkg/models/error.go also uses 14001/14002 for API-token errors (pre-existing double booking — the frontend keys translations by bare code, so error.14001 renders the api-token text for the zip error). If you'd rather, I move the planka ones (or the whole migration block) to a fresh range, e.g. 18xxx — say which.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

move to a 142xx range

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in d79747228 — 14201 invalid credentials, 14202 login step required, 14203 invalid config, 14204 no planka at url (+ error.142xx keys).

Payloads can carry credentials and user data; the migration event carries the
migrator with its credentials.
…mported bucket

Creating a done task puts it in the view's default done bucket; moving it into
the imported bucket flipped it back to open. The imported state (incl. done_at)
is restored in bulk after the task loop.
…bounded json decode

Migrators that talk to a user-supplied host need a client with a redirect
policy, a cap on response bodies and no retries on permanent errors
(utils.ErrDoNotRetry). Diagnostic body reads in the shared helpers are capped.
Authenticates with an API key, a JWT or username + password (Planka checks
Bearer before X-Api-Key, so only one header is ever sent; downloads use the
accessToken cookie). The synchronous credential check has one 15s deadline and
no retries. Redirects to another host or to plain http are refused for the
api, followed without credentials for attachment downloads. Response bodies
are capped, url userinfo is stripped, pending login steps (totp, terms) and
non-planka endpoints are reported as distinct client errors (142xx).
Pages archived cards and comments (Planka requires both cursor fields, pages
until empty with a hard cap, partial results are kept), reads base custom
field definitions from the projects payload and rejects Planka v1 payloads.
Everything lands under a "Migrated from Planka" root project; projects with
several boards become a parent with one child per board. Lists become kanban
buckets (trash skipped), closed/archived cards are done, labels keep their
Planka colours, checklists, custom fields and link attachments are rendered
into the description, comments are prefixed with the author unless it is the
importing user. No assignees, memberships or other users' data.
Credentials migrators are verified synchronously before the async migration is
queued (CredentialsChecker). Routes live on v2 only; the listener learns the
migrator via a route-free RegisterMigratorForEvents, which the oauth v2
registrar uses as well. Shared status/migrate registration moved to
migration_shared.go.
URL + API key or username/password, talking to the v2 migration api (all code
migrators now use v2). Generic credentials form component with planka-specific
help texts, status screens for running/previous migrations, translated error
codes.
@tink-bot
tink-bot force-pushed the planka-import-research branch from d797472 to ecdfcd4 Compare August 19, 2026 20:23
@kolaente
kolaente enabled auto-merge (rebase) August 19, 2026 20:24
@kolaente
kolaente merged commit 741133b into main Aug 20, 2026
41 checks passed
@kolaente
kolaente deleted the planka-import-research branch August 20, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/attachments File attachments on tasks: upload, preview, download area/import-export Importers (WeKan, Todoist, …) and data exports area/kanban Kanban view, buckets, bucket CRUD, done-bucket behavior integration/inbound Data coming IN from external systems (importers, email-in, MCP, ingest) pr-swarm/done PR Swarm finished, nothing deferred

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants