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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,44 @@ To run the actions from your yaml template file run:

If you need more verbose output for debugging, define `JOFT_DEBUG=1` environment variable.

## Configuration

JOFT uses a TOML configuration file (`joft.config.toml`) to manage its settings. You can create this file by copying the provided default configuration file:

```bash
cp joft.config.toml.default joft.config.toml
```

The configuration file has the following sections:

### Jira Server Settings

```toml
[jira.server]
hostname = "<your jira server url>" # e.g., "https://your-company.atlassian.net"
pat_token = "<your jira pat token>" # Your Personal Access Token
```

### Logging Configuration

You can configure both console (stdout) and file logging:

```toml
[logging.stdout]
log_level = "INFO" # One of: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"

[logging.file]
log_level = "DEBUG" # One of: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
log_dir = "/path/to/logs" # Optional: Directory where log files will be stored
```

- Both stdout and file logging sections are optional
- Log files are automatically named with timestamp (format: `joft_YYYYMMDD_HHMMSS.log`)
- For file logging:
- `log_dir` is optional and defaults to the current working directory
- Different log levels can be set for stdout and file logging
- Each log entry includes timestamp, logger name, log level, and message

## Docs

Documentation can be found [here](docs/introduction.md).
9 changes: 8 additions & 1 deletion joft.config.toml.default
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
[jira.server]
hostname = "<your jira server url>"
pat_token = "<your jira pat token>"
pat_token = "<your jira pat token>"

[logging.stdout]
log_level = <str_log_level> # "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"

[logging.file]
log_level = <str_log_level> # "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
log_dir = "<path_to_log_dir>"
28 changes: 14 additions & 14 deletions joft/actions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import logging
from joft.logger import logger
from typing import Dict, Union, List, cast, Any

