Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,25 @@ THINGS_TAGS=['jira', 'work']
JIRA_TYPE_TAG=true
```

### Status Mapping
### Sprint Deadlines

Set task deadlines based on the end dates of Jira sprints. Only active and future sprints are considered.

```ini
JIRA_SPRINT_DEADLINES=true
```

### Status Mapping / Scheduling

Tickets can be scheduled using one of two scheduling modes: `status` or `sprint`.

#### Status Mode

In status mode, tickets are scheduled based soley on their Jira status. This is the default mode.

```ini
SCHEDULING_MODE=status
```

Configure how JIRA statuses map to Things scheduling:

Expand All @@ -134,9 +152,64 @@ ANYTIME_STATUS=['To Do', 'Open', 'New']
COMPLETED_STATUS=['Done', 'Closed', 'Resolved']
```

## Status Mapping Logic
### Missing Tickets

When tickets are deleted or reassigned in Jira, they may stop being returned in the JQL response.

To handle this, you can enable the following option to mark missing tickets as canceled in Things:

```ini
CANCEL_MISSING_TICKETS=true
```

If a cancelled ticket later reappears, it will be un-canceled.

### Multiple Projects

You can configure additional queries, each mapped to seperate Things projects

Additional projects are defined by adding `THINGS_PROJECT__<name>` and `JIRA_JQL_QUERY__<name>` entries to the config.

For example, to create separate projects for web and API tickets, you can add:

```ini
THINGS_PROJECT__API=API Tickets
JIRA_JQL_QUERY__API=assignee = currentUser() AND updated >= -14d AND project = API

THINGS_PROJECT__WEB=Web Tickets
JIRA_JQL_QUERY__WEB=assignee = currentUser() AND updated >= -14d AND project = WEB
```

Since there's a 1:1 mapping between Jira tickets and Things tasks, tickets can only be assigned to one project at a time. Because of this, these queries should be non-overlapping, both between each other and with the main JIRA_JQL_QUERY.

This can also be used to separate tickets in the active sprint from those in the backlog:

Configure the "main" project/query as the backlog and add a second project/query for the active sprint:

```ini
THINGS_PROJECT=Backlog
JIRA_JQL_QUERY=assignee = currentUser() AND updated >= -14d AND (Sprint IS null OR Sprint NOT IN openSprints())

THINGS_PROJECT__ACTIVE=Current Sprint
JIRA_JQL_QUERY__ACTIVE=assignee = currentUser() AND Sprint IN openSprints()
```

#### Sprint Mode

```ini
SCHEDULING_MODE=sprint
```

In `sprint` mode, tickets are scheduled based on their sprint and status:

- Tickets in an active sprint are scheduled to "Anytime"
- Tickets in past/future sprints are scheduled to "Someday"
- Any tickets with a status in `TODAY_STATUS` are scheduled to "Today"
- Tickets with a status in `COMPLETED_STATUS` are marked as completed in Things

## Status / Scheduling Logic

The app maps JIRA ticket statuses to Things 3 scheduling areas:
The app maps JIRA ticket statuses or sprints to Things 3 scheduling areas:

- **Today**: Urgent/active work (appears in Today view)
- **Anytime**: Ready to work on (appears in Anytime list)
Expand Down
57 changes: 53 additions & 4 deletions config.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
JIRA_BASE_URL=https://your-company.atlassian.net
JIRA_API_TOKEN=your_jira_api_token_here
JIRA_USER_EMAIL=your.email@company.com

# JQL Query to select Jira issues for syncing
JIRA_JQL_QUERY=assignee = currentUser() AND updated >= -14d

# If using Sprints, you may want to modify the query to ensure all items in the current sprint are included:
# JIRA_JQL_QUERY=assignee = currentUser() AND (updated >= -14d or Sprint IN openSprints())

# Things 3 Configuration - Required for Things sync
THINGS_AUTH_TOKEN=your_things_auth_token_here

Expand All @@ -15,15 +20,59 @@ THINGS_TAGS=["jira", "work"]
# Make sure these tags exist in Things first!
JIRA_TYPE_TAG=true

# Set task deadlines based on the end date of active/future Jira sprints
JIRA_SPRINT_DEADLINES=true

# Status Mapping - Configure how JIRA statuses map to Things scheduling

# Tickets with these statuses go to "Today" in Things
TODAY_STATUS=["In Progress", "Active", "Doing"]

