todo-jira-sync 1.0.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.
- todo_jira_sync-1.0.0/.dockerignore +17 -0
- todo_jira_sync-1.0.0/.env.example +35 -0
- todo_jira_sync-1.0.0/.github/dependabot.yaml +16 -0
- todo_jira_sync-1.0.0/.github/workflows/ci.yaml +74 -0
- todo_jira_sync-1.0.0/.github/workflows/publish-docker.yaml +68 -0
- todo_jira_sync-1.0.0/.github/workflows/publish-pypi.yaml +40 -0
- todo_jira_sync-1.0.0/.gitignore +27 -0
- todo_jira_sync-1.0.0/.pre-commit-config.yaml +13 -0
- todo_jira_sync-1.0.0/AGENTS.md +68 -0
- todo_jira_sync-1.0.0/Dockerfile +44 -0
- todo_jira_sync-1.0.0/LICENSE +21 -0
- todo_jira_sync-1.0.0/Makefile +46 -0
- todo_jira_sync-1.0.0/PKG-INFO +202 -0
- todo_jira_sync-1.0.0/README.md +176 -0
- todo_jira_sync-1.0.0/docker-compose.yaml +21 -0
- todo_jira_sync-1.0.0/pyproject.toml +71 -0
- todo_jira_sync-1.0.0/setup.cfg +4 -0
- todo_jira_sync-1.0.0/tests/__init__.py +0 -0
- todo_jira_sync-1.0.0/tests/fake_jira.py +53 -0
- todo_jira_sync-1.0.0/tests/fixtures/sample.todo +15 -0
- todo_jira_sync-1.0.0/tests/test_sync.py +217 -0
- todo_jira_sync-1.0.0/tests/test_todo_format.py +128 -0
- todo_jira_sync-1.0.0/todo_jira_sync/__init__.py +1 -0
- todo_jira_sync-1.0.0/todo_jira_sync/cli.py +168 -0
- todo_jira_sync-1.0.0/todo_jira_sync/config.py +78 -0
- todo_jira_sync-1.0.0/todo_jira_sync/jira_client.py +207 -0
- todo_jira_sync-1.0.0/todo_jira_sync/models.py +104 -0
- todo_jira_sync-1.0.0/todo_jira_sync/settings.py +98 -0
- todo_jira_sync-1.0.0/todo_jira_sync/state.py +72 -0
- todo_jira_sync-1.0.0/todo_jira_sync/sync.py +351 -0
- todo_jira_sync-1.0.0/todo_jira_sync/todo_format.py +263 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/PKG-INFO +202 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/SOURCES.txt +35 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/dependency_links.txt +1 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/entry_points.txt +2 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/requires.txt +9 -0
- todo_jira_sync-1.0.0/todo_jira_sync.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# --- Connection -------------------------------------------------------------
|
|
2
|
+
# Jira Cloud base URL (no trailing path), e.g. https://acme.atlassian.net
|
|
3
|
+
JIRA_URL=https://your-domain.atlassian.net
|
|
4
|
+
|
|
5
|
+
# Auth mode: "basic" for Jira Cloud (email + API token),
|
|
6
|
+
# "bearer" for Server/Data Center personal access tokens.
|
|
7
|
+
JIRA_AUTH=basic
|
|
8
|
+
|
|
9
|
+
# For basic auth (Cloud): your account email + an API token from
|
|
10
|
+
# https://id.atlassian.com/manage-profile/security/api-tokens
|
|
11
|
+
JIRA_EMAIL=you@example.com
|
|
12
|
+
JIRA_API_TOKEN=your-api-token
|
|
13
|
+
|
|
14
|
+
# --- What to sync -----------------------------------------------------------
|
|
15
|
+
JIRA_PROJECT=WEB
|
|
16
|
+
TODO_FILE=todo.todo
|
|
17
|
+
|
|
18
|
+
# --- Behaviour --------------------------------------------------------------
|
|
19
|
+
# Conflict policy when BOTH sides changed the same field: jira | todo | skip
|
|
20
|
+
CONFLICT=jira
|
|
21
|
+
# Re-add issues to the todo file if they were deleted locally but still in Jira
|
|
22
|
+
PULL_LOCALLY_DELETED=false
|
|
23
|
+
|
|
24
|
+
# --- Optional: Jira issue-type names (match your project scheme) ------------
|
|
25
|
+
# TYPE_EPIC=Epic
|
|
26
|
+
# TYPE_STORY=Story
|
|
27
|
+
# TYPE_TASK=Task
|
|
28
|
+
# TYPE_SUBTASK=Sub-task
|
|
29
|
+
|
|
30
|
+
# --- Optional: Todo+ syntax overrides (comma-separated) ---------------------
|
|
31
|
+
# BOX_SYMBOLS=☐,[ ]
|
|
32
|
+
# DONE_SYMBOLS=✔,[x],[X],✓
|
|
33
|
+
# CANCELLED_SYMBOLS=✘,[-]
|
|
34
|
+
# INDENT_UNIT=
|
|
35
|
+
# CANCELLED_STATUS_NAMES=Cancelled,Canceled,Won't Do,Rejected
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: pip
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
|
|
8
|
+
- package-ecosystem: github-actions
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: weekly
|
|
12
|
+
|
|
13
|
+
- package-ecosystem: docker
|
|
14
|
+
directory: "/"
|
|
15
|
+
schedule:
|
|
16
|
+
interval: weekly
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
ref: ${{ github.head_ref || github.ref_name }}
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v7
|
|
21
|
+
|
|
22
|
+
- name: Create venv
|
|
23
|
+
run: uv venv --python 3.12
|
|
24
|
+
|
|
25
|
+
- name: Install
|
|
26
|
+
run: uv pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Auto-fix with ruff
|
|
29
|
+
run: |
|
|
30
|
+
uv run ruff check --fix .
|
|
31
|
+
uv run ruff format .
|
|
32
|
+
|
|
33
|
+
- name: Commit auto-fixes
|
|
34
|
+
run: |
|
|
35
|
+
if ! git diff --quiet; then
|
|
36
|
+
git config user.name "github-actions[bot]"
|
|
37
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
38
|
+
git commit -am "style: auto-fix ruff lint issues"
|
|
39
|
+
git push
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
test:
|
|
43
|
+
needs: lint
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
strategy:
|
|
46
|
+
fail-fast: false
|
|
47
|
+
matrix:
|
|
48
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v6
|
|
51
|
+
with:
|
|
52
|
+
ref: ${{ github.head_ref || github.ref_name }}
|
|
53
|
+
fetch-depth: 0 # setuptools-scm needs tags/history
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v7
|
|
57
|
+
|
|
58
|
+
# Create a venv that uses *exactly* the matrix interpreter. uv downloads
|
|
59
|
+
# the version if the runner doesn't have it. Every step below runs via
|
|
60
|
+
# `uv run`, so it executes inside this venv on the matrix Python.
|
|
61
|
+
- name: Create venv on Python ${{ matrix.python-version }}
|
|
62
|
+
run: uv venv --python ${{ matrix.python-version }}
|
|
63
|
+
|
|
64
|
+
- name: Install
|
|
65
|
+
run: uv pip install -e ".[dev]"
|
|
66
|
+
|
|
67
|
+
- name: Show interpreter under test
|
|
68
|
+
run: uv run python --version
|
|
69
|
+
|
|
70
|
+
- name: Type check
|
|
71
|
+
run: uv run mypy todo_jira_sync
|
|
72
|
+
|
|
73
|
+
- name: Test
|
|
74
|
+
run: uv run pytest
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Publish container
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
REGISTRY: ghcr.io
|
|
11
|
+
IMAGE_NAME: ${{ github.repository }}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
docker:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
packages: write # push to GitHub Container Registry
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0 # so setuptools-scm can resolve the version
|
|
23
|
+
|
|
24
|
+
- name: Resolve version
|
|
25
|
+
id: version
|
|
26
|
+
run: |
|
|
27
|
+
if command -v git >/dev/null && git describe --tags >/dev/null 2>&1; then
|
|
28
|
+
echo "value=$(git describe --tags --always | sed 's/^v//')" >> "$GITHUB_OUTPUT"
|
|
29
|
+
else
|
|
30
|
+
echo "value=0.0.0" >> "$GITHUB_OUTPUT"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- name: Set up QEMU
|
|
34
|
+
uses: docker/setup-qemu-action@v4
|
|
35
|
+
|
|
36
|
+
- name: Set up Buildx
|
|
37
|
+
uses: docker/setup-buildx-action@v4
|
|
38
|
+
|
|
39
|
+
- name: Log in to GHCR
|
|
40
|
+
uses: docker/login-action@v4
|
|
41
|
+
with:
|
|
42
|
+
registry: ${{ env.REGISTRY }}
|
|
43
|
+
username: ${{ github.actor }}
|
|
44
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
45
|
+
|
|
46
|
+
- name: Image metadata
|
|
47
|
+
id: meta
|
|
48
|
+
uses: docker/metadata-action@v5
|
|
49
|
+
with:
|
|
50
|
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
51
|
+
tags: |
|
|
52
|
+
type=ref,event=branch
|
|
53
|
+
type=semver,pattern={{version}}
|
|
54
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
55
|
+
type=sha
|
|
56
|
+
|
|
57
|
+
- name: Build and push (amd64 + arm64)
|
|
58
|
+
uses: docker/build-push-action@v6
|
|
59
|
+
with:
|
|
60
|
+
context: .
|
|
61
|
+
platforms: linux/amd64,linux/arm64
|
|
62
|
+
push: true
|
|
63
|
+
build-args: |
|
|
64
|
+
VERSION=${{ steps.version.outputs.value }}
|
|
65
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
66
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
67
|
+
cache-from: type=gha
|
|
68
|
+
cache-to: type=gha,mode=max
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0 # setuptools-scm needs the tag history
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v7
|
|
18
|
+
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: uv build
|
|
21
|
+
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: pypi
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write # OIDC for PyPI Trusted Publishing (no API token needed)
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
- name: Publish
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtual envs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
|
|
18
|
+
# Local config & secrets
|
|
19
|
+
.env
|
|
20
|
+
|
|
21
|
+
# Sync state sidecars (generated next to the todo file)
|
|
22
|
+
*.todojira.json
|
|
23
|
+
|
|
24
|
+
# Editors / OS
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
.DS_Store
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.5.7
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
9
|
+
rev: v1.10.1
|
|
10
|
+
hooks:
|
|
11
|
+
- id: mypy
|
|
12
|
+
additional_dependencies: [types-requests, pydantic-settings, typer]
|
|
13
|
+
files: ^todo_jira_sync/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents (and humans) working in this repo.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A bidirectional sync between a **Jira** project and a **Todo+** text file
|
|
8
|
+
(the format used by the `fabiospampinato/vscode-todo-plus` VS Code extension).
|
|
9
|
+
|
|
10
|
+
## Architecture (important)
|
|
11
|
+
|
|
12
|
+
The package is split so the **sync core has zero third-party dependencies** and
|
|
13
|
+
can be unit-tested with the standard library alone:
|
|
14
|
+
|
|
15
|
+
| Module | Depends on | Purpose |
|
|
16
|
+
|-------------------|-------------------|------------------------------------------------------|
|
|
17
|
+
| `config.py` | stdlib only | `FormatConfig` dataclass + conflict constants |
|
|
18
|
+
| `models.py` | stdlib only | `Node`, `JiraIssue`, `NodeKind`, `Status` |
|
|
19
|
+
| `todo_format.py` | stdlib only | `parse()`, `serialize()`, `resolve_jira_parents()` |
|
|
20
|
+
| `state.py` | stdlib only | JSON sidecar baseline for 3-way merge |
|
|
21
|
+
| `sync.py` | stdlib only | the 3-way merge engine: `sync(...) -> SyncResult` |
|
|
22
|
+
| `jira_client.py` | `requests` (lazy) | `RestJiraClient` against Jira Cloud REST v3 |
|
|
23
|
+
| `settings.py` | `pydantic-settings` | env/`.env` -> `FormatConfig` + credentials |
|
|
24
|
+
| `cli.py` | `typer` | `sync` / `push` / `pull` / `status` commands |
|
|
25
|
+
|
|
26
|
+
**Rule:** never import `requests`, `pydantic`, or `typer` from the core
|
|
27
|
+
modules (`config`, `models`, `todo_format`, `state`, `sync`). Keep them at the
|
|
28
|
+
edges (`jira_client`, `settings`, `cli`).
|
|
29
|
+
|
|
30
|
+
## Mapping rules
|
|
31
|
+
|
|
32
|
+
- Line ending with `:` at column 0 -> **Epic**
|
|
33
|
+
- Indented line ending with `:` -> **User Story**
|
|
34
|
+
- A Todo+ task (leading box/done/cancelled symbol) -> **Task**
|
|
35
|
+
- A task nested under another task -> **Sub-task**
|
|
36
|
+
- Identity anchor is the `@jira(KEY)` tag, written back into the file.
|
|
37
|
+
|
|
38
|
+
A node's Jira **parent** is its nearest enclosing container: Task -> enclosing
|
|
39
|
+
Story (or Epic if none); Story -> Epic; Sub-task -> the Task it sits under
|
|
40
|
+
(deeper nesting collapses onto that Task, since Jira forbids sub-task-of-sub-task).
|
|
41
|
+
|
|
42
|
+
## Jira API note
|
|
43
|
+
|
|
44
|
+
Uses `POST /rest/api/3/search/jql` with `nextPageToken` pagination. The legacy
|
|
45
|
+
`/rest/api/3/search` endpoint was removed by Atlassian and must **not** be
|
|
46
|
+
reintroduced. `fields` must be requested explicitly (the default is now `id`).
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pytest # via uv/pip-installed pytest
|
|
52
|
+
python tests/test_sync.py # also runs standalone, no pytest needed
|
|
53
|
+
python tests/test_todo_format.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The engine is tested against `tests/fake_jira.py`, an in-memory double — no
|
|
57
|
+
live Jira is required.
|
|
58
|
+
|
|
59
|
+
## Build, container & CI
|
|
60
|
+
|
|
61
|
+
uv is used everywhere (Makefile, Dockerfile, workflows). The image is a
|
|
62
|
+
multi-stage build: a `uv pip install` build stage and a slim runtime stage
|
|
63
|
+
whose entrypoint is the `todo-jira-sync` CLI. Versioning is `setuptools-scm`
|
|
64
|
+
(git tags); pass `--build-arg VERSION=...` to the Docker build since the build
|
|
65
|
+
context has no `.git`. GitHub Actions: `ci.yaml` (ruff + mypy + pytest, py3.10
|
|
66
|
+
–3.14), `publish-pypi.yaml` (sdist/wheel to PyPI via OIDC on `v*` tags) and
|
|
67
|
+
`publish-docker.yaml` (multi-arch image to GHCR on `v*` tags / manual dispatch).
|
|
68
|
+
Dependabot tracks pip, github-actions and docker.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
# Build stage: install the package (and its deps) into an isolated venv with uv.
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
FROM python:3.14-slim-bookworm AS build
|
|
7
|
+
|
|
8
|
+
# uv, copied from its official distroless image (pinned by the publish workflow).
|
|
9
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
10
|
+
|
|
11
|
+
ENV UV_COMPILE_BYTECODE=1 \
|
|
12
|
+
UV_LINK_MODE=copy \
|
|
13
|
+
UV_PYTHON_DOWNLOADS=never
|
|
14
|
+
|
|
15
|
+
# setuptools-scm derives the version from git. In CI we pass the resolved
|
|
16
|
+
# version as a build arg so the build does not need the .git directory.
|
|
17
|
+
ARG VERSION=0.0.0
|
|
18
|
+
ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
COPY pyproject.toml README.md ./
|
|
22
|
+
COPY todo_jira_sync ./todo_jira_sync
|
|
23
|
+
|
|
24
|
+
# Create the venv and install the project into it.
|
|
25
|
+
RUN uv venv /opt/venv \
|
|
26
|
+
&& VIRTUAL_ENV=/opt/venv uv pip install --no-cache .
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Runtime stage: copy only the venv onto a slim base. No compilers, no uv.
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
FROM python:3.14-slim-bookworm AS runtime
|
|
32
|
+
|
|
33
|
+
# Run as a non-root user.
|
|
34
|
+
RUN useradd --create-home --uid 1000 app
|
|
35
|
+
|
|
36
|
+
COPY --from=build /opt/venv /opt/venv
|
|
37
|
+
ENV PATH="/opt/venv/bin:$PATH"
|
|
38
|
+
|
|
39
|
+
# Work against a mounted directory holding the todo file and its .env.
|
|
40
|
+
WORKDIR /work
|
|
41
|
+
USER app
|
|
42
|
+
|
|
43
|
+
ENTRYPOINT ["todo-jira-sync"]
|
|
44
|
+
CMD ["--help"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Regis
|
|
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,46 @@
|
|
|
1
|
+
.PHONY: help install lint format typecheck test check run docker docker-run clean
|
|
2
|
+
|
|
3
|
+
help:
|
|
4
|
+
@echo "Targets:"
|
|
5
|
+
@echo " install Create the venv and install in editable mode (uv)"
|
|
6
|
+
@echo " lint Run ruff"
|
|
7
|
+
@echo " format Auto-fix with ruff"
|
|
8
|
+
@echo " typecheck Run mypy"
|
|
9
|
+
@echo " test Run the test suite (pytest)"
|
|
10
|
+
@echo " check lint + typecheck + test"
|
|
11
|
+
@echo " run Run the CLI (pass ARGS=...)"
|
|
12
|
+
@echo " docker Build the container image"
|
|
13
|
+
@echo " clean Remove caches and build artifacts"
|
|
14
|
+
|
|
15
|
+
install:
|
|
16
|
+
uv venv
|
|
17
|
+
uv pip install -e ".[dev]"
|
|
18
|
+
|
|
19
|
+
lint:
|
|
20
|
+
uv run ruff check .
|
|
21
|
+
|
|
22
|
+
format:
|
|
23
|
+
uv run ruff check --fix .
|
|
24
|
+
uv run ruff format .
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
uv run mypy todo_jira_sync
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
uv run pytest
|
|
31
|
+
|
|
32
|
+
check: lint typecheck test
|
|
33
|
+
|
|
34
|
+
run:
|
|
35
|
+
uv run todo-jira-sync $(ARGS)
|
|
36
|
+
|
|
37
|
+
VERSION ?= 0.0.0
|
|
38
|
+
docker:
|
|
39
|
+
docker build --build-arg VERSION=$(VERSION) -t todo-jira-sync:local .
|
|
40
|
+
|
|
41
|
+
docker-run:
|
|
42
|
+
docker run --rm --env-file .env -v "$(PWD):/work" todo-jira-sync:local $(ARGS)
|
|
43
|
+
|
|
44
|
+
clean:
|
|
45
|
+
rm -rf .pytest_cache .ruff_cache .mypy_cache build dist *.egg-info
|
|
46
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: todo-jira-sync
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bidirectionally sync a Jira project with a Todo+ (vscode-todo-plus) text file.
|
|
5
|
+
Author: Regis
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kalw/todo-jira-sync
|
|
8
|
+
Project-URL: Issues, https://github.com/kalw/todo-jira-sync/issues
|
|
9
|
+
Keywords: jira,todo,todo-plus,sync,taskpaper,vscode
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Requires-Dist: requests>=2.31
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.2
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
24
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# todo-jira-sync
|
|
28
|
+
|
|
29
|
+
Bidirectionally sync a **Jira** project with a **Todo+** plain-text file — the
|
|
30
|
+
format used by the [`vscode-todo-plus`](https://github.com/fabiospampinato/vscode-todo-plus)
|
|
31
|
+
extension. Edit your backlog as a flat, version-controllable text file in your
|
|
32
|
+
editor; run one command; Jira and the file converge.
|
|
33
|
+
|
|
34
|
+
Scaffolded from [`tedivm/robs_awesome_python_template`](https://github.com/tedivm/robs_awesome_python_template)
|
|
35
|
+
conventions (uv, `pyproject.toml`, Typer, Pydantic Settings, Ruff, mypy,
|
|
36
|
+
pytest, GitHub Actions).
|
|
37
|
+
|
|
38
|
+
## Mapping rules
|
|
39
|
+
|
|
40
|
+
Each non-blank, non-comment line maps to one Jira issue, by indentation and the
|
|
41
|
+
trailing colon:
|
|
42
|
+
|
|
43
|
+
| Todo+ line | Jira issue type |
|
|
44
|
+
|----------------------------------------------|-----------------|
|
|
45
|
+
| ends with `:`, **no** leading indent | **Epic** |
|
|
46
|
+
| ends with `:`, **indented** | **User Story** |
|
|
47
|
+
| a task (`☐` / `✔` / `✘` …) | **Task** |
|
|
48
|
+
| a task **nested under another task** | **Sub-task** |
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Authentication: ← Epic
|
|
52
|
+
Login flow: ← Story (parent: Authentication)
|
|
53
|
+
☐ Build the login form ← Task (parent: Login flow)
|
|
54
|
+
☐ Add OAuth providers ← Task (parent: Login flow)
|
|
55
|
+
☐ Google provider ← Sub-task (parent: Add OAuth providers)
|
|
56
|
+
☐ Password reset email ← Task (parent: Authentication)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A node's Jira parent is its **nearest enclosing container**: a Task takes the
|
|
60
|
+
Story it sits under, or the Epic if there is no enclosing Story; a Story takes
|
|
61
|
+
its Epic; a Sub-task takes the Task it sits under.
|
|
62
|
+
|
|
63
|
+
### Status mapping
|
|
64
|
+
|
|
65
|
+
| Todo+ symbol | Status | Jira category |
|
|
66
|
+
|-------------------------|---------------|----------------------|
|
|
67
|
+
| `☐` `[ ]` | To Do | *new* |
|
|
68
|
+
| `@started(...)` | In Progress | *indeterminate* |
|
|
69
|
+
| `✔` `✓` `[x]` | Done | *done* |
|
|
70
|
+
| `✘` `[-]` `@cancelled` | Cancelled | *done* + cancelled status name |
|
|
71
|
+
|
|
72
|
+
Jira has no "cancelled" status *category*; cancelled is detected by status
|
|
73
|
+
**name** (configurable via `CANCELLED_STATUS_NAMES`).
|
|
74
|
+
|
|
75
|
+
### How identity works
|
|
76
|
+
|
|
77
|
+
Each synced line gets a `@jira(KEY)` tag written back into the file (inserted
|
|
78
|
+
*before* the trailing colon for projects, so it still reads as a project):
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Authentication @jira(WEB-1):
|
|
82
|
+
☐ Build the login form @jira(WEB-3)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That tag is the stable anchor — rename a line freely and the link survives.
|
|
86
|
+
|
|
87
|
+
## Install
|
|
88
|
+
|
|
89
|
+
Requires Python ≥ 3.10. Using [uv](https://docs.astral.sh/uv/):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
uv venv
|
|
93
|
+
uv pip install -e ".[dev]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
or with pip:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m venv .venv && . .venv/bin/activate
|
|
100
|
+
pip install -e ".[dev]"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Configure
|
|
104
|
+
|
|
105
|
+
Copy `.env.example` to `.env` and fill in your details:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cp .env.example .env
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For Jira Cloud, create an API token at
|
|
112
|
+
<https://id.atlassian.com/manage-profile/security/api-tokens> and set
|
|
113
|
+
`JIRA_EMAIL` + `JIRA_API_TOKEN` with `JIRA_AUTH=basic`. For Server/Data Center,
|
|
114
|
+
use a personal access token with `JIRA_AUTH=bearer`.
|
|
115
|
+
|
|
116
|
+
## Use
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# See what would happen — touches nothing:
|
|
120
|
+
todo-jira-sync status --todo todo.todo --project WEB
|
|
121
|
+
|
|
122
|
+
# Full bidirectional sync:
|
|
123
|
+
todo-jira-sync sync --todo todo.todo --project WEB
|
|
124
|
+
|
|
125
|
+
# One-way only:
|
|
126
|
+
todo-jira-sync push # local file -> Jira (never edits the file)
|
|
127
|
+
todo-jira-sync pull # Jira -> local file (never creates/edits Jira)
|
|
128
|
+
|
|
129
|
+
# Conflict policy when both sides changed the same field:
|
|
130
|
+
todo-jira-sync sync --conflict jira # jira | todo | skip
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A JSON sidecar `todo.todo.todojira.json` is written next to your file. It is
|
|
134
|
+
the 3-way-merge baseline (the common ancestor) — keep it, but it need not be
|
|
135
|
+
committed (it is git-ignored by default).
|
|
136
|
+
|
|
137
|
+
## How sync decides (3-way merge)
|
|
138
|
+
|
|
139
|
+
For each issue the engine compares the **live file** and **live Jira** against
|
|
140
|
+
the **baseline** from the last run:
|
|
141
|
+
|
|
142
|
+
- only the file changed → **push** to Jira
|
|
143
|
+
- only Jira changed → **pull** into the file
|
|
144
|
+
- both changed the same field → **conflict**, resolved by `--conflict`
|
|
145
|
+
- new in file → **created** in Jira (parents first)
|
|
146
|
+
- new in Jira → **pulled** into the file (under the right parent)
|
|
147
|
+
- gone from Jira but known → kept locally and reported (never silently lost)
|
|
148
|
+
|
|
149
|
+
The engine **never deletes Jira issues** and **never deletes local lines**.
|
|
150
|
+
|
|
151
|
+
## Docker
|
|
152
|
+
|
|
153
|
+
A multi-stage, uv-based image is included. It builds the package into a slim
|
|
154
|
+
runtime image whose entrypoint is the CLI, running as a non-root user.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Build (VERSION is only needed because versioning comes from git tags):
|
|
158
|
+
docker build --build-arg VERSION=0.1.0 -t todo-jira-sync .
|
|
159
|
+
|
|
160
|
+
# Run against a working directory that holds your todo file and .env:
|
|
161
|
+
docker run --rm --env-file .env -v "$PWD:/work" todo-jira-sync \
|
|
162
|
+
sync --todo todo.todo --project WEB
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Or via Compose (put your `todo` file and `.env` in `./work`):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
docker compose run --rm sync status
|
|
169
|
+
docker compose run --rm sync sync --todo todo.todo --project WEB
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Prebuilt multi-arch images (amd64 + arm64) are published to GitHub Container
|
|
173
|
+
Registry by CI on version tags (and on demand via the workflow's manual
|
|
174
|
+
trigger).
|
|
175
|
+
|
|
176
|
+
## Releasing
|
|
177
|
+
|
|
178
|
+
Versioning is derived from git tags via `setuptools-scm`. Pushing a tag like
|
|
179
|
+
`v0.1.0` triggers two workflows: one builds the sdist/wheel and publishes to
|
|
180
|
+
PyPI (via OIDC Trusted Publishing — no API token stored), the other builds and
|
|
181
|
+
pushes the multi-arch container image. CI (`ci.yaml`) runs ruff, mypy and
|
|
182
|
+
pytest on a Python 3.10–3.14 matrix for every push and PR.
|
|
183
|
+
|
|
184
|
+
## Develop
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
make check # ruff + mypy + pytest
|
|
188
|
+
make test
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The sync core (`config`, `models`, `todo_format`, `state`, `sync`) is pure
|
|
192
|
+
standard library, so the tests run against an in-memory fake Jira with no
|
|
193
|
+
network and no live credentials. The two test files also run standalone:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
python tests/test_todo_format.py
|
|
197
|
+
python tests/test_sync.py
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT — see [LICENSE](LICENSE).
|