Skip to content

Commit e9e0091

Browse files
committed
initial commit
0 parents  commit e9e0091

9 files changed

+1655
-0
lines changed

.env_example

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
KETRYX_URL=
2+
KETRYX_PROJECT=
3+
KETRYX_API_KEY=
4+
KETRYX_VERSION=
5+
GITHUB_SHA=

README.md

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Ketryx Test Results Reporter
2+
3+
A Python utility for uploading and reporting test results and Software Bill of Materials (SBOM) to the Ketryx platform.
4+
5+
## Features
6+
7+
- Upload JUnit XML test results
8+
- Upload Cucumber JSON test results
9+
- Upload SBOM files (supports CycloneDX and SPDX formats)
10+
- Configurable build reporting via YAML configuration
11+
- Environment variable configuration support
12+
13+
## Prerequisites
14+
15+
- Python 3.6 or higher
16+
- pip (Python package installer)
17+
- Access to Ketryx platform (API key required)
18+
19+
## Installation
20+
21+
1. Clone this repository:
22+
```bash
23+
git clone [repository-url]
24+
cd [repository-name]
25+
```
26+
27+
2. Create and activate a virtual environment:
28+
```bash
29+
python -m venv venv
30+
31+
# On Windows
32+
.\venv\Scripts\activate
33+
34+
# On Unix or MacOS
35+
source venv/bin/activate
36+
```
37+
38+
3. Install the required packages:
39+
```bash
40+
pip install -r requirements.txt
41+
```
42+
43+
## Configuration
44+
45+
### Environment Variables
46+
47+
Create a `.env` file in the project root directory with the following variables:
48+
49+
```ini
50+
KETRYX_URL=https://your-ketryx-instance.com
51+
KETRYX_PROJECT=your-project-name
52+
KETRYX_API_KEY=your-api-key
53+
KETRYX_VERSION=your-version
54+
GITHUB_SHA=your-commit-sha # Optional, used if KETRYX_VERSION is not set
55+
```
56+
57+
### Build Configuration
58+
59+
Create a YAML configuration file (e.g., `build-config.yaml`) to specify your builds:
60+
61+
```yaml
62+
builds:
63+
- name: "Unit Tests"
64+
type: "test-results"
65+
artifacts:
66+
junit:
67+
- "test-results/*.xml"
68+
cucumber:
69+
- "cucumber-results/*.json"
70+
71+
- name: "SBOM Analysis"
72+
type: "sbom"
73+
artifacts:
74+
- file: "sbom/cyclonedx.json"
75+
type: "cyclonedx"
76+
- file: "sbom/spdx.json"
77+
type: "spdx"
78+
```
79+
80+
## Usage
81+
82+
Run the script with your configuration file:
83+
84+
```bash
85+
python build_api_reporter.py build-config.yaml
86+
```
87+
88+
## Build Types
89+
90+
The tool supports two types of builds:
91+
92+
### Test Results
93+
- Supports JUnit XML and Cucumber JSON formats
94+
- Can specify multiple file patterns using glob syntax
95+
- Both formats can be used in the same build
96+
97+
### SBOM
98+
- Supports CycloneDX and SPDX formats
99+
- Each SBOM file needs to specify its type
100+
- Multiple SBOM files can be uploaded in a single build
101+
102+
## Error Handling
103+
104+
The script will:
105+
- Validate the presence of all required environment variables
106+
- Check for the existence of all specified files before uploading
107+
- Exit with a non-zero status code if any required files are missing
108+
- Display detailed error messages for troubleshooting
109+
110+
## Example Output
111+
112+
Successful execution:
113+
```
114+
Reported Unit Tests to Ketryx: build-123
115+
Reported SBOM Analysis to Ketryx: build-124
116+
```
117+
118+
Missing files error:
119+
```
120+
Missing files:
121+
- JUnit file not found: test-results/*.xml
122+
- SBOM file not found: sbom/cyclonedx.json
123+
```
124+
125+
## Notes
126+
127+
- All file paths in the configuration YAML are relative to the working directory
128+
- The script supports glob patterns for file paths
129+
- If KETRYX_VERSION is not specified, GITHUB_SHA will be used as the commit reference
130+
- API responses include build IDs which can be used to track builds in the Ketryx platform