import jira
Expand Down Expand Up @@ -27,12 +27,12 @@ def create_ticket(
"""
joft.base.update_reference_pool(action.reference_data, reference_pool)
joft.base.apply_reference_pool_to_payload(reference_pool, action.fields)
logging.debug(f"Creating new ticket of type: {action.fields['issuetype']['name']}")
logging.debug(f"Payload:\n{action.fields}")
logger.debug(f"Creating new ticket of type: {action.fields['issuetype']['name']}")
logger.debug(f"Payload:\n{action.fields}")

new_issue: jira.Issue = jira_session.create_issue(action.fields)

logging.info(f"New Jira ticket created: {new_issue.permalink()}")
logger.info(f"New Jira ticket created: {new_issue.permalink()}")

if action.object_id:
reference_pool[action.object_id] = new_issue
Expand Down Expand Up @@ -67,11 +67,11 @@ def update_ticket(

ticket_to: jira.Issue = cast(jira.Issue, reference_pool[action.reference_id])

logging.debug(f"Updating ticket '{ticket_to.key}'")
logging.debug(f"Payload:\n{action.fields}")
logger.debug(f"Updating ticket '{ticket_to.key}'")
logger.debug(f"Payload:\n{action.fields}")
ticket_to.update(action.fields)

logging.info(f"Ticket '{ticket_to.key}' updated.")
logger.info(f"Ticket '{ticket_to.key}' updated.")

if action.object_id:
reference_pool[action.object_id] = ticket_to
Expand All @@ -95,10 +95,10 @@ def link_issues(
joft.base.update_reference_pool(action.reference_data, reference_pool)
joft.base.apply_reference_pool_to_payload(reference_pool, action.fields)

logging.info("Linking issues...")
logging.info(f"Link type: {action.fields['type']}")
logging.info(f"Linking From Issue: {action.fields['inward_issue']}")
logging.info(f"Linking To Issue: {action.fields['outward_issue']}")
logger.info("Linking issues...")
logger.info(f"Link type: {action.fields['type']}")
logger.info(f"Linking From Issue: {action.fields['inward_issue']}")
logger.info(f"Linking To Issue: {action.fields['outward_issue']}")

jira_session.create_issue_link(
action.fields["type"],
Expand Down Expand Up @@ -136,11 +136,11 @@ def transition_issue(

ticket_to: jira.Issue = cast(jira.Issue, reference_pool[action.reference_id])

logging.info(f"Transitioning issue '{ticket_to.key}'...")
logging.info(
logger.info(f"Transitioning issue '{ticket_to.key}'...")
logger.info(
f"Changing status from '{ticket_to.fields.status}' to '{action.transition}'"
)
logging.info(f"With comment: \n{action.comment}")
logger.info(f"With comment: \n{action.comment}")

jira_session.transition_issue(
ticket_to, action.transition, action.fields, action.comment
Expand Down
8 changes: 4 additions & 4 deletions joft/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import copy
import logging
from typing import Dict, Union, List, Any, cast

import jira
import jira.client
import tabulate

import joft.actions
from joft.logger import logger
import joft.models
import joft.utils

Expand Down Expand Up @@ -40,7 +40,7 @@ def load_and_validate_template(template_file_path: str) -> joft.models.JiraTempl
template: Dict[str, Any] = joft.utils.load_and_parse_yaml_file(template_file_path)
jira_template = joft.models.JiraTemplate(**template)

logging.info("Yaml file loaded...")
logger.info("Yaml file loaded...")
# we validate if the user entered unique object_ids for different actions in the
# template. Each object_id references a action or a trigger, which in turn references
# their results (a ticket or a results of a search)
Expand Down Expand Up @@ -113,7 +113,7 @@ def execute_template(template_file_path: str, jira_session: jira.JIRA) -> int:
trigger_result = search_issues(jira_template, jira_session)

if not trigger_result:
logging.info(
logger.info(
(
"No tickets found according to the provided jira query "
f"'{jira_template.jira_search.jql}'!"
Expand Down Expand Up @@ -194,7 +194,7 @@ def execute_actions(
reference_pool,
)
case _:
logging.warning(f"Unknown action type: {action.type}")
logger.warning(f"Unknown action type: {action.type}")


def execute_actions_per_trigger_ticket(
Expand Down
25 changes: 9 additions & 16 deletions joft/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import logging
import os
import sys
from typing import Dict, Any, Optional

Expand All @@ -8,12 +6,7 @@

import joft.base
import joft.utils


if os.getenv("JOFT_DEBUG"):
logging_level = logging.DEBUG
else:
logging_level = logging.WARNING
from joft.logger import configure_logger, logger


@click.group()
Expand All @@ -24,6 +17,8 @@ def main(ctx: click.Context, config: Optional[str] = None) -> None:
A CLI automation tool which interacts with a Jira instance and automates tasks.
"""
ctx.obj = joft.utils.load_toml_app_config(config_path=config)
if "logging" in ctx.obj:
configure_logger(logging_config=ctx.obj["logging"])


# TODO: refactor th CLI interface so it makes more sense
Expand All @@ -38,17 +33,16 @@ def validate(template: str) -> int:
@click.option("--template", help="File path to the template file.")
@click.pass_obj
def run(ctx: Dict[str, Dict[str, Any]], template: str) -> int:
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging_level)
logging.info(
logger.info(
f"Establishing session with jira server: {ctx['jira']['server']['hostname']}:"
)

jira_session = jira.JIRA(
ctx["jira"]["server"]["hostname"], token_auth=ctx["jira"]["server"]["pat_token"]
)

logging.info("Session established...")
logging.info(f"Executing Jira template: {template}")
logger.info("Session established...")
logger.info(f"Executing Jira template: {template}")

ret_code = joft.base.execute_template(template, jira_session)

Expand All @@ -59,16 +53,15 @@ def run(ctx: Dict[str, Dict[str, Any]], template: str) -> int:
@click.option("--template", help="File path to the template file.")
@click.pass_obj
def list_issues(ctx: Dict[str, Dict[str, Any]], template: str) -> None:
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging_level)
logging.info(
logger.info(
f"Establishing session with jira server: {ctx['jira']['server']['hostname']}:"
)

jira_session = jira.JIRA(
ctx["jira"]["server"]["hostname"], token_auth=ctx["jira"]["server"]["pat_token"]
)

logging.info("Session established...")
logging.info(f"Executing trigger from Jira template: {template}")
logger.info("Session established...")
logger.info(f"Executing trigger from Jira template: {template}")

print(joft.base.list_issues(template, jira_session))
59 changes: 59 additions & 0 deletions joft/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from datetime import datetime
import logging
import os
from typing import Dict, Any

if os.getenv("JOFT_DEBUG"):
log_level = logging.DEBUG
else:
log_level = logging.WARNING

logger = logging.getLogger("joft")
logger.setLevel(log_level)


def configure_logger(logging_config: Dict[str, Any]) -> None:
"""
Configure the logger for the application according to the logging configuration.

Args:
logging_config: The logging configuration.
"""

logger = logging.getLogger("joft")
# Clear existing handlers
logger.handlers.clear()

# Track minimum level across all handlers
min_level = logging.CRITICAL

if "stdout" in logging_config:
log_level = getattr(logging, logging_config["stdout"]["log_level"])
min_level = min(min_level, log_level)
stdout_handler = logging.StreamHandler()
stdout_handler.setLevel(log_level)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)

