From 55bb69ebe85f1f183538682c1cb7421321e702b6 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Tue, 21 Oct 2025 16:17:20 +0800 Subject: [PATCH 01/15] Updated files for 1.12.3 release --- CHANGELOG.MD | 7 +++++++ README.md | 8 ++++---- trcli/__init__.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 0749997..499b899 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -6,6 +6,13 @@ This project adheres to [Semantic Versioning](https://semver.org/). Version numb - **MINOR**: New features that are backward-compatible. - **PATCH**: Bug fixes or minor changes that do not affect backward compatibility. +## [1.12.3] + +_released 11-03-2025 + +### Added + - Improve instrumentation/analytics for all API requests + ## [1.12.2] _released 10-16-2025 diff --git a/README.md b/README.md index f192822..2bb0ab4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ trcli ``` You should get something like this: ``` -TestRail CLI v1.12.2 +TestRail CLI v1.12.3 Copyright 2025 Gurock Software GmbH - www.gurock.com Supported and loaded modules: - parse_junit: JUnit XML Files (& Similar) @@ -47,7 +47,7 @@ CLI general reference -------- ```shell $ trcli --help -TestRail CLI v1.12.1 +TestRail CLI v1.12.3 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli [OPTIONS] COMMAND [ARGS]... @@ -1094,7 +1094,7 @@ Options: ### Reference ```shell $ trcli add_run --help -TestRail CLI v1.12.1 +TestRail CLI v1.12.3 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli add_run [OPTIONS] @@ -1218,7 +1218,7 @@ providing you with a solid base of test cases, which you can further expand on T ### Reference ```shell $ trcli parse_openapi --help -TestRail CLI v1.12.1 +TestRail CLI v1.12.3 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli parse_openapi [OPTIONS] diff --git a/trcli/__init__.py b/trcli/__init__.py index 858085d..53d6006 100644 --- a/trcli/__init__.py +++ b/trcli/__init__.py @@ -1 +1 @@ -__version__ = "1.12.2" +__version__ = "1.12.3" From 378cb5efe95bed39032a9eb30bc4817f855e6928 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Tue, 21 Oct 2025 16:37:03 +0800 Subject: [PATCH 02/15] TRCLI-170 Added new header X-Uploader-Metadata for all API request in TRCLI --- trcli/api/api_client.py | 47 +++++++++++++++++++++++++++++-- trcli/api/project_based_client.py | 14 +++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/trcli/api/api_client.py b/trcli/api/api_client.py index 6a12538..251b06a 100644 --- a/trcli/api/api_client.py +++ b/trcli/api/api_client.py @@ -1,5 +1,9 @@ import json from pathlib import Path +import platform +import os +import base64 +import hashlib import requests from beartype.typing import Union, Callable, Dict, List @@ -49,7 +53,8 @@ def __init__( verify: bool = True, proxy: str = None, #added proxy params proxy_user: str = None, - noproxy: str = None, + noproxy: str = None, + uploader_metadata: str = None, ): self.username = "" self.password = "" @@ -62,7 +67,8 @@ def __init__( self.__validate_and_set_timeout(timeout) self.proxy = proxy self.proxy_user = proxy_user - self.noproxy = noproxy.split(',') if noproxy else [] + self.noproxy = noproxy.split(',') if noproxy else [] + self.uploader_metadata = uploader_metadata if not host_name.endswith("/"): host_name = host_name + "/" @@ -99,6 +105,7 @@ def __send_request(self, method: str, uri: str, payload: dict, files: Dict[str, auth = HTTPBasicAuth(username=self.username, password=password) headers = {"User-Agent": self.USER_AGENT} headers.update(self.__get_proxy_headers()) + headers.update(self.__get_uploader_metadata_headers()) if files is None and not as_form_data: headers["Content-Type"] = "application/json" verbose_log_message = "" @@ -209,6 +216,15 @@ def __get_proxy_headers(self) -> Dict[str, str]: return headers + def __get_uploader_metadata_headers(self) -> Dict[str, str]: + """ + Returns headers for uploader metadata. + """ + headers = {} + if self.uploader_metadata: + headers["X-Uploader-Metadata"] = self.uploader_metadata + return headers + def _get_proxies_for_request(self, url: str) -> Dict[str, str]: """ Returns the appropriate proxy dictionary for a given request URL. @@ -276,6 +292,33 @@ def __validate_and_set_timeout(self, timeout): ) self.timeout = DEFAULT_API_CALL_TIMEOUT + @staticmethod + def build_uploader_metadata(version: str, project_id: int = None) -> str: + """ + Build uploader metadata as base64-encoded JSON. + + :param version: Application version + :param project_id: Project ID (optional) + :returns: Base64-encoded metadata string + """ + user = os.getenv("USER_EMAIL", "unknown") + user_hash = hashlib.sha256(user.encode()).hexdigest()[:8] + + data = { + "app_name": "trcli", + "app_version": version, + "os": platform.system().lower(), + "arch": platform.machine(), + "run_mode": "ci" if os.getenv("CI") else "manual", + "container": os.path.exists("/.dockerenv"), + "user_hash": user_hash, + } + + if project_id is not None: + data["project_id"] = project_id + + return base64.b64encode(json.dumps(data).encode()).decode() + @staticmethod def format_request_for_vlog(method: str, url: str, payload: dict): return ( diff --git a/trcli/api/project_based_client.py b/trcli/api/project_based_client.py index fef7835..a7be0c2 100644 --- a/trcli/api/project_based_client.py +++ b/trcli/api/project_based_client.py @@ -6,6 +6,7 @@ from trcli.constants import ProjectErrors, FAULT_MAPPING, SuiteModes, PROMPT_MESSAGES from trcli.data_classes.data_parsers import MatchersParser from trcli.data_classes.dataclass_testrail import TestRailSuite +import trcli class ProjectBasedClient: @@ -36,6 +37,13 @@ def instantiate_api_client(self) -> APIClient: proxy = self.environment.proxy # Will be None if --proxy is not defined noproxy = self.environment.noproxy # Will be None if --noproxy is not defined proxy_user = self.environment.proxy_user + + # Generate uploader metadata + uploader_metadata = APIClient.build_uploader_metadata( + version=trcli.__version__, + project_id=getattr(self.environment, 'project_id', None) + ) + if self.environment.timeout: api_client = APIClient( self.environment.host, @@ -45,7 +53,8 @@ def instantiate_api_client(self) -> APIClient: verify=not self.environment.insecure, proxy=proxy, proxy_user=proxy_user, - noproxy=noproxy + noproxy=noproxy, + uploader_metadata=uploader_metadata ) else: api_client = APIClient( @@ -55,7 +64,8 @@ def instantiate_api_client(self) -> APIClient: verify=not self.environment.insecure, proxy=proxy, proxy_user=proxy_user, - noproxy=noproxy + noproxy=noproxy, + uploader_metadata=uploader_metadata ) api_client.username = self.environment.username api_client.password = self.environment.password From 91dea422033fb711b1b6b7d35955b03f8c1d26ad Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Fri, 24 Oct 2025 17:03:00 +0800 Subject: [PATCH 03/15] TRCLI-170 Updated final metadata contents to be passed to TestRail API --- trcli/api/api_client.py | 15 +++-------- trcli/api/project_based_client.py | 45 ++++++++++++------------------- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/trcli/api/api_client.py b/trcli/api/api_client.py index 251b06a..b3962fc 100644 --- a/trcli/api/api_client.py +++ b/trcli/api/api_client.py @@ -3,7 +3,6 @@ import platform import os import base64 -import hashlib import requests from beartype.typing import Union, Callable, Dict, List @@ -293,17 +292,13 @@ def __validate_and_set_timeout(self, timeout): self.timeout = DEFAULT_API_CALL_TIMEOUT @staticmethod - def build_uploader_metadata(version: str, project_id: int = None) -> str: + def build_uploader_metadata(version: str) -> str: """ Build uploader metadata as base64-encoded JSON. - + :param version: Application version - :param project_id: Project ID (optional) :returns: Base64-encoded metadata string """ - user = os.getenv("USER_EMAIL", "unknown") - user_hash = hashlib.sha256(user.encode()).hexdigest()[:8] - data = { "app_name": "trcli", "app_version": version, @@ -311,12 +306,8 @@ def build_uploader_metadata(version: str, project_id: int = None) -> str: "arch": platform.machine(), "run_mode": "ci" if os.getenv("CI") else "manual", "container": os.path.exists("/.dockerenv"), - "user_hash": user_hash, } - - if project_id is not None: - data["project_id"] = project_id - + return base64.b64encode(json.dumps(data).encode()).decode() @staticmethod diff --git a/trcli/api/project_based_client.py b/trcli/api/project_based_client.py index a7be0c2..3d8cd7e 100644 --- a/trcli/api/project_based_client.py +++ b/trcli/api/project_based_client.py @@ -37,36 +37,25 @@ def instantiate_api_client(self) -> APIClient: proxy = self.environment.proxy # Will be None if --proxy is not defined noproxy = self.environment.noproxy # Will be None if --noproxy is not defined proxy_user = self.environment.proxy_user - + # Generate uploader metadata - uploader_metadata = APIClient.build_uploader_metadata( - version=trcli.__version__, - project_id=getattr(self.environment, 'project_id', None) - ) - + uploader_metadata = APIClient.build_uploader_metadata(version=trcli.__version__) + + # Build client configuration + client_kwargs = { + "verbose_logging_function": verbose_logging_function, + "logging_function": logging_function, + "verify": not self.environment.insecure, + "proxy": proxy, + "proxy_user": proxy_user, + "noproxy": noproxy, + "uploader_metadata": uploader_metadata + } + if self.environment.timeout: - api_client = APIClient( - self.environment.host, - verbose_logging_function=verbose_logging_function, - logging_function=logging_function, - timeout=self.environment.timeout, - verify=not self.environment.insecure, - proxy=proxy, - proxy_user=proxy_user, - noproxy=noproxy, - uploader_metadata=uploader_metadata - ) - else: - api_client = APIClient( - self.environment.host, - logging_function=logging_function, - verbose_logging_function=verbose_logging_function, - verify=not self.environment.insecure, - proxy=proxy, - proxy_user=proxy_user, - noproxy=noproxy, - uploader_metadata=uploader_metadata - ) + client_kwargs["timeout"] = self.environment.timeout + + api_client = APIClient(self.environment.host, **client_kwargs) api_client.username = self.environment.username api_client.password = self.environment.password api_client.api_key = self.environment.key From a45484bcb5674e2707a12cd48cf7976ab223a588 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 15:19:02 +0800 Subject: [PATCH 04/15] TRCLI-170 Added validations for invalid host or API requests --- trcli/api/api_client.py | 6 +++++- trcli/api/api_request_handler.py | 6 ++++++ trcli/constants.py | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/trcli/api/api_client.py b/trcli/api/api_client.py index b3962fc..4b7c3a1 100644 --- a/trcli/api/api_client.py +++ b/trcli/api/api_client.py @@ -183,8 +183,12 @@ def __send_request(self, method: str, uri: str, payload: dict, files: Dict[str, response_text = response.json() error_message = response_text.get("error", "") except (JSONDecodeError, ValueError): + response_preview = response.content[:200].decode('utf-8', errors='ignore') response_text = str(response.content) - error_message = response.content + error_message = FAULT_MAPPING["invalid_json_response"].format( + status_code=status_code, + response_preview=response_preview + ) except AttributeError: error_message = "" verbose_log_message = ( diff --git a/trcli/api/api_request_handler.py b/trcli/api/api_request_handler.py index 687f5ea..3dcd196 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -977,6 +977,12 @@ def __get_all_entities(self, entity: str, link=None, entities=[]) -> Tuple[List[ # Endpoints without pagination (legacy) if isinstance(response.response_text, list): return response.response_text, response.error_message + # Check if response is a string (JSON parse failed) + if isinstance(response.response_text, str): + error_msg = FAULT_MAPPING["invalid_api_response"].format( + error_details=response.response_text[:200] + ) + return [], error_msg # Endpoints with pagination entities = entities + response.response_text[entity] if response.response_text["_links"]["next"] is not None: diff --git a/trcli/constants.py b/trcli/constants.py index f9541c7..0dc9fec 100644 --- a/trcli/constants.py +++ b/trcli/constants.py @@ -65,7 +65,11 @@ proxy_invalid_configuration= "The provided proxy configuration is invalid. Please check the proxy URL and format.", ssl_error_on_proxy= "SSL error encountered while using the HTTPS proxy. Please check the proxy's SSL certificate.", no_proxy_match_error= "The host {host} does not match any NO_PROXY rules. Ensure the correct domains or IP addresses are specified for bypassing the proxy.", - no_suites_found= "The project {project_id} does not have any suites." + no_suites_found= "The project {project_id} does not have any suites.", + invalid_json_response= "Received invalid response from TestRail server (HTTP {status_code}). " + "Please verify your TestRail host URL (-h) is correct and points to a valid TestRail instance. " + "Response preview: {response_preview}", + invalid_api_response= "Invalid response from TestRail API: {error_details}" ) COMMAND_FAULT_MAPPING = dict( From 12e9fbee9d8ea42b97b792fdf8e9d87af6c36333 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 16:31:34 +0800 Subject: [PATCH 05/15] TRCLI-TEST: Testing pre-commit hooks From 5628259d08012740a5418671819ab690bf0fb6e4 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 16:40:37 +0800 Subject: [PATCH 06/15] TRCLI-TEST: Testing pre-commit hooks --- .pre-commit-config.yaml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60df378..3c19fcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,29 @@ +# Pre-commit hooks for code quality, security, and policy enforcement +# Install: pip install pre-commit +# Setup: pre-commit install && pre-commit install --hook-type commit-msg +# Run manually: pre-commit run --all-files + repos: -- repo: https://github.com/psf/black - rev: stable + # Code formatting + - repo: https://github.com/psf/black + rev: 24.10.0 hooks: - - id: black - language_version: python3.8 \ No newline at end of file + - id: black + language_version: python3.10 + args: [--line-length=120] + + # Linting and code quality + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + args: [--max-line-length=120, --extend-ignore=E203,W503] + additional_dependencies: [flake8-docstrings, flake8-bugbear] + + # Security scanning + - repo: https://github.com/PyCQA/bandit + rev: 1.7.10 + hooks: + - id: bandit + args: [-c, .bandit, -r] + exclude: ^tests/ From 0e0586a0bcc4191858500af3c96baef7894ca365 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 16:48:01 +0800 Subject: [PATCH 07/15] TRCLI-TEST: Testing pre-commit hooks From b689c4b20bdd058a4c7538cd330b224658f3d2ed Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 16:55:54 +0800 Subject: [PATCH 08/15] TRCLI-TEST: Testing pre-commit hooks From f4fa3297317158f9abf348f12360da77a7a88428 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 27 Oct 2025 17:04:51 +0800 Subject: [PATCH 09/15] TRCLI-185 Added updates for enhanced development workflow --- .bandit | 8 + .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/workflows/pr-checklist.yml | 176 ++++++++++++ .github/workflows/pr-validation.yml | 186 +++++++++++++ .github/workflows/security-scan.yml | 203 ++++++++++++++ .pre-commit-config.yaml | 2 +- .pre-commit-hooks/commit-msg.py | 103 +++++++ DEVELOPMENT_WORKFLOW.md | 402 ++++++++++++++++++++++++++++ setup-dev-tools.sh | 49 ++++ 9 files changed, 1131 insertions(+), 4 deletions(-) create mode 100644 .bandit create mode 100644 .github/workflows/pr-checklist.yml create mode 100644 .github/workflows/pr-validation.yml create mode 100644 .github/workflows/security-scan.yml create mode 100755 .pre-commit-hooks/commit-msg.py create mode 100644 DEVELOPMENT_WORKFLOW.md create mode 100755 setup-dev-tools.sh diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..aa259f8 --- /dev/null +++ b/.bandit @@ -0,0 +1,8 @@ +# Bandit security scanner configuration (YAML format) +exclude_dirs: + - '/tests/' + - '/tests_e2e/' + - '/venv/' + - '/.venv/' + - '/build/' + - '/dist/' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9e86e68..c7ee1fc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -