1+ name : Python
2+
3+ on :
4+ push :
5+ pull_request :
6+ workflow_dispatch :
7+
8+ permissions : {}
9+
10+ jobs :
11+ build :
12+ strategy :
13+ fail-fast : false
14+ matrix :
15+ job_id : ["Build"]
16+ defaults :
17+ run :
18+ working-directory : .
19+ name : Build AWS MCP Proxy
20+ runs-on : ubuntu-latest
21+ permissions :
22+ contents : read
23+ pull-requests : read
24+ security-events : write
25+ actions : read
26+ steps :
27+ - uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28+
29+ - name : Install uv
30+ uses : astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
31+
32+ - name : Set up Python
33+ uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
34+ with :
35+ python-version-file : " .python-version"
36+ # cache: uv (not supported)
37+
38+ - name : Install dependencies
39+ run : uv sync --frozen --all-extras --dev
40+
41+ - name : Run tests
42+ run : |
43+ if [ -d "tests" ]; then
44+ uv run --frozen pytest --cov --cov-branch --cov-report=term-missing --cov-report=xml:${{ matrix.package }}-coverage.xml
45+ else
46+ echo "No tests directory found, skipping tests"
47+ fi
48+
49+ # - name: Upload coverage reports to Codecov
50+ # uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 #v5.4.3
51+ # with:
52+ # token: ${{ secrets.CODECOV_TOKEN }}
53+ # files: ${{ matrix.package }}-coverage.xml
54+
55+ - name : Run pyright
56+ run : uv run --frozen pyright
57+
58+ - name : Run ruff format
59+ run : uv run --frozen ruff format .
60+
61+ - name : Run ruff check
62+ run : uv run --frozen ruff check .
63+
64+ - name : Build package
65+ run : uv build
66+
67+ - name : Upload distribution
68+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
69+ with :
70+ path : dist/
71+
72+ - name : Generate Software Bill of Materials (SBOM)
73+ run : |
74+ source .venv/bin/activate
75+ echo "Attempt to convert to proper UTF-8 files https://github.com/CycloneDX/cyclonedx-python/issues/868"
76+ find .venv -type f -path '*/*.dist-info/*' > .venv/FILES
77+ # because grep with xargs returns 123 have to do this the long and hard way...
78+ while IFS= read -r line; do
79+ (grep -s -q -axv '.*' $line &&
80+ if [[ "$(file -b --mime-encoding $line)" != "binary" ]]; then
81+ echo "illegal utf-8 characters in $line...converting...";
82+ iconv -f $(file -b --mime-encoding $line) -t utf-8 $line > $line.utf8;
83+ mv $line.utf8 $line;
84+ fi;
85+ ) || echo "good $line"
86+ done < .venv/FILES;
87+ uv tool run --from cyclonedx-bom==6.1.3 cyclonedx-py environment $VIRTUAL_ENV --PEP-639 --gather-license-texts --pyproject pyproject.toml --mc-type library --output-format JSON > sbom.json
88+ - name : Display SBOM
89+ run : |
90+ cat <<EOT |
91+ import re
92+ import json
93+ import importlib.metadata as metadata
94+
95+ def parse_bom(json_file):
96+ # Parse the JSON file
97+ with open(json_file, 'r') as file:
98+ data = json.load(file)
99+
100+ # Extract components
101+ components = []
102+ for component in data['components']:
103+ comp_info = {}
104+
105+ # Get name, version, description, and purl
106+ comp_info['name'] = component.get('name', 'Unknown')
107+ comp_info['version'] = component.get('version', 'Unknown')
108+ comp_info['description'] = component.get('description', 'Unknown')
109+ comp_info['purl'] = component.get('purl', 'Unknown')
110+
111+ # Get licenses
112+ comp_info['licenses'] = []
113+ licenses = component.get('licenses', [])
114+ for license in licenses:
115+ if license.get('license', {}).get('id'):
116+ comp_info['licenses'].append(license.get('license').get('id'))
117+ if len(comp_info['licenses']) == 0:
118+ comp_info['licenses'].append("No licenses")
119+
120+ # Extract additional information (copyright, etc.)
121+ copyright_info = extract_copyright_from_metadata(comp_info['name'])
122+ comp_info['copyright'] = copyright_info if copyright_info else "No copyright information"
123+
124+ components.append(comp_info)
125+
126+ return components
127+
128+ def extract_copyright_from_metadata(package_name):
129+ try:
130+ # Use importlib.metadata to retrieve metadata from the installed package
131+ dist = metadata.distribution(package_name)
132+ metadata_info = dist.metadata
133+
134+ # Extract relevant metadata
135+ copyright_info = []
136+ author = metadata_info.get('Author')
137+ author_email = metadata_info.get('Author-email')
138+ license_info = metadata_info.get('License')
139+
140+ if author:
141+ copyright_info.append(f"Author: {author}")
142+ if author_email:
143+ copyright_info.append(f"Author Email: {author_email}")
144+ if license_info:
145+ copyright_info.append(f"License: {license_info}")
146+
147+ # Check for classifiers or any extra metadata fields
148+ if 'Classifier' in metadata_info:
149+ for classifier in metadata_info.get_all('Classifier'):
150+ if 'copyright' in classifier.lower():
151+ copyright_info.append(classifier)
152+
153+ return ', '.join(copyright_info) if copyright_info else None
154+
155+ except metadata.PackageNotFoundError:
156+ return None
157+
158+
159+ def main():
160+ bom_file = 'sbom.json' # Replace with your BOM file path
161+ components = parse_bom(bom_file)
162+
163+ for component in components:
164+ print(f"Name: {component['name']}")
165+ print(f"Version: {component['version']}")
166+ print(f"Description: {component['description']}")
167+ print(f"PURL: {component['purl']}")
168+ print(f"Licenses: {', '.join(component['licenses'])}")
169+ print(f"Copyright: {component['copyright']}")
170+ print("-" * 40)
171+
172+ if __name__ == "__main__":
173+ main()
174+ EOT
175+ python -
176+
177+ - name : Upload Software Bill of Materials
178+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
179+ with :
180+ name : sbom-aws-mcp-proxy
181+ path : sbom.json
0 commit comments