66 pull_request :
77 branches : [ main ]
88
9+ # Cancel redundant runs per-branch/PR
10+ concurrency :
11+ group : ci-${{ github.workflow }}-${{ github.ref }}
12+ cancel-in-progress : true
13+
14+ # Least-privilege for the jobs below
15+ permissions :
16+ contents : read
17+
918jobs :
1019 test :
1120 name : Test / Lint / Typecheck (uv)
1221 runs-on : ubuntu-latest
22+ # Write perms only where needed
23+ permissions :
24+ contents : read
1325 strategy :
26+ fail-fast : false
1427 matrix :
15- python-version : ["3.11", "3.12", "3.13", "3.14"]
28+ include :
29+ - python-version : " 3.11"
30+ experimental : false
31+ - python-version : " 3.12"
32+ experimental : false
33+ - python-version : " 3.13"
34+ experimental : false
35+ - python-version : " 3.14" # treat 3.14 as experimental so CI doesn't block if it breaks
36+ experimental : true
37+ continue-on-error : ${{ matrix.experimental }}
1638
1739 steps :
1840 - name : Checkout
@@ -23,29 +45,31 @@ jobs:
2345 with :
2446 enable-cache : true
2547
26- - name : Set up Python ${{ matrix.python-version }}
48+ - name : Set up Python
2749 run : uv python install ${{ matrix.python-version }}
2850
29- - name : Sync dependencies with uv
30- run : uv sync --all-extras
51+ # Ensure dev tools (ruff, mypy, pytest, bandit, safety, pytest-cov) are declared in pyproject dev deps.
52+ - name : Sync dependencies
53+ run : uv sync --all-extras --dev
3154
3255 - name : Lint (ruff)
33- run : uv run ruff check python_project_deployment
56+ run : uv run ruff check .
3457
3558 - name : Typecheck (mypy)
36- run : uv run mypy python_project_deployment
59+ run : uv run mypy src
3760
3861 - name : Tests (pytest)
3962 run : uv run pytest --cov --cov-report=xml --cov-report=html
4063
41- - name : Security scan for dangerous APIs
64+ - name : Dangerous API scan (grep)
65+ continue-on-error : true
66+ shell : bash
4267 run : |
4368 set -euo pipefail
44- if grep -R --line-number -E "\beval\(|\bexec\(|pickle\.loads|yaml\.load|subprocess\.(Popen|call)" python_project_deployment / tests/ || true; then
45- echo "⚠️ Potentially dangerous API usage detected. Please review before merging ." >&2
69+ if grep -R --line-number -E "\beval\(|\bexec\(|pickle\.loads|yaml\.load(?!_safe) |subprocess\.(Popen|call)" src / tests/ || true; then
70+ echo "⚠️ Potentially dangerous API usage detected. Please review." >&2
4671 exit 2
4772 fi
48- continue-on-error : true
4973
5074 - name : Upload coverage.xml
5175 uses : actions/upload-artifact@v4
@@ -59,89 +83,65 @@ jobs:
5983 name : coverage-html-${{ matrix.python-version }}
6084 path : htmlcov
6185
86+ # Upload Codecov once to avoid noisy duplicate uploads
87+ - name : Upload to Codecov
88+ if : matrix.python-version == '3.11'
89+ uses : codecov/codecov-action@v4
90+ with :
91+ files : coverage.xml
92+ flags : unittests
93+ fail_ci_if_error : false
94+ env :
95+ CODECOV_TOKEN : ${{ secrets.CODECOV_TOKEN }}
96+
6297 security :
63- name : Security Scan
98+ name : Security Scan (Bandit + Safety)
6499 runs-on : ubuntu-latest
100+ needs : test
101+ # Grant code scanning upload only here
102+ permissions :
103+ contents : read
104+ security-events : write
105+
65106 env :
66107 SECURITY_FAIL_LEVEL : MEDIUM
108+
67109 steps :
68- - uses : actions/checkout@v4
110+ - name : Checkout
111+ uses : actions/checkout@v4
69112
70113 - name : Install uv
71114 uses : astral-sh/setup-uv@v4
72115 with :
73116 enable-cache : true
74117
75- - name : Set up Python 3.11
118+ - name : Set up Python
76119 run : uv python install 3.11
77120
78- - name : Sync deps and install security tools
79- run : |
80- uv sync --all-extras
81- uv pip install bandit safety
121+ - name : Sync dependencies
122+ run : uv sync --all-extras --dev
82123
83- - name : Run Bandit and export SARIF
124+ - name : Run Bandit (JSON + SARIF)
84125 run : |
85- uv run bandit -r python_project_deployment/ -f json -o bandit-report.json
86- uv run bandit -r python_project_deployment/ -f sarif -o bandit-report.sarif
87- continue-on-error : true
126+ uv run bandit -r src/ -f json -o bandit-report.json || true
127+ uv run bandit -r src/ -f sarif -o bandit-report.sarif || true
88128
89129 - name : Upload Bandit SARIF to GitHub Code Scanning
90130 uses : github/codeql-action/upload-sarif@v3
91131 with :
92132 sarif_file : bandit-report.sarif
93133 continue-on-error : true
94134
95- - name : Run Safety (fail on any vulnerability)
96- run : |
97- uv run safety check --json > safety-report.json || true
98- python - <<'PY'
99- import json,sys,os
100- try:
101- data=json.load(open('safety-report.json'))
102- if data.get('vulnerabilities'):
103- print('Safety reported vulnerabilities')
104- sys.exit(2)
105- print('No safety vulnerabilities')
106- except Exception as e:
107- print(f'Error parsing safety output: {e}')
108- sys.exit(0)
109- PY
110- continue-on-error : true
135+ - name : Run Safety (JSON)
136+ run : uv run safety check --json > safety-report.json || true
111137
112138 - name : Apply Bandit threshold
113- run : |
114- python - <<'PY'
115- import json,sys,os
116- level=os.environ.get('SECURITY_FAIL_LEVEL','MEDIUM').upper()
117- try:
118- data=json.load(open('bandit-report.json'))
119- sevs=[issue.get('issue_severity','') for issue in data.get('results',[])]
120- if level=='NONE':
121- print('SECURITY_FAIL_LEVEL=NONE: not failing on bandit issues')
122- sys.exit(0)
123- if level=='HIGH':
124- if 'HIGH' in sevs:
125- print('Failing on HIGH bandit issues')
126- sys.exit(2)
127- else:
128- print('No HIGH bandit issues')
129- sys.exit(0)
130- if level=='MEDIUM':
131- if any(s in ('HIGH','MEDIUM') for s in sevs):
132- print('Failing on MEDIUM or HIGH bandit issues')
133- sys.exit(2)
134- else:
135- print('No MEDIUM/HIGH bandit issues')
136- sys.exit(0)
137- print('Unknown SECURITY_FAIL_LEVEL:', level)
138- sys.exit(1)
139- except Exception as e:
140- print(f'Error parsing bandit report: {e}')
141- sys.exit(0)
142- PY
139+ run : uv run python scripts/security_bandit_check.py
143140 continue-on-error : true
144141
142+ - name : Fail on Safety vulnerabilities
143+ run : uv run python scripts/security_safety_check.py
144+
145145 - name : Upload security reports
146146 if : always()
147147 uses : actions/upload-artifact@v4
@@ -151,4 +151,3 @@ jobs:
151151 bandit-report.json
152152 bandit-report.sarif
153153 safety-report.json
154- if data.get('vulnerabilities'):
0 commit comments