# Tickets with these statuses go to "Anytime" in Things (default for unlisted statuses)
# Tickets with these statuses are marked as completed in Things
COMPLETED_STATUS=["Done", "Closed", "Resolved", "Completed"]

# Set the scheduling mode for Things tasks
#
# When set to "status", the lists below determine how tasks are scheduled
# between "Anytime" and "Someday". When set to "sprint", tasks in the active
# sprint are set to "Anytime" and all other tasks are set to "Someday".
SCHEDULING_MODE=status

# In "status" mode, tickets with these statuses go to "Anytime" in Things
# (default for unlisted statuses)
ANYTIME_STATUS=["To Do", "Open", "Ready", "Dev Ready"]

# Tickets with these statuses go to "Someday" in Things
# In "status" mode, tickets with these statuses go to "Someday" in Things
SOMEDAY_STATUS=["Backlog", "Future", "Product Backlog", "Icebox"]

# Tickets with these statuses are marked as completed in Things
COMPLETED_STATUS=["Done", "Closed", "Resolved", "Completed"]
# Optional: Configure additional queries to map to different Things projects
#
# Additional projects are defined by adding THINGS_PROJECT__<name> and JIRA_JQL_QUERY__<name>
#
# For example, to create separate projects for web and API tickets, you can add:
#
# THINGS_PROJECT__API=API Tickets
# JIRA_JQL_QUERY__API=assignee = currentUser() AND updated >= -14d AND project = API
#
# THINGS_PROJECT__WEB=Web Tickets
# JIRA_JQL_QUERY__WEB=assignee = currentUser() AND updated >= -14d AND project = WEB
#
# Since there's a 1:1 mapping between Jira tickets and Things tasks, tickets
# can only be assigned to one project at a time. Because of this, these queries
# should be non-overlapping, both between each other and with the main JIRA_JQL_QUERY.
#
# This can also be used to separate tickets in the active sprint from those in the backlog:
#
# Configure the "main" project/query as the backlog:
#
# THINGS_PROJECT=Backlog
# JIRA_JQL_QUERY=assignee = currentUser() AND updated >= -14d AND (Sprint IS null OR Sprint NOT IN openSprints())
#
# Add a second project for the active sprint:
#
# THINGS_PROJECT__ACTIVE=Current Sprint
# JIRA_JQL_QUERY__ACTIVE=assignee = currentUser() AND Sprint IN openSprints()

# Should missing tickets be marked as canceled in Things?
# For example, if a ticket is reassigned or deleted in Jira, it will not be returned in the JQL fetch.
CANCEL_MISSING_TICKETS=false
15 changes: 7 additions & 8 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,39 @@

@dataclass
class JiraConfig:
"""Configuration for JIRA connection and queries."""
"""Configuration for JIRA connection."""
base_url: str
api_token: str
user_email: str
jql_query: str = "project = DEMO AND status != Done ORDER BY created DESC"

@classmethod
def from_file(cls, filename="config"):
"""Load JIRA configuration from file."""
config = load_config_vars(filename)

# Validate required fields
required_fields = ['JIRA_BASE_URL', 'JIRA_API_TOKEN', 'JIRA_USER_EMAIL']
missing_fields = [field for field in required_fields if field not in config]
if missing_fields:
raise ValueError(f"Missing required JIRA configuration fields: {', '.join(missing_fields)}")



return cls(
base_url=config["JIRA_BASE_URL"],
api_token=config["JIRA_API_TOKEN"],
user_email=config["JIRA_USER_EMAIL"],
jql_query=config.get("JIRA_JQL_QUERY", "project = DEMO AND status != Done ORDER BY created DESC")
)

@dataclass
class DatabaseConfig:
"""Configuration for SQLite database."""
db_path: str = "jira_tasks.db"
db_path: str = "jira_tasks.db"

def load_config_vars(filename="config"):
"""Load configuration variables from a key=value file."""
if not os.path.exists(filename):
raise FileNotFoundError(f"Configuration file '{filename}' not found")

config = {}
try:
with open(filename, "r") as f:
Expand All @@ -55,5 +54,5 @@ def load_config_vars(filename="config"):
print(f"Warning: Malformed line {line_num} in {filename}: {line}")
except Exception as e:
raise RuntimeError(f"Error reading configuration file '{filename}': {e}")

return config
Loading