Skip to content

Commit 28fcdd8

Browse files
Jira upload utility
1 parent 3c868fa commit 28fcdd8

File tree

6 files changed

+756
-11
lines changed

6 files changed

+756
-11
lines changed

Makefile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,22 @@ include scripts/init.mk
55

66
# ==============================================================================
77

8-
# NOTE: This project currently only uses this Makefile as part of CI/CD checks.
8+
# This allows the setup of Playwright by using a single command ready to use, and checks
9+
# if the user is in a virtual environment before running the setup.
10+
11+
.PHONY: check-venv
12+
check-venv: # Checks if in a Python venv / VIRTUALENV before installing a bunch of dependencies
13+
@python -c "import sys, os; \
14+
venv = (hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or 'VIRTUAL_ENV' in os.environ); \
15+
import sys; sys.exit(0 if venv else 1)"
16+
17+
setup-playwright: # Install Playwright and associated packages, and create local.env
18+
@echo "Checking for virtual environment..."
19+
$(MAKE) check-venv || (echo "ERROR: Not in a virtual environment, please create before running this command!"; exit 1)
20+
21+
@echo "Install Playwright"
22+
pip install -r requirements.txt
23+
playwright install
24+
25+
@echo "Setup local.env file"
26+
python setup_env_file.py
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Utility Guide: JiraConfluenceUtil
2+
3+
The `JiraConfluenceUtil` utility provides methods for interacting with Jira and Confluence, specifically for uploading Playwright test results and metadata to Jira tickets.
4+
5+
> NOTE: For most use cases, you should use the [jira_upload.py](../../jira_upload.py) script as outlined in the [README](../../README.md).
6+
> This guide is for developers who want to use the utility directly in custom workflows or scripts.
7+
8+
## Table of Contents
9+
10+
- [Utility Guide: JiraConfluenceUtil](#utility-guide-jiraconfluenceutil)
11+
- [Table of Contents](#table-of-contents)
12+
- [Using the JiraConfluenceUtil class](#using-the-jiraconfluenceutil-class)
13+
- [Required Environment Variables](#required-environment-variables)
14+
- [Initialise the class](#initialise-the-class)
15+
- [Public Methods](#public-methods)
16+
- [get\_issue\_data](#get_issue_data)
17+
- [get\_issue\_summary\_in\_issue\_data](#get_issue_summary_in_issue_data)
18+
- [check\_attachment\_exists\_in\_issue\_data](#check_attachment_exists_in_issue_data)
19+
- [is\_valid\_jira\_reference](#is_valid_jira_reference)
20+
- [determine\_jira\_reference\_local](#determine_jira_reference_local)
21+
- [get\_environment\_metadata\_if\_available](#get_environment_metadata_if_available)
22+
- [is\_file\_is\_less\_than\_jira\_file\_limit](#is_file_is_less_than_jira_file_limit)
23+
- [upload\_test\_results\_dir\_to\_jira](#upload_test_results_dir_to_jira)
24+
- [Example Usage](#example-usage)
25+
26+
## Using the JiraConfluenceUtil class
27+
28+
### Required Environment Variables
29+
30+
The following environment variables need to be set (in local.env if running locally) for any Jira-based actions:
31+
32+
- **JIRA_URL**: The Jira instance to upload to.
33+
- **JIRA_PROJECT_KEY**: The Jira project key that should be uploaded to.
34+
- **JIRA_API_KEY**: The API key to use to complete actions. Locally you should generate your own key, and use a bot in a pipeline/workflow.
35+
36+
The following environment variables are optional:
37+
38+
- **JIRA_TICKET_REFERENCE**: The Jira ticket to push to if set. If not, will attempt to derive the value from the git branch.
39+
40+
The following environment variables need to be set for any Confluence-based actions:
41+
42+
- **CONFLUENCE_URL**: The Confluence instance to upload to.
43+
- **CONFLUENCE_API_KEY**: The API key to use to complete actions. Locally you should generate your own key, and use a bot in a pipeline/workflow.
44+
45+
### Initialise the class
46+
47+
You can initialise the class by importing and creating an instance:
48+
49+
```python
50+
from utils.jira_confluence_util import JiraConfluenceUtil
51+
52+
util = JiraConfluenceUtil()
53+
```
54+
55+
You can also specify a custom results directory:
56+
57+
```python
58+
util = JiraConfluenceUtil(results_dir="path/to/results")
59+
```
60+
61+
## Public Methods
62+
63+
### get_issue_data
64+
65+
```python
66+
get_issue_data(ticket_id: str) -> dict | None
67+
```
68+
69+
Checks if a Jira issue exists and returns its data as a dictionary, or `None` if not found.
70+
71+
---
72+
73+
### get_issue_summary_in_issue_data
74+
75+
```python
76+
get_issue_summary_in_issue_data(issue_data: dict) -> str | None
77+
```
78+
79+
Returns a summary string for the given Jira issue data in the format "[Ticket]: [Summary Line]", or `None` if not available.
80+
81+
---
82+
83+
### check_attachment_exists_in_issue_data
84+
85+
```python
86+
check_attachment_exists_in_issue_data(issue_data: dict, filename: str) -> bool
87+
```
88+
89+
Checks if a Jira issue already has an attachment with the specified filename.
90+
91+
---
92+
93+
### is_valid_jira_reference
94+
95+
```python
96+
is_valid_jira_reference(ticket_id: str) -> bool
97+
```
98+
99+
Validates that the Jira ticket reference is in the expected format (e.g., `SCM-1234` or `BSS2-5678`).
100+
101+
---
102+
103+
### determine_jira_reference_local
104+
105+
```python
106+
determine_jira_reference_local() -> str
107+
```
108+
109+
Determines the Jira ticket reference from the current git branch or if `JIRA_TICKET_REFERENCE` has been set.
110+
111+
This is currently configured to search for the format `feature/[Jira Reference]`, so for example:
112+
113+
- `feature/TEST-1234` would return `TEST-1234`.
114+
- `feature/TEST-2345-feature-name` would return `TEST-2345`.
115+
116+
> NOTE: Depending on your projects branch naming strategy, you may want to modify this method to suit your needs
117+
> accordingly.
118+
119+
---
120+
121+
### get_environment_metadata_if_available
122+
123+
```python
124+
get_environment_metadata_if_available() -> str
125+
```
126+
127+
This is method designed to return metadata for the environment under test. In this project, it is a stub method
128+
designed to be overwritten.
129+
130+
> NOTE: You will need to populate this method with the code required to get the metadata for your environment.
131+
> It is heavily recommended that you populate `results.json` with the data required, and then read the file
132+
> using this method to extract the data required.
133+
134+
---
135+
136+
### is_file_is_less_than_jira_file_limit
137+
138+
```python
139+
is_file_is_less_than_jira_file_limit(file_path: Path) -> bool
140+
```
141+
142+
Checks if the file size is below the Jira upload limit (10MB).
143+
144+
---
145+
146+
### upload_test_results_dir_to_jira
147+
148+
```python
149+
upload_test_results_dir_to_jira(
150+
ticket_id: str,
151+
overwrite_files: bool = True,
152+
include_html: bool = True,
153+
include_trace_files: bool = True,
154+
include_screenshots: bool = True,
155+
include_csv: bool = True,
156+
include_env_metadata: bool = True,
157+
add_comment: bool = True,
158+
automatically_accept: bool = False,
159+
) -> None
160+
```
161+
162+
Uploads files from the results directory to the specified Jira ticket.
163+
Options allow you to control which file types are included, whether to overwrite existing files, add a comment, and auto-confirm the upload.
164+
165+
For any files over 10MB, they will **not** be uploaded using this method as they will exceed the default file size limit
166+
set for Jira.
167+
168+
Each of the following arguments relate to the following actions:
169+
170+
- `overwrite_files` = If the file already exists on Jira, it will overwrite the file and use the same name if True. If false, it'll generate a unique filename based on the datetime of the upload.
171+
- `include_html` = Will check for any `.html` files in the root of the `results_dir` provided and include them if under 10MB if True.
172+
- `include_trace_files` = Will check for any `.zip` files in subdirectories of the `results_dir` provided and include them if under 10MB if True, renaming the file to include the subdirectory name.
173+
- `include_screenshots` = Will check for any `.png` files in the rood directory `results_dir` provided and the `screenshot/` subdirectory and include them if under 10MB if True.
174+
- `include_csv` = Will check for any `.csv` files in the root of the `results_dir` provided and include them if under 10MB if True.
175+
- `include_env_metadata` = Will check for any environment metadata generated by `get_environment_metadata_if_available` and include it in the comment if True.
176+
- `add_comment` = Will add a comment to Jira summarizing all the attachments and environment metadata if True.
177+
- `automatically_accept` = Will bypass generating a terminal message that needs to be accepted and assume the answer was `y` if True.
178+
179+
---
180+
181+
## Example Usage
182+
183+
```python
184+
from utils.jira_confluence_util import JiraConfluenceUtil
185+
186+
util = JiraConfluenceUtil()
187+
ticket_id = util.determine_jira_reference_local()
188+
if util.is_valid_jira_reference(ticket_id):
189+
util.upload_test_results_dir_to_jira(
190+
ticket_id=ticket_id,
191+
overwrite_files=True,
192+
include_html=True,
193+
include_trace_files=True,
194+
include_screenshots=True,
195+
include_csv=True,
196+
include_env_metadata=True,
197+
add_comment=True,
198+
automatically_accept=False
199+
)
200+
```

jira_upload.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
This script allows for the uploading of files to Jira once a test run has been completed and the test-results/ directory
3+
has been populated. This script is designed to work locally (when building tests) and via a pipeline or workflow during
4+
CI/CD operations.
5+
6+
The following environment variables need to be set (in local.env if running locally) before this can upload any files:
7+
- JIRA_URL: The Jira instance to upload to.
8+
- JIRA_PROJECT_KEY: The Jira project key that should be uploaded to.
9+
- JIRA_API_KEY: The API key to use to complete actions. Locally you should generate your own key, and use a bot in a pipeline/workflow.
10+
11+
The following environment variables are optional:
12+
- JIRA_TICKET_REFERENCE: The Jira ticket to push to if set. If not, will attempt to derive the value from the git branch.
13+
14+
The script itself can be executed using the following command:
15+
python jira_upload.py
16+
17+
The following arguments are supported in addition:
18+
--jira-ref <Jira Reference> = The Jira ticket to upload to. Will take precedence over auto-deriving from branch name and the set environment variable.
19+
--results-dir <Directory> = The directory to point to. If not set, points to test-results/ in this directory.
20+
--no-html = Don't include HTML files in the upload.
21+
--no-trace = Don't include Trace files (.zip) in the upload.
22+
--no-csv = Don't include CSV files in the upload.
23+
--no-screenshots = Don't include screenshots (.png) in the upload.
24+
--no-comment = Don't add a Jira comment highlighting the results.
25+
--no-env-data = Don't include environment data in the Jira comment.
26+
--overwrite-files = If a filename exists on the ticket that matches those in the results directory, overwrite them.
27+
--auto-confirm = Will not ask if you want to proceed if set, and will assume that yes has been pressed.
28+
"""
29+
30+
import argparse
31+
import sys
32+
from utils.jira_confluence_util import JiraConfluenceUtil
33+
34+
35+
def upload_jira_files(args: argparse.Namespace) -> None:
36+
"""
37+
This checks the arguments passed in and calls the logic to upload the data from the test-results directory to
38+
Jira.
39+
"""
40+
try:
41+
jira_instance = JiraConfluenceUtil() if args.results_dir is None else JiraConfluenceUtil(results_dir=args.results_dir)
42+
43+
if args.jira_ref is not None:
44+
jira_ref = args.jira_ref if jira_instance.is_valid_jira_reference(args.jira_ref) else ""
45+
else:
46+
jira_ref = jira_instance.determine_jira_reference_local()
47+
48+
if not jira_ref:
49+
raise ValueError("ERROR: Cannot proceed due to invalid Jira reference")
50+
51+
jira_instance.upload_test_results_dir_to_jira(
52+
ticket_id=jira_ref,
53+
overwrite_files=args.overwrite_files,
54+
include_html=not args.no_html,
55+
include_trace_files=not args.no_trace,
56+
include_screenshots=not args.no_screenshots,
57+
include_csv=not args.no_csv,
58+
include_env_metadata=not args.no_env_data,
59+
add_comment=not args.no_comment,
60+
automatically_accept=args.auto_confirm
61+
)
62+
except Exception as e:
63+
print("An error has been encountered so exiting upload process")
64+
print(f"{e}")
65+
sys.exit(1)
66+
67+
68+
if __name__ == "__main__":
69+
parser = argparse.ArgumentParser(
70+
description="Upload test results from the test-results directory."
71+
)
72+
parser.add_argument("--jira-ref", type=str, help="Specify the Jira reference to upload to")
73+
parser.add_argument("--results-dir", type=str, help="Specify the results directory to upload from")
74+
parser.add_argument("--no-html", action="store_true", help="Don't include HTML files in upload")
75+
parser.add_argument("--no-trace", action="store_true", help="Don't include trace files")
76+
parser.add_argument("--no-csv", action="store_true", help="Don't include CSV files")
77+
parser.add_argument("--no-screenshots", action="store_true", help="Don't include screenshots")
78+
parser.add_argument("--no-comment", action="store_true", help="Don't include a comment")
79+
parser.add_argument("--no-env-data", action="store_true", help="Don't include environment metadata in comment")
80+
parser.add_argument("--overwrite-files", action="store_true", help="If files are already on the Jira ticket with the same name, overwrite them with this file.")
81+
parser.add_argument("--auto-confirm", action="store_true", help="Don't prompt to confirm actions before proceeding")
82+
args = parser.parse_args()
83+
upload_jira_files(args)

requirements.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
pytest-playwright>=0.7.0
1+
pytest-playwright>=0.7.1
22
pytest-html>=4.1.1
33
pytest-json-report>=1.5.0
44
pytest-playwright-axe>=4.10.3
5-
python-dotenv>=1.1.0
5+
python-dotenv>=1.1.1
6+
atlassian-python-api>=4.0.7
7+
GitPython>=3.1.45

setup_env_file.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,44 @@
88
keys required to run this project.
99
"""
1010

11-
import os
1211
from pathlib import Path
1312

14-
REQUIRED_KEYS = ["USER_PASS"]
15-
DEFAULT_LOCAL_ENV_PATH = Path(os.getcwd()) / 'local.env'
13+
REQUIRED_KEYS = [
14+
"# Authentication details",
15+
"USER_PASS",
16+
"",
17+
"# Jira / Confluence Configuration",
18+
"JIRA_URL",
19+
"JIRA_PROJECT_KEY",
20+
"JIRA_API_KEY",
21+
"JIRA_TICKET_REFERENCE",
22+
"CONFLUENCE_URL",
23+
"CONFLUENCE_API_KEY"
24+
]
25+
DEFAULT_LOCAL_ENV_PATH = Path(__file__).resolve().parent / "local.env"
26+
1627

1728
def create_env_file():
1829
"""
1930
Create a local.env file with the required keys.
2031
"""
21-
with open(DEFAULT_LOCAL_ENV_PATH, 'w') as f:
22-
f.write("# Use this file to populate secrets without committing them to the codebase (as this file is set in .gitignore).\n")
23-
f.write("# To retrieve values as part of your tests, use os.getenv('VARIABLE_NAME').\n")
24-
f.write("# Note: When running in a pipeline or workflow, you should pass these variables in at runtime.\n\n")
32+
with open(DEFAULT_LOCAL_ENV_PATH, "w") as f:
33+
f.write(
34+
"# Use this file to populate secrets without committing them to the codebase (as this file is set in .gitignore).\n"
35+
)
36+
f.write(
37+
"# To retrieve values as part of your tests, use os.getenv('VARIABLE_NAME').\n"
38+
)
39+
f.write(
40+
"# Note: When running in a pipeline or workflow, you should pass these variables in at runtime.\n\n"
41+
)
2542
for key in REQUIRED_KEYS:
26-
f.write(f'{key}=\n')
43+
if key.startswith("#"): # This is a comment
44+
f.write(f"{key}\n")
45+
elif key == "": # New line only
46+
f.write("\n")
47+
else: # Expected key/value pair
48+
f.write(f"{key}=\n")
2749

2850

2951
if __name__ == "__main__":

0 commit comments

Comments
 (0)