Kanboard to Gitea migration
4 min read August 05, 2025 #sqlSome time ago I decided to move all tasks from Kanboard closer to the projects in my Gitea self-hosted instance. Surprisingly, Gitea doesn't have any importers, so I ended up preparing the data and calling the Gitea API.
Database#
I started by downloading the SQLite database. Inside the application, there is a specific option to do this:
There are quite a lot of tables, but the structure is simple.
tags#
Zero project id means these tags are global.
id | name | project_id | color_id |
---|---|---|---|
2 | easy | 0 | green |
3 | medium | 0 | yellow |
4 | hard | 0 | red |
5 | bug | 0 | red |
6 | improvement | 0 | green |
7 | feature | 0 | blue |
project_has_categories#
id | name | project_id | color_id |
---|---|---|---|
3 | articles | 1 | deep_orange |
4 | core | 1 | purple |
5 | forum | 1 | cyan |
tasks#
id | title | description | is_active | date_creation | category_id |
---|---|---|---|---|---|
30 | Authors in articles | Short About section | 1 | 1706568367 | 3 |
110 | FIX Mail | 0 | 1706652157 | 4 |
Of course, there are also modification and completion dates, colors, etc. Note that the category has a one-to-one relation, while the tags have many-to-many.
In Gitea, all of these tags and categories should be under Labels. First, let's create them:
Gitea API#
Now, let's go to the Gitea API to check what's needed to create an issue.
POST /repos/{owner}/{repo}/issues
Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
owner* string owner of the repo
repo * string name of the repo
body object
CreateIssueOption
Quite a simple structure. Moreover, I don't need the due_date
, milestone
, ref
, and assignees
is always only me.
Overall, the idea is:
- Prepare a SQL query containing the
title
,body
,closed
andlabels
columns. - Convert the result to a valid JSON array of tasks.
- For each task object in the array, make a request to the Gitea API using cURL.
Data retrieval#
Let's start by retrieving the title, body, category and tags. For the tags, grouping and aggregation are required:
SELECT
t.title,
(1 - t.is_active) as closed,
t.description as body,
c.name as category,
group_concat(tg.name) as tags
FROM tasks t
LEFT JOIN project_has_categories c ON t.category_id = c.id
LEFT JOIN task_has_tags ttg ON ttg.task_id = t.id
LEFT JOIN tags tg ON tg.id = ttg.tag_id
GROUP BY t.id
ORDER BY t.date_completed, t.date_modification
title | closed | body | category | tags |
---|---|---|---|---|
Authors in articles | 0 | Short About section | articles | hard,feature |
FIX Mail | 1 | core | medium |
Now, let's combine category and tags into one column labels
. Also, why not just include the creation/modification date time at the end of the body column?
SELECT
t.title,
(1 - t.is_active) as closed,
-- body
trim(
concat(
t.description,
"\n\nCreated: ",
strftime(
"%Y-%m-%d %H:%M:%S",
datetime(t.date_creation, "unixepoch")
),
"\nLast modified: ",
strftime(
"%Y-%m-%d %H:%M:%S",
datetime(coalesce(t.date_completed, t.date_modification), "unixepoch")
)
),
"\n"
) as body,
-- tags
trim(
concat(c.name, ",", group_concat(tg.name)),
","
) as labels
FROM tasks t
LEFT JOIN project_has_categories c ON t.category_id = c.id
LEFT JOIN task_has_tags ttg ON ttg.task_id = t.id
LEFT JOIN tags tg ON tg.id = ttg.tag_id
GROUP BY t.id
ORDER BY t.date_completed, t.date_modification
title | closed | body | labels |
---|---|---|---|
Authors in articles | 0 | Short About section Created: 2024-01-29 22:46:07 Last modified: 2024-02-02 19:52:46 | articles,hard,feature |
FIX Mail | 1 | Created: 2024-01-30 22:02:37 Last modified: 2024-02-05 22:07:10 | core,medium |
The body now concatenates the task description, the formatted creation time, and the last modification time. If there's no description, trim("\n\nCreated: ...", "\n")
will remove leading line breaks.
Label IDs#
According to the Gitea API, labels
should contain an array of label ids. We need to replace these "articles,hard,feature" with ids from Gitea:
GET /repos/{owner}/{repo}/labels
Get all of a repository's labels
The trick is to just rename the categories and tags:
and voilà
title | closed | body | labels |
---|---|---|---|
Authors in articles | 0 | Short About section Created: 2024-01-29 22:46:07 Last modified: 2024-02-02 19:52:46 | 48,45,50 |
FIX Mail | 1 | Created: 2024-01-30 22:02:37 Last modified: 2024-02-05 22:07:10 | 49,44 |
JSON transformation#
Finally, export the result as JSON. Many SQLite clients have an option for this.
All that's left to do is construct the correct JSON using https://jsoneditoronline.org/ and JSON Query.
Bash script#
The rest is a simple Bash script that uses jq to extract an object from a JSON array and sends a POST request to the Gitea API using cURL:
#!/usr/bin/env sh
| | while ; do
done
Unfortunately, the Gitea API doesn't have a Projects API yet, so it's impossible to assign a ticket to a project board.
Result#
Finally, here's the result: