66 workflow_dispatch :
77
88jobs :
9- test-deploy :
9+ # ---------------------------------------------------------------------------
10+ # 1. Run the full test suite before attempting any deployment
11+ # ---------------------------------------------------------------------------
12+ test :
13+ name : Run tests (Python ${{ matrix.python-version }})
14+ runs-on : ubuntu-latest
15+ strategy :
16+ fail-fast : true
17+ matrix :
18+ python-version : ["3.9", "3.10", "3.11", "3.12"]
19+ steps :
20+ - uses : actions/checkout@v4
21+
22+ - uses : actions/setup-python@v5
23+ with :
24+ python-version : ${{ matrix.python-version }}
25+
26+ - name : Install dependencies
27+ run : |
28+ python -m pip install --upgrade pip
29+ pip install pytest
30+ pip install -r requirements.txt
31+
32+ - name : Run pytest
33+ run : pytest --tb=short
34+
35+ # ---------------------------------------------------------------------------
36+ # 2. Build the distribution once — all deploy jobs reuse the same artifact
37+ # ---------------------------------------------------------------------------
38+ build :
39+ name : Build distribution
40+ runs-on : ubuntu-latest
41+ needs : test
42+ steps :
43+ - uses : actions/checkout@v4
44+
45+ - uses : actions/setup-python@v5
46+ with :
47+ python-version : " 3.x"
48+
49+ - name : Clean build cache
50+ run : |
51+ rm -rf build dist .eggs
52+ find . -name "*.pyc" -delete
53+ find . -name "__pycache__" -delete
54+
55+ - name : Install build tools
56+ run : pip install --upgrade pip build tomli
57+
58+ - name : Read version from pyproject.toml
59+ id : version
60+ run : |
61+ VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
62+ echo "version=$VERSION" >> $GITHUB_OUTPUT
63+ echo "Building version $VERSION"
64+
65+ - name : Build sdist and wheel
66+ run : python -m build
67+
68+ - name : Upload distribution artifact
69+ uses : actions/upload-artifact@v4
70+ with :
71+ name : dist-${{ steps.version.outputs.version }}
72+ path : dist/
73+ retention-days : 7
74+
75+ outputs :
76+ version : ${{ steps.version.outputs.version }}
77+
78+ # ---------------------------------------------------------------------------
79+ # 3. Deploy to TestPyPI and run a smoke test
80+ # ---------------------------------------------------------------------------
81+ deploy-test :
1082 name : Deploy to TestPyPI
1183 runs-on : ubuntu-latest
84+ needs : build
85+ environment : testpypi
1286 permissions :
1387 id-token : write
1488 contents : read
15- steps :
16- - uses : actions/checkout@v3
17-
18- - name : Set up Python
19- uses : actions/setup-python@v4
20- with :
21- python-version : " 3.x"
22-
23- - name : Clean build cache
24- run : |
25- rm -rf build dist .eggs
26- find . -name "*.pyc" -delete
27- find . -name "__pycache__" -delete
28-
29- - name : Install dependencies
30- run : |
31- python -m pip install --upgrade pip
32- pip install build twine tomli
33-
34- - name : Install dependencies requirements
35- run : |
36- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
37-
38- - name : Debug directory content
39- run : |
40- pwd
41- ls -la
42-
43- - name : Build package
44- run : python -m build
45-
46- - name : Check version on TestPyPI and Upload if new
47- env :
48- TWINE_USERNAME : __token__
49- TWINE_PASSWORD : ${{ secrets.TEST_PYPI_TOKEN }}
50- TWINE_REPOSITORY_URL : https://test.pypi.org/legacy/
51- continue-on-error : true
52- run : |
53- # Get current version from pyproject.toml
54- VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
55- echo "Package version: $VERSION"
56-
57- # Try to install current version from TestPyPI
58- if pip install --index-url https://test.pypi.org/simple/ --no-deps canyonbpy==$VERSION 2>/dev/null; then
59- echo "Version $VERSION already exists on TestPyPI"
60- else
61- echo "Version $VERSION not found on TestPyPI, uploading..."
62- python -m twine upload --verbose dist/*
63- fi
64-
65- - name : Wait for TestPyPI to process upload
66- run : sleep 60
67-
68- - name : Test installation from TestPyPI
69- run : |
70- python -m pip install --index-url https://test.pypi.org/simple/ --no-deps canyonbpy
71- python -c "from canyonbpy import canyonb"
7289
90+ steps :
91+ - uses : actions/checkout@v4
92+
93+ - uses : actions/setup-python@v5
94+ with :
95+ python-version : " 3.x"
96+
97+ - name : Download distribution artifact
98+ uses : actions/download-artifact@v4
99+ with :
100+ name : dist-${{ needs.build.outputs.version }}
101+ path : dist/
102+
103+ - name : Check if version already exists on TestPyPI
104+ id : testpypi-check
105+ run : |
106+ VERSION=${{ needs.build.outputs.version }}
107+ STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
108+ https://test.pypi.org/pypi/canyonbpy/$VERSION/json)
109+ if [ "$STATUS" = "200" ]; then
110+ echo "exists=true" >> $GITHUB_OUTPUT
111+ echo "Version $VERSION already on TestPyPI — skipping upload."
112+ else
113+ echo "exists=false" >> $GITHUB_OUTPUT
114+ echo "Version $VERSION not found on TestPyPI — will upload."
115+ fi
116+
117+ - name : Upload to TestPyPI
118+ if : steps.testpypi-check.outputs.exists == 'false'
119+ env :
120+ TWINE_USERNAME : __token__
121+ TWINE_PASSWORD : ${{ secrets.TEST_PYPI_TOKEN }}
122+ TWINE_REPOSITORY_URL : https://test.pypi.org/legacy/
123+ run : |
124+ pip install twine
125+ twine upload --verbose dist/*
126+
127+ - name : Wait for TestPyPI to index the release
128+ if : steps.testpypi-check.outputs.exists == 'false'
129+ run : sleep 60
130+
131+ - name : Install from TestPyPI
132+ run : |
133+ pip install -r requirements.txt
134+ pip install \
135+ --index-url https://test.pypi.org/simple/ \
136+ --extra-index-url https://pypi.org/simple/ \
137+ canyonbpy==${{ needs.build.outputs.version }}
138+
139+ - name : Smoke test — verify imports, version and accessor
140+ run : |
141+ python - <<'EOF'
142+ import canyonbpy
143+ from canyonbpy import canyonb, DatasetToNumpy
144+
145+ assert canyonbpy.__version__ == "${{ needs.build.outputs.version }}", \
146+ f"Version mismatch: {canyonbpy.__version__}"
147+
148+ import xarray as xr
149+ import numpy as np
150+ ds = xr.Dataset({"pressure": ("x", [100.0])})
151+ assert hasattr(ds, "canyonb"), "ds.canyonb accessor not registered"
152+
153+ print(f"OK canyonbpy {canyonbpy.__version__} smoke test passed")
154+ EOF
155+
156+ # ---------------------------------------------------------------------------
157+ # 4. Deploy to PyPI — only runs after TestPyPI smoke test passes
158+ # ---------------------------------------------------------------------------
73159 deploy-prod :
74160 name : Deploy to PyPI
75- needs : test-deploy
76161 runs-on : ubuntu-latest
77- if : success()
162+ needs : [build, deploy-test]
163+ environment : pypi
164+ permissions :
165+ id-token : write
166+ contents : read
167+
168+ steps :
169+ - uses : actions/setup-python@v5
170+ with :
171+ python-version : " 3.x"
172+
173+ - name : Download distribution artifact
174+ uses : actions/download-artifact@v4
175+ with :
176+ name : dist-${{ needs.build.outputs.version }}
177+ path : dist/
178+
179+ - name : Check if version already exists on PyPI
180+ id : pypi-check
181+ run : |
182+ VERSION=${{ needs.build.outputs.version }}
183+ STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
184+ https://pypi.org/pypi/canyonbpy/$VERSION/json)
185+ if [ "$STATUS" = "200" ]; then
186+ echo "exists=true" >> $GITHUB_OUTPUT
187+ echo "Version $VERSION already on PyPI — skipping upload."
188+ else
189+ echo "exists=false" >> $GITHUB_OUTPUT
190+ echo "Version $VERSION not found on PyPI — will upload."
191+ fi
192+
193+ - name : Upload to PyPI
194+ if : steps.pypi-check.outputs.exists == 'false'
195+ env :
196+ TWINE_USERNAME : __token__
197+ TWINE_PASSWORD : ${{ secrets.PYPI_TOKEN }}
198+ run : |
199+ pip install twine
200+ twine upload --verbose dist/*
201+
202+ # ---------------------------------------------------------------------------
203+ # 5. Attach the wheel and sdist to the GitHub Release
204+ # This also triggers Zenodo archiving (Zenodo watches release events)
205+ # ---------------------------------------------------------------------------
206+ attach-release-assets :
207+ name : Attach dist to GitHub Release
208+ runs-on : ubuntu-latest
209+ needs : [build, deploy-prod]
210+ # Only run when triggered by an actual published release, not workflow_dispatch
211+ if : github.event_name == 'release'
212+ permissions :
213+ contents : write
214+
78215 steps :
79- - uses : actions/checkout@v3
80-
81- - name : Set up Python
82- uses : actions/setup-python@v4
83- with :
84- python-version : " 3.x"
85-
86- - name : Install dependencies
87- run : |
88- python -m pip install --upgrade pip
89- pip install build twine tomli
90-
91- - name : Build package
92- run : python -m build
93-
94- - name : Check version on PyPI and Upload if new
95- env :
96- TWINE_USERNAME : __token__
97- TWINE_PASSWORD : ${{ secrets.PYPI_TOKEN }}
98- run : |
99- # Get current version from pyproject.toml
100- VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
101- echo "Package version: $VERSION"
102-
103- # Try to install current version from PyPI
104- if pip install canyonbpy==$VERSION 2>/dev/null; then
105- echo "Version $VERSION already exists on PyPI"
106- else
107- echo "Version $VERSION not found on PyPI, uploading..."
108- python -m twine upload --verbose dist/*
109- fi
216+ - name : Download distribution artifact
217+ uses : actions/download-artifact@v4
218+ with :
219+ name : dist-${{ needs.build.outputs.version }}
220+ path : dist/
221+
222+ - name : Upload wheel and sdist to GitHub Release
223+ uses : softprops/action-gh-release@v2
224+ with :
225+ files : dist/*
0 commit comments