Skip to content

Commit dc2081a

Browse files
committed
ci: Migrate release workflow to Trusted Publishing
- Replace Twine/token-based auth with PyPI Trusted Publishing - Add smoke tests to verify wheel and sdist before publishing - Run smoke tests against all supported Python versions (3.8-3.14) - Use matrix strategy for parallel testing across versions - Use uv publish for streamlined publishing Workflow structure: 1. build: Create wheel and sdist artifacts 2. smoke-test: Test on Python 3.8-3.14 in parallel 3. publish: Upload to PyPI after all tests pass Smoke tests verify: - Package imports correctly - Both sync/async clients instantiate - All module properties accessible - Core types and exceptions importable - Dependencies properly bundled - py.typed marker present
1 parent 8aef060 commit dc2081a

File tree

2 files changed

+317
-13
lines changed

2 files changed

+317
-13
lines changed

.github/workflows/release.yml

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,61 @@ defaults:
1212
shell: bash
1313

1414
jobs:
15-
pypi:
16-
name: Publish to PyPI
15+
build:
16+
name: Build distribution
1717
runs-on: ubuntu-latest
18-
permissions:
19-
contents: read
2018
steps:
2119
- name: Checkout
2220
uses: actions/checkout@v5
2321
- name: Install uv
2422
uses: astral-sh/setup-uv@v6
25-
- name: Install dependencies
26-
run: uv sync --locked
27-
- name: Test
28-
run: uv run pytest
2923
- name: Build
3024
run: uv build
25+
- name: Upload artifacts
26+
uses: actions/upload-artifact@v4
27+
with:
28+
name: dist
29+
path: dist/
30+
31+
smoke-test:
32+
name: Smoke test (Python ${{ matrix.python }})
33+
needs: build
34+
runs-on: ubuntu-latest
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v5
42+
- name: Install uv
43+
uses: astral-sh/setup-uv@v6
44+
- name: Download artifacts
45+
uses: actions/download-artifact@v4
46+
with:
47+
name: dist
48+
path: dist/
49+
- name: Smoke test (wheel)
50+
run: uv run --python ${{ matrix.python }} --isolated --no-project --with dist/*.whl tests/smoke_test.py
51+
- name: Smoke test (source distribution)
52+
run: uv run --python ${{ matrix.python }} --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
53+
54+
publish:
55+
name: Publish to PyPI
56+
needs: smoke-test
57+
runs-on: ubuntu-latest
58+
environment:
59+
name: pypi
60+
permissions:
61+
id-token: write
62+
contents: read
63+
steps:
64+
- name: Download artifacts
65+
uses: actions/download-artifact@v4
66+
with:
67+
name: dist
68+
path: dist/
69+
- name: Install uv
70+
uses: astral-sh/setup-uv@v6
3171
- name: Publish
32-
env:
33-
TWINE_NON_INTERACTIVE: true
34-
TWINE_USERNAME: "__token__"
35-
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
36-
run: uvx twine upload dist/* --skip-existing
72+
run: uv publish

tests/smoke_test.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
#!/usr/bin/env python3
2+
"""Smoke tests to verify the built package works correctly.
3+
4+
These tests run against the installed package (wheel or sdist) to verify:
5+
- All imports work correctly
6+
- Dependencies are properly bundled
7+
- Type markers are present
8+
- Both sync and async clients can be instantiated
9+
- All module properties are accessible
10+
11+
Run with: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
12+
"""
13+
14+
import sys
15+
from pathlib import Path
16+
17+
18+
def test_basic_import() -> None:
19+
"""Verify the package can be imported."""
20+
import workos
21+
22+
assert workos is not None
23+
print("✓ Basic import works")
24+
25+
26+
def test_version_accessible() -> None:
27+
"""Verify version metadata is accessible."""
28+
from importlib.metadata import version
29+
30+
pkg_version = version("workos")
31+
assert pkg_version is not None
32+
assert len(pkg_version) > 0
33+
print(f"✓ Version accessible: {pkg_version}")
34+
35+
36+
def test_py_typed_marker() -> None:
37+
"""Verify py.typed marker is included for type checking support."""
38+
import workos
39+
40+
package_path = Path(workos.__file__).parent
41+
py_typed = package_path / "py.typed"
42+
assert py_typed.exists(), f"py.typed marker not found at {py_typed}"
43+
print(f"✓ py.typed marker present at {py_typed}")
44+
45+
46+
def test_sync_client_import_and_instantiate() -> None:
47+
"""Verify sync client can be imported and instantiated."""
48+
from workos import WorkOSClient
49+
50+
# Instantiate with dummy credentials (no API calls made)
51+
client = WorkOSClient(api_key="sk_test_smoke", client_id="client_smoke")
52+
assert client is not None
53+
print("✓ WorkOSClient imports and instantiates")
54+
55+
56+
def test_async_client_import_and_instantiate() -> None:
57+
"""Verify async client can be imported and instantiated."""
58+
from workos import AsyncWorkOSClient
59+
60+
# Instantiate with dummy credentials (no API calls made)
61+
client = AsyncWorkOSClient(api_key="sk_test_smoke", client_id="client_smoke")
62+
assert client is not None
63+
print("✓ AsyncWorkOSClient imports and instantiates")
64+
65+
66+
def test_sync_client_modules_accessible() -> None:
67+
"""Verify all module properties are accessible on sync client."""
68+
from workos import WorkOSClient
69+
70+
client = WorkOSClient(api_key="sk_test_smoke", client_id="client_smoke")
71+
72+
modules = [
73+
"api_keys",
74+
"audit_logs",
75+
"directory_sync",
76+
"events",
77+
"fga",
78+
"mfa",
79+
"organizations",
80+
"organization_domains",
81+
"passwordless",
82+
"pipes",
83+
"portal",
84+
"sso",
85+
"user_management",
86+
"vault",
87+
"webhooks",
88+
"widgets",
89+
]
90+
91+
for module_name in modules:
92+
module = getattr(client, module_name, None)
93+
assert module is not None, f"Module {module_name} not accessible"
94+
print(f" ✓ client.{module_name}")
95+
96+
print(f"✓ All {len(modules)} sync client modules accessible")
97+
98+
99+
def test_async_client_modules_accessible() -> None:
100+
"""Verify all module properties are accessible on async client.
101+
102+
Note: Some modules raise NotImplementedError as they're not yet
103+
supported in the async client. We verify the property exists and
104+
raises the expected error.
105+
"""
106+
from workos import AsyncWorkOSClient
107+
108+
client = AsyncWorkOSClient(api_key="sk_test_smoke", client_id="client_smoke")
109+
110+
# Modules fully supported in async client
111+
supported_modules = [
112+
"api_keys",
113+
"directory_sync",
114+
"events",
115+
"organizations",
116+
"organization_domains",
117+
"pipes",
118+
"sso",
119+
"user_management",
120+
]
121+
122+
# Modules that exist but raise NotImplementedError
123+
not_implemented_modules = [
124+
"audit_logs",
125+
"fga",
126+
"mfa",
127+
"passwordless",
128+
"portal",
129+
"vault",
130+
"webhooks",
131+
"widgets",
132+
]
133+
134+
for module_name in supported_modules:
135+
module = getattr(client, module_name, None)
136+
assert module is not None, f"Module {module_name} not accessible"
137+
print(f" ✓ async_client.{module_name}")
138+
139+
for module_name in not_implemented_modules:
140+
try:
141+
getattr(client, module_name)
142+
raise AssertionError(f"Module {module_name} should raise NotImplementedError")
143+
except NotImplementedError:
144+
print(f" ✓ async_client.{module_name} (not yet implemented)")
145+
146+
total = len(supported_modules) + len(not_implemented_modules)
147+
print(f"✓ All {total} async client modules verified")
148+
149+
150+
def test_core_types_importable() -> None:
151+
"""Verify core type models can be imported."""
152+
# SSO types
153+
from workos.types.sso import Connection, ConnectionDomain, Profile
154+
155+
assert Connection is not None
156+
assert ConnectionDomain is not None
157+
assert Profile is not None
158+
159+
# Organization types
160+
from workos.types.organizations import Organization
161+
162+
assert Organization is not None
163+
164+
# Directory Sync types
165+
from workos.types.directory_sync import Directory, DirectoryGroup, DirectoryUser
166+
167+
assert Directory is not None
168+
assert DirectoryGroup is not None
169+
assert DirectoryUser is not None
170+
171+
# User Management types
172+
from workos.types.user_management import (
173+
AuthenticationResponse,
174+
Invitation,
175+
OrganizationMembership,
176+
User,
177+
)
178+
179+
assert AuthenticationResponse is not None
180+
assert Invitation is not None
181+
assert OrganizationMembership is not None
182+
assert User is not None
183+
184+
# Events types
185+
from workos.types.events import Event
186+
187+
assert Event is not None
188+
189+
# FGA types
190+
from workos.types.fga import Warrant, CheckResponse
191+
192+
assert Warrant is not None
193+
assert CheckResponse is not None
194+
195+
print("✓ Core types importable")
196+
197+
198+
def test_exceptions_importable() -> None:
199+
"""Verify exception classes can be imported."""
200+
from workos.exceptions import (
201+
AuthenticationException,
202+
AuthorizationException,
203+
BadRequestException,
204+
ConflictException,
205+
NotFoundException,
206+
ServerException,
207+
)
208+
209+
assert AuthenticationException is not None
210+
assert AuthorizationException is not None
211+
assert BadRequestException is not None
212+
assert ConflictException is not None
213+
assert NotFoundException is not None
214+
assert ServerException is not None
215+
216+
print("✓ Exception classes importable")
217+
218+
219+
def test_dependencies_available() -> None:
220+
"""Verify core dependencies are installed and importable."""
221+
import httpx
222+
import pydantic
223+
import cryptography
224+
import jwt
225+
226+
print("✓ Core dependencies available (httpx, pydantic, cryptography, jwt)")
227+
228+
229+
def main() -> int:
230+
"""Run all smoke tests."""
231+
print("=" * 60)
232+
print("WorkOS Python SDK - Smoke Tests")
233+
print("=" * 60)
234+
print()
235+
236+
tests = [
237+
test_basic_import,
238+
test_version_accessible,
239+
test_py_typed_marker,
240+
test_sync_client_import_and_instantiate,
241+
test_async_client_import_and_instantiate,
242+
test_sync_client_modules_accessible,
243+
test_async_client_modules_accessible,
244+
test_core_types_importable,
245+
test_exceptions_importable,
246+
test_dependencies_available,
247+
]
248+
249+
failed = 0
250+
for test in tests:
251+
try:
252+
test()
253+
except Exception as e:
254+
print(f"✗ {test.__name__} FAILED: {e}")
255+
failed += 1
256+
print()
257+
258+
print("=" * 60)
259+
if failed == 0:
260+
print(f"All {len(tests)} smoke tests passed!")
261+
return 0
262+
else:
263+
print(f"FAILED: {failed}/{len(tests)} tests failed")
264+
return 1
265+
266+
267+
if __name__ == "__main__":
268+
sys.exit(main())

0 commit comments

Comments
 (0)