Skip to content

Commit 53c8192

Browse files
committed
ci: consolidate workflows into single uv-first CI (add wheelhouse, pre-commit, docs deploy, security)
1 parent 058155c commit 53c8192

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

.github/workflows/ci.yml

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
name: CI (uv)
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
name: Test / Lint / Typecheck (uv)
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Cache pip
22+
uses: actions/cache@v4
23+
with:
24+
path: |
25+
~/.cache/pip
26+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
27+
restore-keys: |
28+
${{ runner.os }}-pip-${{ matrix.python-version }}-
29+
30+
- name: Cache pip wheels & pre-commit
31+
uses: actions/cache@v4
32+
with:
33+
path: |
34+
~/.cache/pip/wheels
35+
.wheelhouse
36+
~/.cache/pre-commit
37+
key: ${{ runner.os }}-pip-wheels-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
38+
restore-keys: |
39+
${{ runner.os }}-pip-wheels-${{ matrix.python-version }}-
40+
41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v4
43+
with:
44+
enable-cache: true
45+
46+
- name: Set up Python ${{ matrix.python-version }}
47+
run: uv python install ${{ matrix.python-version }}
48+
49+
- name: Build wheelhouse for project and dev deps (via uv)
50+
run: |
51+
# Build wheels for the project and development extras into .wheelhouse
52+
uv pip wheel -w .wheelhouse --no-build-isolation "[dev]" || true
53+
54+
- name: Create venv (uv)
55+
run: |
56+
# create a fresh .venv using uv
57+
uv venv
58+
59+
- name: Install project dev dependencies into venv from wheelhouse
60+
run: |
61+
# Install using only the local wheels for reproducibility / speed
62+
uv pip install --no-index --find-links .wheelhouse "[dev]" || uv pip install --no-index --find-links .wheelhouse "[dev]"
63+
64+
- name: Lint (ruff)
65+
run: uv run ruff check .
66+
67+
- name: Check formatting with Black (via uv)
68+
run: uv run python -m black --check .
69+
70+
- name: Typecheck (mypy)
71+
run: uv run mypy python_project_deployment
72+
73+
- name: Run pre-commit hooks (all files) via uv
74+
run: |
75+
uv run pre-commit install
76+
uv run pre-commit run --all-files
77+
78+
- name: Tests (pytest)
79+
run: uv run pytest --cov --cov-report=xml --cov-report=html
80+
81+
- name: Security scan for dangerous APIs
82+
run: |
83+
set -euo pipefail
84+
if grep -R --line-number -E "\beval\(|\bexec\(|pickle\.loads|yaml\.load|subprocess\.(Popen|call)" python_project_deployment/ tests/ || true; then
85+
echo "⚠️ Potentially dangerous API usage detected. Please review before merging." >&2
86+
exit 2
87+
fi
88+
continue-on-error: true
89+
90+
- name: Upload coverage.xml
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: coverage-${{ matrix.python-version }}
94+
path: coverage.xml
95+
96+
- name: Upload coverage HTML
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: coverage-html-${{ matrix.python-version }}
100+
path: htmlcov
101+
102+
- name: Build Sphinx docs via uv
103+
run: |
104+
uv run python -m sphinx -b html docs docs/_build/html || true
105+
106+
- name: Upload docs artifact
107+
uses: actions/upload-artifact@v4
108+
with:
109+
name: docs-html
110+
path: docs/_build/html
111+
112+
security:
113+
name: Security Scan
114+
runs-on: ubuntu-latest
115+
env:
116+
SECURITY_FAIL_LEVEL: MEDIUM
117+
steps:
118+
- uses: actions/checkout@v4
119+
120+
- name: Install uv
121+
uses: astral-sh/setup-uv@v4
122+
with:
123+
enable-cache: true
124+
125+
- name: Set up Python 3.11
126+
run: uv python install 3.11
127+
128+
- name: Sync deps and install security tools
129+
run: |
130+
uv sync --all-extras
131+
uv pip install bandit safety
132+
133+
- name: Run Bandit and export SARIF
134+
run: |
135+
uv run bandit -r python_project_deployment/ -f json -o bandit-report.json
136+
uv run bandit -r python_project_deployment/ -f sarif -o bandit-report.sarif
137+
continue-on-error: true
138+
139+
- name: Upload Bandit SARIF to GitHub Code Scanning
140+
uses: github/codeql-action/upload-sarif@v3
141+
with:
142+
sarif_file: bandit-report.sarif
143+
continue-on-error: true
144+
145+
- name: Run Safety (fail on any vulnerability)
146+
run: |
147+
uv run safety check --json > safety-report.json || true
148+
python - <<'PY'
149+
import json,sys,os
150+
try:
151+
data=json.load(open('safety-report.json'))
152+
if data.get('vulnerabilities'):
153+
print('Safety reported vulnerabilities')
154+
sys.exit(2)
155+
print('No safety vulnerabilities')
156+
except Exception as e:
157+
print(f'Error parsing safety output: {e}')
158+
sys.exit(0)
159+
PY
160+
continue-on-error: true
161+
162+
- name: Apply Bandit threshold
163+
run: |
164+
python - <<'PY'
165+
import json,sys,os
166+
level=os.environ.get('SECURITY_FAIL_LEVEL','MEDIUM').upper()
167+
try:
168+
data=json.load(open('bandit-report.json'))
169+
sevs=[issue.get('issue_severity','') for issue in data.get('results',[])]
170+
if level=='NONE':
171+
print('SECURITY_FAIL_LEVEL=NONE: not failing on bandit issues')
172+
sys.exit(0)
173+
if level=='HIGH':
174+
if 'HIGH' in sevs:
175+
print('Failing on HIGH bandit issues')
176+
sys.exit(2)
177+
else:
178+
print('No HIGH bandit issues')
179+
sys.exit(0)
180+
if level=='MEDIUM':
181+
if any(s in ('HIGH','MEDIUM') for s in sevs):
182+
print('Failing on MEDIUM or HIGH bandit issues')
183+
sys.exit(2)
184+
else:
185+
print('No MEDIUM/HIGH bandit issues')
186+
sys.exit(0)
187+
print('Unknown SECURITY_FAIL_LEVEL:', level)
188+
sys.exit(1)
189+
except Exception as e:
190+
print(f'Error parsing bandit report: {e}')
191+
sys.exit(0)
192+
PY
193+
continue-on-error: true
194+
195+
- name: Upload security reports
196+
uses: actions/upload-artifact@v4
197+
with:
198+
name: security-reports
199+
path: |
200+
bandit-report.json
201+
bandit-report.sarif
202+
safety-report.json
203+
if: always()
204+
205+
deploy-docs:
206+
name: Publish docs to GitHub Pages
207+
runs-on: ubuntu-latest
208+
needs: test
209+
# Only deploy on pushes to main (avoid publishing from PRs)
210+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
211+
steps:
212+
- name: Checkout
213+
uses: actions/checkout@v4
214+
215+
- name: Download docs artifact
216+
uses: actions/download-artifact@v4
217+
with:
218+
name: docs-html
219+
path: docs/_build/html
220+
221+
- name: Upload pages artifact
222+
uses: actions/upload-pages-artifact@v1
223+
with:
224+
path: docs/_build/html
225+
226+
- name: Deploy to GitHub Pages
227+
uses: actions/deploy-pages@v1
228+
with: {}

0 commit comments

Comments
 (0)