if "file" in logging_config:
log_level = getattr(logging, logging_config["file"]["log_level"])
min_level = min(min_level, log_level)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

# Use current working directory if log_dir is not specified
log_dir = logging_config["file"].get("log_dir", os.getcwd())
log_file = os.path.join(log_dir, f"joft_{timestamp}.log")

file_handler = logging.FileHandler(log_file)
file_handler.setLevel(log_level)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# Set logger to minimum level from handlers
logger.setLevel(min_level)
16 changes: 8 additions & 8 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,15 @@ def test_multiple_references_in_str_field() -> None:
assert mock_reference_pool["issue.key"] in mock_fields["summary"]


@unittest.mock.patch("logging.info")
@unittest.mock.patch("joft.base.logger")
@unittest.mock.patch("joft.utils.load_and_parse_yaml_file")
@unittest.mock.patch("joft.base.execute_actions_per_trigger_ticket")
@unittest.mock.patch("joft.base.execute_actions")
def test_execute_template_with_trigger(
mock_execute_actions,
mock_execute_actions_per_trigger_ticket,
mock_load_and_parse_yaml,
mock_log_info,
mock_logger,
) -> None:
"""Comprehensive test that loads the whole yaml template. Checks if the functions
returns correct CLI codes."""
Expand Down Expand Up @@ -302,18 +302,18 @@ def test_execute_template_with_trigger(
mock_execute_actions_per_trigger_ticket.assert_called_once_with(
trigger_result, jira_template, mock_jira_session
)
mock_log_info.assert_called_once_with("Yaml file loaded...")
mock_logger.info.assert_called_once_with("Yaml file loaded...")


@unittest.mock.patch("logging.info")
@unittest.mock.patch("joft.base.logger")
@unittest.mock.patch("joft.utils.load_and_parse_yaml_file")
@unittest.mock.patch("joft.base.execute_actions_per_trigger_ticket")
@unittest.mock.patch("joft.base.execute_actions")
def test_execute_template_exit_no_tickets(
mock_execute_actions,
mock_execute_actions_per_trigger_ticket,
mock_load_and_parse_yaml,
mock_log_info,
mock_logger,
) -> None:
"""If there are no tickets returned from a JQL query the program should exit
as soon as possible."""
Expand All @@ -337,9 +337,9 @@ def test_execute_template_exit_no_tickets(

assert mock_execute_actions_per_trigger_ticket.call_count == 0
assert mock_execute_actions.call_count == 0
assert mock_log_info.call_count == 2
assert mock_log_info.mock_calls[0].args[0] == "Yaml file loaded..."
assert jira_template.jira_search.jql in mock_log_info.mock_calls[1].args[0]
assert mock_logger.info.call_count == 2
assert mock_logger.info.mock_calls[0].args[0] == "Yaml file loaded..."
assert jira_template.jira_search.jql in mock_logger.info.mock_calls[1].args[0]


@unittest.mock.patch("joft.base.validate_uniqueness_of_object_ids")
Expand Down
Loading