Describe durable service-call snapshots #293
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Version Validation | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| jobs: | |
| version-validation: | |
| name: Test CLI and SDK version validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout server | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Generate APP_KEY | |
| id: app-key | |
| run: | | |
| # Generate a 32-character base64 key for Laravel | |
| KEY=$(openssl rand -base64 32) | |
| echo "key=base64:$KEY" >> $GITHUB_OUTPUT | |
| # Test 1: Compatible version (2.0.0) - CLI success | |
| - name: Start server (version 2.0.0) | |
| env: | |
| APP_KEY: ${{ steps.app-key.outputs.key }} | |
| DW_AUTH_TOKEN: test-token-123 | |
| APP_VERSION: 2.0.0 | |
| run: | | |
| docker compose up -d --wait | |
| sleep 5 | |
| # Verify server is healthy | |
| curl -f http://localhost:8080/api/health || exit 1 | |
| # Verify version endpoint returns 2.0.0 | |
| VERSION=$(curl -fsS -H "Authorization: Bearer ${DW_AUTH_TOKEN}" http://localhost:8080/api/cluster/info | jq -r '.version') | |
| echo "Server version: $VERSION" | |
| if [ "$VERSION" != "2.0.0" ]; then | |
| echo "ERROR: Expected version 2.0.0, got $VERSION" | |
| exit 1 | |
| fi | |
| - name: Install CLI | |
| run: | | |
| cd /tmp | |
| git clone https://github.com/durable-workflow/cli.git | |
| cd cli | |
| composer install --no-dev --optimize-autoloader | |
| chmod +x bin/dw | |
| echo "/tmp/cli/bin" >> $GITHUB_PATH | |
| - name: Test CLI with compatible server (should succeed) | |
| env: | |
| DURABLE_WORKFLOW_SERVER_URL: http://localhost:8080 | |
| DURABLE_WORKFLOW_AUTH_TOKEN: test-token-123 | |
| DURABLE_WORKFLOW_NAMESPACE: default | |
| run: | | |
| # CLI should connect successfully to version 2.0.0 | |
| dw server:info || exit 1 | |
| echo "✅ CLI successfully connected to compatible server" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.10' | |
| - name: Install Python SDK | |
| run: | | |
| # Version-manifest validation tracks the source contract shared by | |
| # server main and sdk-python main. Published PyPI artifacts may lag | |
| # this compatibility behavior. | |
| pip install "durable-workflow @ git+https://github.com/durable-workflow/sdk-python.git@main" | |
| - name: Test Python SDK with compatible server (should succeed) | |
| run: | | |
| cat > /tmp/test_worker.py << 'EOF' | |
| import asyncio | |
| from durable_workflow import Client, Worker, workflow, activity | |
| @activity.defn(name="test_activity") | |
| def test_activity() -> str: | |
| return "ok" | |
| @workflow.defn(name="test_workflow") | |
| class TestWorkflow: | |
| def run(self, ctx): | |
| return "ok" | |
| async def main(): | |
| client = Client("http://localhost:8080", token="test-token-123", namespace="default") | |
| # This should succeed because server is 2.0.0 | |
| worker = Worker( | |
| client, | |
| task_queue="test-queue", | |
| workflows=[TestWorkflow], | |
| activities=[test_activity], | |
| worker_id="test-worker-compat", | |
| ) | |
| # Worker registration validates version in _register() | |
| # We'll just call _register() directly to test validation | |
| try: | |
| await worker._register() | |
| print("✅ Python SDK successfully connected to compatible server") | |
| finally: | |
| await client.aclose() | |
| asyncio.run(main()) | |
| EOF | |
| python /tmp/test_worker.py | |
| - name: Stop server | |
| run: docker compose down -v | |
| # Test 2: Top-level APP_VERSION is build identity only. Protocol | |
| # manifests remain authoritative for client compatibility. | |
| - name: Create docker-compose override for version 3.0.0 | |
| run: | | |
| cat > docker-compose.override.yml << 'EOF' | |
| services: | |
| bootstrap: | |
| environment: | |
| APP_VERSION: "3.0.0" | |
| server: | |
| environment: | |
| APP_VERSION: "3.0.0" | |
| worker: | |
| environment: | |
| APP_VERSION: "3.0.0" | |
| scheduler: | |
| environment: | |
| APP_VERSION: "3.0.0" | |
| EOF | |
| - name: Start server (version 3.0.0) | |
| env: | |
| APP_KEY: ${{ steps.app-key.outputs.key }} | |
| DW_AUTH_TOKEN: test-token-123 | |
| run: | | |
| docker compose up -d --wait | |
| sleep 5 | |
| # Verify server reports version 3.0.0 | |
| VERSION=$(curl -fsS -H "Authorization: Bearer ${DW_AUTH_TOKEN}" http://localhost:8080/api/cluster/info | jq -r '.version') | |
| echo "Server version: $VERSION" | |
| if [ "$VERSION" != "3.0.0" ]; then | |
| echo "ERROR: Expected version 3.0.0, got $VERSION" | |
| exit 1 | |
| fi | |
| - name: Test CLI with informational top-level version (should succeed) | |
| env: | |
| DURABLE_WORKFLOW_SERVER_URL: http://localhost:8080 | |
| DURABLE_WORKFLOW_AUTH_TOKEN: test-token-123 | |
| DURABLE_WORKFLOW_NAMESPACE: default | |
| run: | | |
| dw server:info || exit 1 | |
| echo "✅ CLI accepted compatible protocol manifests despite APP_VERSION=3.0.0" | |
| - name: Test Python SDK with informational top-level version (should succeed) | |
| run: | | |
| cat > /tmp/test_worker_app_version.py << 'EOF' | |
| import asyncio | |
| from durable_workflow import Client, Worker, workflow, activity | |
| @activity.defn(name="test_activity") | |
| def test_activity() -> str: | |
| return "ok" | |
| @workflow.defn(name="test_workflow") | |
| class TestWorkflow: | |
| def run(self, ctx): | |
| return "ok" | |
| async def main(): | |
| client = Client("http://localhost:8080", token="test-token-123", namespace="default") | |
| worker = Worker( | |
| client, | |
| task_queue="test-queue", | |
| workflows=[TestWorkflow], | |
| activities=[test_activity], | |
| worker_id="test-worker-incompat", | |
| ) | |
| try: | |
| await worker._register() | |
| print("✅ Python SDK accepted compatible protocol manifests despite APP_VERSION=3.0.0") | |
| finally: | |
| await client.aclose() | |
| asyncio.run(main()) | |
| EOF | |
| python /tmp/test_worker_app_version.py | |
| - name: Stop server | |
| run: docker compose down -v | |
| # Test 3: Incompatible worker protocol manifest. The CLI only uses the | |
| # control plane and should still succeed; the Python worker validates the | |
| # worker protocol and must fail closed. | |
| - name: Create docker-compose override with incompatible worker protocol | |
| run: | | |
| cat > docker-compose.override.yml << 'EOF' | |
| services: | |
| bootstrap: | |
| environment: | |
| DW_WORKER_PROTOCOL_VERSION: "2.0" | |
| server: | |
| environment: | |
| DW_WORKER_PROTOCOL_VERSION: "2.0" | |
| worker: | |
| environment: | |
| DW_WORKER_PROTOCOL_VERSION: "2.0" | |
| scheduler: | |
| environment: | |
| DW_WORKER_PROTOCOL_VERSION: "2.0" | |
| EOF | |
| - name: Start server (worker protocol 2.0) | |
| env: | |
| APP_KEY: ${{ steps.app-key.outputs.key }} | |
| DW_AUTH_TOKEN: test-token-123 | |
| run: | | |
| docker compose up -d --wait | |
| sleep 5 | |
| VERSION=$(curl -fsS -H "Authorization: Bearer ${DW_AUTH_TOKEN}" http://localhost:8080/api/cluster/info | jq -r '.worker_protocol.version') | |
| echo "Worker protocol version: $VERSION" | |
| if [ "$VERSION" != "2.0" ]; then | |
| echo "ERROR: Expected worker protocol 2.0, got $VERSION" | |
| exit 1 | |
| fi | |
| - name: Test CLI with incompatible worker protocol (should succeed) | |
| env: | |
| DURABLE_WORKFLOW_SERVER_URL: http://localhost:8080 | |
| DURABLE_WORKFLOW_AUTH_TOKEN: test-token-123 | |
| DURABLE_WORKFLOW_NAMESPACE: default | |
| run: | | |
| dw server:info || exit 1 | |
| echo "✅ CLI ignored worker-protocol-only incompatibility" | |
| - name: Test Python SDK with incompatible worker protocol (should fail with clear error) | |
| run: | | |
| cat > /tmp/test_worker_incompatible.py << 'EOF' | |
| import asyncio | |
| from durable_workflow import Client, Worker, workflow, activity | |
| @activity.defn(name="test_activity") | |
| def test_activity() -> str: | |
| return "ok" | |
| @workflow.defn(name="test_workflow") | |
| class TestWorkflow: | |
| def run(self, ctx): | |
| return "ok" | |
| async def main(): | |
| client = Client("http://localhost:8080", token="test-token-123", namespace="default") | |
| worker = Worker( | |
| client, | |
| task_queue="test-queue", | |
| workflows=[TestWorkflow], | |
| activities=[test_activity], | |
| worker_id="test-worker-incompat", | |
| ) | |
| try: | |
| await worker._register() | |
| print("❌ ERROR: Worker registration should have failed with incompatible worker protocol") | |
| await client.aclose() | |
| exit(1) | |
| except RuntimeError as e: | |
| error_msg = str(e) | |
| print(f"Worker registration error: {error_msg}") | |
| if "unsupported worker_protocol.version" in error_msg and "sdk-python 0.2.x" in error_msg: | |
| print("✅ Python SDK failed with correct worker protocol error message") | |
| else: | |
| print("❌ ERROR: Python SDK error message doesn't match expected format") | |
| print("Expected: 'unsupported worker_protocol.version' and 'sdk-python 0.2.x'") | |
| print(f"Got: {error_msg}") | |
| await client.aclose() | |
| exit(1) | |
| finally: | |
| await client.aclose() | |
| asyncio.run(main()) | |
| EOF | |
| python /tmp/test_worker_incompatible.py | |
| - name: Stop server | |
| run: docker compose down -v | |
| # Test 4: Unknown top-level version remains informational. | |
| - name: Create docker-compose override with no version | |
| run: | | |
| cat > docker-compose.override.yml << 'EOF' | |
| services: | |
| bootstrap: | |
| environment: | |
| APP_VERSION: "unknown" | |
| server: | |
| environment: | |
| APP_VERSION: "unknown" | |
| worker: | |
| environment: | |
| APP_VERSION: "unknown" | |
| scheduler: | |
| environment: | |
| APP_VERSION: "unknown" | |
| EOF | |
| - name: Start server (version unknown) | |
| env: | |
| APP_KEY: ${{ steps.app-key.outputs.key }} | |
| DW_AUTH_TOKEN: test-token-123 | |
| run: | | |
| docker compose up -d --wait | |
| sleep 5 | |
| - name: Test CLI with unknown top-level version (should succeed) | |
| env: | |
| DURABLE_WORKFLOW_SERVER_URL: http://localhost:8080 | |
| DURABLE_WORKFLOW_AUTH_TOKEN: test-token-123 | |
| DURABLE_WORKFLOW_NAMESPACE: default | |
| run: | | |
| dw server:info || exit 1 | |
| echo "✅ CLI accepted compatible protocol manifests despite APP_VERSION=unknown" | |
| - name: Test Python SDK with unknown top-level version (should succeed) | |
| run: | | |
| cat > /tmp/test_worker_unknown.py << 'EOF' | |
| import asyncio | |
| from durable_workflow import Client, Worker, workflow, activity | |
| @activity.defn(name="test_activity") | |
| def test_activity() -> str: | |
| return "ok" | |
| @workflow.defn(name="test_workflow") | |
| class TestWorkflow: | |
| def run(self, ctx): | |
| return "ok" | |
| async def main(): | |
| client = Client("http://localhost:8080", token="test-token-123", namespace="default") | |
| worker = Worker( | |
| client, | |
| task_queue="test-queue", | |
| workflows=[TestWorkflow], | |
| activities=[test_activity], | |
| worker_id="test-worker-unknown", | |
| ) | |
| # Should succeed because protocol manifests are valid. | |
| try: | |
| await worker._register() | |
| print("✅ Python SDK accepted compatible protocol manifests despite APP_VERSION=unknown") | |
| finally: | |
| await client.aclose() | |
| asyncio.run(main()) | |
| EOF | |
| python /tmp/test_worker_unknown.py | |
| - name: Stop server | |
| if: always() | |
| run: docker compose down -v |