Skip to content

Commit 290bacd

Browse files
committed
Add automated PyPI release workflow
Tag-triggered (v*) pipeline: - test: run unit tests across Python 3.6–3.13, example tests on 3.13 only - build: produce sdist and wheel via python -m build - publish: create GitHub Release and upload to PyPI via twine - smoke-test: install from PyPI and verify version + live search
1 parent bf3e5fa commit 290bacd

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

.github/workflows/release.yml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ['v**']
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: false
10+
11+
jobs:
12+
test:
13+
name: Test / Python ${{ matrix.python-version }}
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
18+
steps:
19+
- uses: actions/checkout@v6
20+
21+
- uses: actions/setup-python@v6
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: pip install -e .[test]
27+
28+
- name: Run tests
29+
run: pytest -k "not example"
30+
env:
31+
API_KEY: ${{ secrets.API_KEY }}
32+
33+
- name: Run example tests
34+
if: matrix.python-version == '3.13'
35+
run: pytest -k "example"
36+
env:
37+
API_KEY: ${{ secrets.API_KEY }}
38+
39+
build:
40+
name: Build distribution
41+
needs: [test]
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v6
45+
46+
- uses: actions/setup-python@v6
47+
with:
48+
python-version: "3.13"
49+
50+
- name: Build sdist and wheel
51+
run: |
52+
pip install build
53+
python -m build
54+
55+
- uses: actions/upload-artifact@v7
56+
with:
57+
name: dist
58+
path: dist/
59+
if-no-files-found: error
60+
61+
publish:
62+
name: Publish release
63+
needs: [build]
64+
runs-on: ubuntu-latest
65+
permissions:
66+
contents: write
67+
packages: write
68+
steps:
69+
- uses: actions/download-artifact@v8
70+
with:
71+
name: dist
72+
path: dist/
73+
74+
- uses: actions/setup-python@v6
75+
with:
76+
python-version: "3.13"
77+
78+
- name: Create GitHub Release
79+
env:
80+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81+
GH_REPO: ${{ github.repository }}
82+
run: |
83+
gh release create ${{ github.ref_name }} dist/* --generate-notes \
84+
|| gh release upload ${{ github.ref_name }} dist/* --clobber
85+
86+
- name: Install twine
87+
run: pip install --upgrade pip twine
88+
89+
- name: Publish to PyPI
90+
env:
91+
TWINE_USERNAME: __token__
92+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
93+
run: twine upload dist/*
94+
95+
- name: Publish to GitHub Packages
96+
env:
97+
TWINE_USERNAME: ${{ github.repository_owner }}
98+
TWINE_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
99+
run: twine upload --repository-url https://upload.pypi.pkg.github.com/${{ github.repository_owner }}/ dist/*
100+
101+
smoke-test:
102+
name: Smoke test published package
103+
needs: [publish]
104+
runs-on: ubuntu-latest
105+
steps:
106+
- uses: actions/setup-python@v6
107+
with:
108+
python-version: "3.13"
109+
110+
- name: Wait for PyPI availability
111+
run: |
112+
VERSION=${GITHUB_REF_NAME#v}
113+
echo "Waiting for serpapi==$VERSION on PyPI..."
114+
for i in $(seq 1 12); do
115+
if pip index versions serpapi 2>/dev/null | grep -q "$VERSION"; then
116+
echo "Available!"
117+
exit 0
118+
fi
119+
echo "Attempt $i/12 — sleeping 10s..."
120+
sleep 10
121+
done
122+
echo "Timed out waiting for PyPI propagation" && exit 1
123+
124+
- name: Install from PyPI
125+
run: |
126+
VERSION=${GITHUB_REF_NAME#v}
127+
pip install "serpapi==$VERSION"
128+
129+
- name: Verify import and version
130+
env:
131+
API_KEY: ${{ secrets.API_KEY }}
132+
run: |
133+
VERSION=${GITHUB_REF_NAME#v}
134+
python - <<PY
135+
import os
136+
import serpapi
137+
assert serpapi.__version__ == '$VERSION', f'Version mismatch: {serpapi.__version__} != $VERSION'
138+
print(f'OK: serpapi=={serpapi.__version__} installed successfully')
139+
client = serpapi.Client(api_key=os.environ["API_KEY"])
140+
results = client.search({"engine": "google", "q": "coffee"})
141+
assert results.get("organic_results"), "No organic results returned"
142+
print(f'OK: live search returned {len(results["organic_results"])} organic results')
143+
PY

0 commit comments

Comments
 (0)