build_api_reporter.py

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import glob
4+
import json
5+
import requests
6+
import subprocess
7+
from pathlib import Path
8+
from typing import List, Dict, Optional, Union
9+
from dataclasses import dataclass
10+
import xml.etree.ElementTree as ET
11+
import yaml
12+
from dotenv import load_dotenv
13+
import argparse
14+
15+
16+
17+
@dataclass
18+
class KetryxConfig:
19+
ketryx_url: str
20+
project: str
21+
api_key: str
22+
commit_sha: Optional[str]
23+
version: Optional[str]
24+
25+
26+
class KetryxReporter:
27+
def __init__(self, config: KetryxConfig):
28+
self.config = config
29+
self.base_url = config.ketryx_url.rstrip('/')
30+
self.headers = {
31+
'Authorization': f'Bearer {config.api_key}',
32+
'Content-Type': 'application/json'
33+
}
34+
35+
def upload_artifact(self, file_path: str, content_type: str = 'application/octet-stream') -> str:
36+
"""Upload a single artifact file to Ketryx."""
37+
url = f"{self.base_url}/api/v1/build-artifacts"
38+
params = {'project': self.config.project}
39+
40+
with open(file_path, 'rb') as f:
41+
files = {'file': (os.path.basename(file_path), f, content_type)}
42+
response = requests.post(url, params=params, headers={'Authorization': self.headers['Authorization']}, files=files)
43+
44+
if response.status_code != 200:
45+
raise Exception(f"Failed to upload artifact: {response.text}")
46+
47+
return response.json()['id']
48+
49+
def upload_test_results(self, junit_paths: List[str], cucumber_paths: List[str]) -> List[Dict]:
50+
"""Upload all test results and return artifact data."""
51+
artifacts = []
52+
53+
# Upload JUnit XML results
54+
for pattern in junit_paths:
55+
for file_path in glob.glob(pattern):
56+
artifact_id = self.upload_artifact(file_path, 'application/xml')
57+
artifacts.append({
58+
'id': artifact_id,
59+
'type': 'junit-xml'
60+
})
61+
62+
# Upload Cucumber JSON results
63+
for pattern in cucumber_paths:
64+
for file_path in glob.glob(pattern):
65+
artifact_id = self.upload_artifact(file_path, 'application/json')
66+
artifacts.append({
67+
'id': artifact_id,
68+
'type': 'cucumber-json'
69+
})
70+
71+
return artifacts
72+
73+
def upload_sbom(self, sbom_paths: List[str], sbom_type: str) -> List[Dict]:
74+
"""Upload SBOM files.
75+
76+
Args:
77+
sbom_paths: List of file paths to SBOM files
78+
sbom_type: Type of SBOM (e.g., 'cyclonedx', 'spdx')
79+
"""
80+
artifacts = []
81+
82+
for pattern in sbom_paths:
83+
for file_path in glob.glob(pattern):
84+
artifact_id = self.upload_artifact(file_path, 'application/json')
85+
artifacts.append({
86+
'id': artifact_id,
87+
'type': f'{sbom_type}-json'
88+
})
89+
90+
return artifacts
91+
92+
def report_build(self, build_name: str, artifacts: List[Dict]) -> Dict:
93+
"""Report build data to Ketryx."""
94+
url = f"{self.base_url}/api/v1/builds"
95+
96+
data = {
97+
'project': self.config.project,
98+
'buildName': build_name,
99+
'artifacts': artifacts,
100+
'sourceUrl': os.getenv('GITHUB_SERVER_URL', ''),
101+
'repositoryUrls': [f"{os.getenv('GITHUB_SERVER_URL', '')}/{os.getenv('GITHUB_REPOSITORY', '')}"],
102+
}
103+
104+
# If version is specified, it takes precedence over commitSha
105+
if self.config.version:
106+
data['version'] = self.config.version
107+
elif self.config.commit_sha:
108+
data['commitSha'] = self.config.commit_sha
109+
110+
response = requests.post(url, headers=self.headers, json=data)
111+
if response.status_code != 200:
112+
raise Exception(f"Failed to report build: {response.text}")
113+
114+
return response.json()
115+
116+
def process_build_config(config_file: str) -> List[Dict]:
117+
"""Process the YAML config file and return build artifacts."""
118+
with open(config_file, 'r') as f:
119+
build_config = yaml.safe_load(f)
120+
121+
if not build_config or 'builds' not in build_config:
122+
raise ValueError("Invalid config file: missing 'builds' section")
123+
124+
# Collect all missing files
125+
missing_files = []
126+
127+
for build in build_config['builds']:
128+
if build['type'] == 'test-results':
129+
for junit_path in build['artifacts'].get('junit', []):
130+
if not glob.glob(junit_path):
131+
missing_files.append(f"JUnit file not found: {junit_path}")
132+
for cucumber_path in build['artifacts'].get('cucumber', []):
133+
if not glob.glob(cucumber_path):
134+
missing_files.append(f"Cucumber file not found: {cucumber_path}")
135+
elif build['type'] == 'sbom':
136+
for artifact in build['artifacts']:
137+
if not glob.glob(artifact['file']):
138+
missing_files.append(f"SBOM file not found: {artifact['file']}")
139+
140+
if missing_files:
141+
print("\nMissing files:")
142+
for file in missing_files:
143+
print(f" - {file}")
144+
import sys; sys.exit(1)
145+
146+
return build_config['builds']
147+
148+
def main():
149+
# Add argument parser
150+
parser = argparse.ArgumentParser(description='Process build configuration and report to Ketryx')
151+
parser.add_argument('config_file', help='Path to the YAML configuration file')
152+
args = parser.parse_args()
153+
154+
# Load environment variables from .env file
155+
load_dotenv()
156+
157+
# Configuration from environment variables
158+
config = KetryxConfig(
159+
ketryx_url=os.getenv('KETRYX_URL'),
160+
project=os.getenv('KETRYX_PROJECT'),
161+
api_key=os.getenv('KETRYX_API_KEY'),
162+
version=os.getenv('KETRYX_VERSION'),
163+
commit_sha=os.getenv('GITHUB_SHA')
164+
)
165+
166+
if not all([config.project, config.api_key, config.version]):
167+
raise ValueError("Missing required environment variables")
168+
169+
reporter = KetryxReporter(config)
170+
171+
# Run tests and collect results
172+
try:
173+
builds = process_build_config(args.config_file)
174+
175+
for build in builds:
176+
if build['type'] == 'test-results':
177+
artifacts = reporter.upload_test_results(
178+
junit_paths=build['artifacts'].get('junit', []),
179+
cucumber_paths=build['artifacts'].get('cucumber', [])
180+
)
181+
elif build['type'] == 'sbom':
182+
for artifact in build['artifacts']:
183+
artifacts = reporter.upload_sbom(
184+
sbom_paths=[artifact['file']],
185+
sbom_type=artifact['type']
186+
)
187+
else:
188+
print(f"Warning: Unknown build type '{build['type']}'")
189+
continue
190+
191+
build_result = reporter.report_build(build['name'], artifacts)
192+
print(f"Reported {build['name']} to Ketryx: {build_result.get('buildId')}")
193+
194+
except Exception as e:
195+
print(f"Error: {e}")
196+
raise
197+
198+
if __name__ == '__main__':
199+
main()

example_config.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# ketryx-config.yml
2+
builds:
3+
- name: sbom
4+
type: sbom
5+
artifacts:
6+
- type: cyclonedx
7+
file: examples/example_sbom.cdx.json
8+
- name: js-build
9+
type: test-results
10+
artifacts:
11+
junit:
12+
- examples/example_jest_report_junit.xml
13+
cucumber:
14+
- examples/example_cucumber_report.json
15+
16+
- name: java-build
17+
type: test-results
18+
artifacts:
19+
junit:
20+
- examples/example_report_junit.xml
21+
cucumber: []

0 commit comments

Comments
 (0)