Bidirectionally sync a Jira project with a Todo+ (vscode-todo-plus) text file.
Project description
todo-jira-sync
Bidirectionally sync a Jira project with a Todo+ plain-text file — the
format used by the vscode-todo-plus
extension. Edit your backlog as a flat, version-controllable text file in your
editor; run one command; Jira and the file converge.
Scaffolded from tedivm/robs_awesome_python_template
conventions (uv, pyproject.toml, Typer, Pydantic Settings, Ruff, mypy,
pytest, GitHub Actions).
Mapping rules
Each non-blank, non-comment line maps to one Jira issue, by indentation and the trailing colon:
| Todo+ line | Jira issue type |
|---|---|
ends with :, no leading indent |
Epic |
ends with :, indented |
User Story |
a task (☐ / ✔ / ✘ …) |
Task |
| a task nested under another task | Sub-task |
Authentication: ← Epic
Login flow: ← Story (parent: Authentication)
☐ Build the login form ← Task (parent: Login flow)
☐ Add OAuth providers ← Task (parent: Login flow)
☐ Google provider ← Sub-task (parent: Add OAuth providers)
☐ Password reset email ← Task (parent: Authentication)
A node's Jira parent is its nearest enclosing container: a Task takes the Story it sits under, or the Epic if there is no enclosing Story; a Story takes its Epic; a Sub-task takes the Task it sits under.
Status mapping
| Todo+ symbol | Status | Jira category |
|---|---|---|
☐ [ ] |
To Do | new |
@started(...) |
In Progress | indeterminate |
✔ ✓ [x] |
Done | done |
✘ [-] @cancelled |
Cancelled | done + cancelled status name |
Jira has no "cancelled" status category; cancelled is detected by status
name (configurable via CANCELLED_STATUS_NAMES).
How identity works
Each synced line gets a @jira(KEY) tag written back into the file (inserted
before the trailing colon for projects, so it still reads as a project):
Authentication @jira(WEB-1):
☐ Build the login form @jira(WEB-3)
That tag is the stable anchor — rename a line freely and the link survives.
Install
Requires Python ≥ 3.10. Using uv:
uv venv
uv pip install -e ".[dev]"
or with pip:
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
Configure
Copy .env.example to .env and fill in your details:
cp .env.example .env
For Jira Cloud, create an API token at
https://id.atlassian.com/manage-profile/security/api-tokens and set
JIRA_EMAIL + JIRA_API_TOKEN with JIRA_AUTH=basic. For Server/Data Center,
use a personal access token with JIRA_AUTH=bearer.
Use
# See what would happen — touches nothing:
todo-jira-sync status --todo todo.todo --project WEB
# Full bidirectional sync:
todo-jira-sync sync --todo todo.todo --project WEB
# One-way only:
todo-jira-sync push # local file -> Jira (never edits the file)
todo-jira-sync pull # Jira -> local file (never creates/edits Jira)
# Conflict policy when both sides changed the same field:
todo-jira-sync sync --conflict jira # jira | todo | skip
A JSON sidecar todo.todo.todojira.json is written next to your file. It is
the 3-way-merge baseline (the common ancestor) — keep it, but it need not be
committed (it is git-ignored by default).
How sync decides (3-way merge)
For each issue the engine compares the live file and live Jira against the baseline from the last run:
- only the file changed → push to Jira
- only Jira changed → pull into the file
- both changed the same field → conflict, resolved by
--conflict - new in file → created in Jira (parents first)
- new in Jira → pulled into the file (under the right parent)
- gone from Jira but known → kept locally and reported (never silently lost)
The engine never deletes Jira issues and never deletes local lines.
Docker
A multi-stage, uv-based image is included. It builds the package into a slim runtime image whose entrypoint is the CLI, running as a non-root user.
# Build (VERSION is only needed because versioning comes from git tags):
docker build --build-arg VERSION=0.1.0 -t todo-jira-sync .
# Run against a working directory that holds your todo file and .env:
docker run --rm --env-file .env -v "$PWD:/work" todo-jira-sync \
sync --todo todo.todo --project WEB
Or via Compose (put your todo file and .env in ./work):
docker compose run --rm sync status
docker compose run --rm sync sync --todo todo.todo --project WEB
Prebuilt multi-arch images (amd64 + arm64) are published to GitHub Container Registry by CI on version tags (and on demand via the workflow's manual trigger).
Releasing
Versioning is derived from git tags via setuptools-scm. Pushing a tag like
v0.1.0 triggers two workflows: one builds the sdist/wheel and publishes to
PyPI (via OIDC Trusted Publishing — no API token stored), the other builds and
pushes the multi-arch container image. CI (ci.yaml) runs ruff, mypy and
pytest on a Python 3.10–3.14 matrix for every push and PR.
Develop
make check # ruff + mypy + pytest
make test
The sync core (config, models, todo_format, state, sync) is pure
standard library, so the tests run against an in-memory fake Jira with no
network and no live credentials. The two test files also run standalone:
python tests/test_todo_format.py
python tests/test_sync.py
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file todo_jira_sync-1.0.0.tar.gz.
File metadata
- Download URL: todo_jira_sync-1.0.0.tar.gz
- Upload date:
- Size: 32.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6d7c7c3822df59f0577a686268a51ab5694f3a34b5196ac66ea241b0a747fb9
|
|
| MD5 |
23f9c5933e8314fcf62cab04914011ba
|
|
| BLAKE2b-256 |
3c67e1fc6b8aa04b5d5c03f58f229b4a4c093917ffabae070d215d5a7ad8b894
|
Provenance
The following attestation bundles were made for todo_jira_sync-1.0.0.tar.gz:
Publisher:
publish-pypi.yaml on kalw/todo-jira-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
todo_jira_sync-1.0.0.tar.gz -
Subject digest:
b6d7c7c3822df59f0577a686268a51ab5694f3a34b5196ac66ea241b0a747fb9 - Sigstore transparency entry: 1614013602
- Sigstore integration time:
-
Permalink:
kalw/todo-jira-sync@6641cc5e6e3689f8b286c5c31e38ec19fb69f14e -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/kalw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yaml@6641cc5e6e3689f8b286c5c31e38ec19fb69f14e -
Trigger Event:
push
-
Statement type:
File details
Details for the file todo_jira_sync-1.0.0-py3-none-any.whl.
File metadata
- Download URL: todo_jira_sync-1.0.0-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee5c6bb444fd6678afe5a0a82eb63a350d5756daf451b6f1a448a34979482f0d
|
|
| MD5 |
28ee6b24b032ed9e540a60aec8a324cf
|
|
| BLAKE2b-256 |
f26700e9e95813410e8036f2a97fedd23e7f52a8117c1498dcb2be1cfa6720e8
|
Provenance
The following attestation bundles were made for todo_jira_sync-1.0.0-py3-none-any.whl:
Publisher:
publish-pypi.yaml on kalw/todo-jira-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
todo_jira_sync-1.0.0-py3-none-any.whl -
Subject digest:
ee5c6bb444fd6678afe5a0a82eb63a350d5756daf451b6f1a448a34979482f0d - Sigstore transparency entry: 1614013693
- Sigstore integration time:
-
Permalink:
kalw/todo-jira-sync@6641cc5e6e3689f8b286c5c31e38ec19fb69f14e -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/kalw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yaml@6641cc5e6e3689f8b286c5c31e38ec19fb69f14e -
Trigger Event:
push
-
Statement type: