-
Notifications
You must be signed in to change notification settings - Fork 0
251 lines (220 loc) · 8.73 KB
/
Copy pathci.yml
File metadata and controls
251 lines (220 loc) · 8.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
name: CI
# Trigger CI on pushes to main and all pull requests
# This ensures every code change is validated before merging
on:
push:
branches:
- main
pull_request:
# Run on all PRs regardless of target branch
branches:
- '**'
# Let release.yml run CI before publishing
workflow_call:
# CI only reads the code
permissions:
contents: read
jobs:
# Job 1: Lint and Format Check
# Runs the same ruff hooks (version, args) as prek does locally
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
- name: Run ruff
# fails if ruff-check finds issues or ruff-format would change a file
# No afterpython/ruff.toml means ruff isn't set up (and there are no ruff hooks), pass green.
run: |
if [ ! -f afterpython/ruff.toml ]; then
echo "afterpython/ruff.toml not found (ruff not set up); skipping."
exit 0
fi
uvx prek run --config afterpython/.pre-commit-config.yaml --all-files --show-diff-on-failure ruff-check ruff-format
# Job 2: Detect Test Strategy
# Determines whether to use pixi or uv based on pixi.toml presence
detect-test-strategy:
runs-on: ubuntu-latest
outputs:
use_pixi: ${{ steps.check.outputs.use_pixi }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Check for pixi.toml
id: check
run: |
if [ -f "pixi.toml" ]; then
echo "use_pixi=true" >> $GITHUB_OUTPUT
else
echo "use_pixi=false" >> $GITHUB_OUTPUT
fi
# Job 3.1: Test Suite - UV Workflow
# Traditional Python testing with uv for projects without pixi
test-uv:
needs: detect-test-strategy
if: needs.detect-test-strategy.outputs.use_pixi == 'false'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', '3.13', '3.14']
fail-fast: false
env:
# every uv command in this job uses this Python (uv downloads it if needed)
UV_PYTHON: ${{ matrix.python-version }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
- name: Install dependencies
# the package + all [dependency-groups] (e.g. test, dev), at the versions in uv.lock
# --locked fails if uv.lock is missing or out of date with pyproject.toml
run: uv sync --locked --all-groups
- name: Run tests with pytest
# No pytest in any dependency group means "no tests yet", pass green.
# Exit code 5 (no tests collected) is success too.
# `|| code=$?` because GitHub runs bash with -e, which would stop at pytest's failure.
run: |
if [ ! -x .venv/bin/pytest ]; then
echo "pytest not installed (not in any [dependency-groups]); skipping."
exit 0
fi
code=0
uv run --no-sync pytest -v || code=$?
if [ $code -eq 5 ]; then exit 0; else exit $code; fi
# Job 3.2: Test Suite - Pixi Workflow
# Pixi-based testing for projects using pixi.toml
test-pixi:
needs: detect-test-strategy
if: needs.detect-test-strategy.outputs.use_pixi == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
environment: ['py312', 'py313', 'py314']
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up pixi
uses: prefix-dev/setup-pixi@v0.10.2
with:
pixi-version: v0.81.0
cache: true
# install (and cache) only the environment this matrix run tests
environments: ${{ matrix.environment }}
# Optional: Uncomment if you need prefix.dev authentication
# auth-host: prefix.dev
# auth-token: ${{ secrets.PREFIX_DEV_TOKEN }}
- name: Run tests with pixi
# Runs the "test" task of pixi.toml in this matrix run's environment (e.g. py313)
# Exit code 5 (no tests collected) is success.
# `|| code=$?` because GitHub runs bash with -e, which would stop at the failure.
run: |
code=0
pixi run -e ${{ matrix.environment }} test || code=$?
if [ $code -eq 5 ]; then exit 0; else exit $code; fi
# Job 3.3: Test Suite - Complete
# Unified test status for branch protection
test:
if: always()
needs: [detect-test-strategy, test-uv, test-pixi]
runs-on: ubuntu-latest
steps:
- name: Check test results
run: |
uv_result="${{ needs.test-uv.result }}"
pixi_result="${{ needs.test-pixi.result }}"
# Invariant: exactly one track runs (use_pixi gate) and must succeed;
# the other is skipped. Any other combination — both skipped (detect
# job died), both succeeded (broken gating), or anything failing —
# is red. The "no tests yet" case is handled inside the running track
# (pytest step exits 0), so it reports success here, not skipped.
if [ "$uv_result" == "success" ] && [ "$pixi_result" == "skipped" ]; then
echo "All tests passed (uv track)."
exit 0
elif [ "$pixi_result" == "success" ] && [ "$uv_result" == "skipped" ]; then
echo "All tests passed (pixi track)."
exit 0
else
echo "Tests did not pass cleanly: uv=$uv_result, pixi=$pixi_result"
exit 1
fi
# Job 4: Build Verification
# Ensures the package can be built successfully
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
- name: Build package
# --no-sources: build like it will be published, ignoring [tool.uv.sources] (local paths, git)
run: uv build --no-sources
- name: List build artifacts
# Show what was built (wheel + sdist)
# Useful for debugging build issues
run: ls -lh dist/
- name: Verify installation
# Install the wheel into a throwaway env and import it, catches packaging issues like missing files.
# Import name: [tool.uv.build-backend] module-name if set, else project.name with '-' -> '_'.
# python -P: don't put the current folder on sys.path, so the import can't pick up
# the source folder (or the afterpython/ folder) instead of the installed wheel.
run: |
uv run --isolated --no-project --with dist/*.whl python -P - <<'PY'
import importlib
import tomllib
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)
backend = data.get("tool", {}).get("uv", {}).get("build-backend", {})
names = backend.get("module-name") or data["project"]["name"].replace("-", "_")
for name in [names] if isinstance(names, str) else names:
importlib.import_module(name)
print(f"import {name} OK")
PY
- name: Upload build artifacts
# release.yml publishes exactly these files instead of building again
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
if-no-files-found: error
# Job 5: Type Check
# Runs `ap check` like the ty hook does locally, in the project's own environment
# (uv's .venv, or pixi's default environment when pixi.toml exists)
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Check for ty setup
# No afterpython/ty.toml means ty isn't set up (and there's no ty hook), pass green.
id: ty
run: |
if [ -f afterpython/ty.toml ]; then
echo "enabled=true" >> $GITHUB_OUTPUT
else
echo "afterpython/ty.toml not found (ty not set up); skipping."
fi
- name: Install uv
if: steps.ty.outputs.enabled == 'true' && hashFiles('pixi.toml') == ''
uses: astral-sh/setup-uv@v10.2.0
- name: Run ty (uv)
# the package + all [dependency-groups] (incl. afterpython itself), at the versions in uv.lock
if: steps.ty.outputs.enabled == 'true' && hashFiles('pixi.toml') == ''
run: |
uv sync --locked --all-groups
uv run --no-sync ap check
- name: Set up pixi
if: steps.ty.outputs.enabled == 'true' && hashFiles('pixi.toml') != ''
uses: prefix-dev/setup-pixi@v0.10.2
with:
pixi-version: v0.81.0
cache: true
environments: default
- name: Run ty (pixi)
# ty uses the pixi environment it runs in (there's no .venv in CI)
if: steps.ty.outputs.enabled == 'true' && hashFiles('pixi.toml') != ''
run: pixi run ap check