From 295b71289a098579a8d04e936f4b591b8dd8ff88 Mon Sep 17 00:00:00 2001 From: Anders Roxell Date: Sat, 27 Sep 2025 22:27:57 +0200 Subject: [PATCH 1/2] jiralogin: improve authentication error handling Add clear, user-friendly error messages when Jira returns HTML instead of JSON. This typically happens due to authentication failures, CAPTCHA requirements, or expired sessions. The new error handling detects the specific issue type and provides actionable guidance to users. Fixes the cryptic JSONDecodeError that users were seeing. Reported-by: Linus Walleij Signed-off-by: Anders Roxell --- jipdate/jiralogin.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/jipdate/jiralogin.py b/jipdate/jiralogin.py index c4b7217..184370d 100644 --- a/jipdate/jiralogin.py +++ b/jipdate/jiralogin.py @@ -6,6 +6,26 @@ from jipdate import cfg from jira import JIRA from jira import JIRAError +from requests.exceptions import JSONDecodeError + + +def _get_auth_error_message(response_text, url): + """Return a user-friendly authentication error message.""" + if not response_text: + hint = "Empty response - check network connectivity" + elif any(word in response_text.lower() for word in ["login", "sign in", "captcha"]): + hint = "Authentication required - complete web login/CAPTCHA first" + elif any(word in response_text.lower() for word in ["forbidden", "access denied"]): + hint = "Access denied - check credentials and permissions" + elif any(word in response_text.lower() for word in ["maintenance", "unavailable"]): + hint = "Server maintenance - try again later" + else: + hint = "Received HTML instead of JSON - likely authentication issue" + + return ( + f"Authentication failed: {hint}\n\n" + f"To fix: Open {url} in browser, logout/login, complete any CAPTCHA, then retry." + ) def get_username_from_config(): @@ -135,15 +155,12 @@ def get_jira_instance(use_test_server): ), username, ) + except JSONDecodeError as e: + log.error(_get_auth_error_message(getattr(e, "doc", ""), url)) + sys.exit(os.EX_NOPERM) except JIRAError as e: - if e.text.find("CAPTCHA_CHALLENGE") != -1: - log.error( - "Captcha verification has been triggered by " - "JIRA - please go to JIRA using your web " - "browser, log out of JIRA, log back in " - "entering the captcha; after that is done, " - "please re-run the script" - ) + if "CAPTCHA_CHALLENGE" in e.text: + log.error(_get_auth_error_message("captcha", url)) sys.exit(os.EX_NOPERM) else: raise From 60b804815ba5fab0d4b9196bd55b78ad71618250 Mon Sep 17 00:00:00 2001 From: Anders Roxell Date: Mon, 29 Sep 2025 11:24:05 +0200 Subject: [PATCH 2/2] cfg: add YAML configuration validation Add validation function that checks .jipdate.yaml file structure to ensure proper YAML formatting and correct nesting of configuration values. The validation runs automatically during config initialization and exits with descriptive error messages if validation fails, helping users quickly identify and fix configuration problems. Suggested-by: Linus Walleij Signed-off-by: Anders Roxell --- jipdate/cfg.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/jipdate/cfg.py b/jipdate/cfg.py index 8a36473..86d6e65 100644 --- a/jipdate/cfg.py +++ b/jipdate/cfg.py @@ -93,6 +93,56 @@ def get_config_file(): return config_path + "/" + config_filename +def validate_yaml_structure(config): + errors = [] + server_fields = ["url", "token"] + expected_top_level = [ + "version", + "server", + "test_server", + "comments", + "header", + "use_combined_issue_header", + "separator", + "text-editor", + ] + + def check_server_section(section_name): + if section_name in config: + section = config[section_name] + if not isinstance(section, dict): + errors.append( + f"'{section_name}' must be a dictionary/mapping with nested fields" + ) + elif not set(section.keys()).intersection(server_fields): + errors.append( + f"'{section_name}' section exists but contains no expected fields - they should be indented under '{section_name}:'" + ) + + check_server_section("server") + check_server_section("test_server") + + for field in server_fields: + if field in config: + if "server" not in config and "test_server" not in config: + errors.append( + f"Found '{field}' at root level - it should be indented under 'server:' or 'test_server:'" + ) + elif "server" in config: + errors.append( + f"Found '{field}' at root level - it should be indented under 'server:'" + ) + + config_keys = set(config.keys()) if config else set() + unexpected_keys = config_keys - set(expected_top_level) - set(server_fields) + + for field_name in ["comments", "header"]: + if field_name in config and not isinstance(config[field_name], list): + errors.append(f"'{field_name}' should be a list") + + return errors + + def get_server(use_test_server=False): # Get Jira Server details. Check first if using the test server # then try user config file, then default from cfg.py @@ -105,9 +155,6 @@ def get_server(use_test_server=False): def initiate_config(): - """Reads the config file (yaml format) and returns the sets the global - instance. - """ global yml_config global config_file @@ -118,3 +165,10 @@ def initiate_config(): log.debug("Using config file: %s" % config_file) with open(config_file, "r") as yml: yml_config = yaml.load(yml, Loader=yaml.FullLoader) + + validation_errors = validate_yaml_structure(yml_config) + if validation_errors: + log.error(f"Configuration validation failed for {config_file}:") + for error in validation_errors: + log.error(f" - {error}") + sys.exit(1)