From 974e1f09fc5fa289513f804398f9422546bf5c9f Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 5 Aug 2026 17:09:12 -0700 Subject: [PATCH 1/3] test: provision Cloud namespace per CI run --- .github/scripts/cloud_namespace.py | 110 +++++++++++++++++++++++++++++ .github/workflows/ci.yml | 39 ++++++++-- 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/cloud_namespace.py diff --git a/.github/scripts/cloud_namespace.py b/.github/scripts/cloud_namespace.py new file mode 100644 index 000000000..c13d1e65e --- /dev/null +++ b/.github/scripts/cloud_namespace.py @@ -0,0 +1,110 @@ +"""Create and delete an isolated Temporal Cloud namespace for CI.""" + +import asyncio +import os +import sys +import time +from pathlib import Path + +from temporalio.api.cloud.cloudservice.v1 import ( + CreateNamespaceRequest, + DeleteNamespaceRequest, + GetAsyncOperationRequest, + GetNamespaceRequest, +) +from temporalio.api.cloud.namespace.v1 import MtlsAuthSpec, NamespaceSpec +from temporalio.api.cloud.operation.v1 import AsyncOperation +from temporalio.client import CloudOperationsClient + + +async def wait_for_operation( + client: CloudOperationsClient, operation: AsyncOperation +) -> None: + deadline = time.monotonic() + 10 * 60 + while True: + operation = ( + await client.cloud_service.get_async_operation( + GetAsyncOperationRequest(async_operation_id=operation.id) + ) + ).async_operation + if operation.state == AsyncOperation.STATE_FULFILLED: + return + if operation.state in { + AsyncOperation.STATE_FAILED, + AsyncOperation.STATE_CANCELLED, + AsyncOperation.STATE_REJECTED, + }: + raise RuntimeError( + "Cloud operation " + f"{operation.id} {AsyncOperation.State.Name(operation.state).lower()}: " + f"{operation.failure_reason}" + ) + if time.monotonic() >= deadline: + raise TimeoutError(f"Timed out waiting for Cloud operation {operation.id}") + delay = max( + operation.check_duration.seconds + + operation.check_duration.nanos / 1_000_000_000, + 1, + ) + await asyncio.sleep(min(delay, deadline - time.monotonic())) + + +async def create() -> None: + client = await cloud_client() + namespace_name = "sdk-python-ci-{}-{}".format( + os.environ["GITHUB_RUN_ID"], os.environ["GITHUB_RUN_ATTEMPT"] + ) + result = await client.cloud_service.create_namespace( + CreateNamespaceRequest( + spec=NamespaceSpec( + name=namespace_name, + regions=["aws-ca-central-1"], + retention_days=1, + mtls_auth=MtlsAuthSpec( + accepted_client_ca=Path( + os.environ["TEMPORAL_CLOUD_CLIENT_CA_PATH"] + ).read_bytes(), + enabled=True, + ), + ) + ) + ) + # Make cleanup possible even if provisioning fails after Cloud accepts the request. + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write(f"namespace={result.namespace}\n") + await wait_for_operation(client, result.async_operation) + + +async def delete(namespace: str) -> None: + client = await cloud_client() + existing = await client.cloud_service.get_namespace( + GetNamespaceRequest(namespace=namespace) + ) + result = await client.cloud_service.delete_namespace( + DeleteNamespaceRequest( + namespace=namespace, + resource_version=existing.namespace.resource_version, + ) + ) + await wait_for_operation(client, result.async_operation) + + +async def cloud_client() -> CloudOperationsClient: + return await CloudOperationsClient.connect( + api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], + version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], + ) + + +async def main() -> None: + match sys.argv[1:]: + case ["create"]: + await create() + case ["delete", namespace]: + await delete(namespace) + case _: + raise ValueError("Usage: cloud_namespace.py create|delete ") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcf42e8af..be0510ca6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,19 +230,46 @@ jobs: - run: uv tool install poethepoet - run: uv sync --all-extras - run: poe build-develop + - name: Generate Cloud test certificates + run: | + cert_dir="$RUNNER_TEMP/cloud-test-certs" + mkdir "$cert_dir" + openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ + -keyout "$cert_dir/ca.key" -out "$cert_dir/ca.pem" \ + -subj '/CN=Temporal Python SDK Cloud CI CA' + openssl req -newkey rsa:2048 -nodes \ + -keyout "$cert_dir/client.key" -out "$cert_dir/client.csr" \ + -subj '/CN=Temporal Python SDK Cloud CI' + openssl x509 -req -days 1 -in "$cert_dir/client.csr" \ + -CA "$cert_dir/ca.pem" -CAkey "$cert_dir/ca.key" -CAcreateserial \ + -out "$cert_dir/client.pem" -extfile <(printf 'extendedKeyUsage=clientAuth') + { + echo "TEMPORAL_CLOUD_CLIENT_CA_PATH=$cert_dir/ca.pem" + echo "TEMPORAL_TLS_CLIENT_CERT_PATH=$cert_dir/client.pem" + echo "TEMPORAL_TLS_CLIENT_KEY_PATH=$cert_dir/client.key" + } >> "$GITHUB_ENV" + - name: Create Cloud namespace + id: create-cloud-namespace + run: uv run python .github/scripts/cloud_namespace.py create + env: + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - run: mkdir junit-xml - run: poe test -s --workflow-environment envconfig --junit-xml=junit-xml/cloud.xml timeout-minutes: 15 env: - TEMPORAL_ADDRESS: sdk-ci.a2dd6.tmprl.cloud:7233 - TEMPORAL_NAMESPACE: sdk-ci.a2dd6 - TEMPORAL_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} - TEMPORAL_TLS_CLIENT_CERT_DATA: ${{ secrets.TEMPORAL_CLIENT_CERT }} - TEMPORAL_TLS_CLIENT_KEY_DATA: ${{ secrets.TEMPORAL_CLIENT_KEY }} + TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233 + TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} TEMPORAL_IS_CLOUD_TESTS: true TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6 + TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} + - name: Delete Cloud namespace + if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }} + run: uv run python .github/scripts/cloud_namespace.py delete "${{ steps.create-cloud-namespace.outputs.namespace }}" + env: + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - name: "Upload junit-xml artifacts" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() From dabe97cdb4d8fbba9841dd864ed13a0c526d4042 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 5 Aug 2026 17:17:20 -0700 Subject: [PATCH 2/3] fix: use current Cloud API for namespace lifecycle --- .github/scripts/cloud_namespace.py | 1 - .github/workflows/ci.yml | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/scripts/cloud_namespace.py b/.github/scripts/cloud_namespace.py index c13d1e65e..c0746a60f 100644 --- a/.github/scripts/cloud_namespace.py +++ b/.github/scripts/cloud_namespace.py @@ -92,7 +92,6 @@ async def delete(namespace: str) -> None: async def cloud_client() -> CloudOperationsClient: return await CloudOperationsClient.connect( api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], - version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], ) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be0510ca6..467e73d27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -253,7 +253,6 @@ jobs: run: uv run python .github/scripts/cloud_namespace.py create env: TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} - TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - run: mkdir junit-xml - run: poe test -s --workflow-environment envconfig --junit-xml=junit-xml/cloud.xml timeout-minutes: 15 @@ -269,7 +268,6 @@ jobs: run: uv run python .github/scripts/cloud_namespace.py delete "${{ steps.create-cloud-namespace.outputs.namespace }}" env: TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} - TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - name: "Upload junit-xml artifacts" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() From bd2ef227e3e3d9fea136ce672803945ce2ad14d7 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 5 Aug 2026 17:23:04 -0700 Subject: [PATCH 3/3] fix: use latest Cloud API version in CI --- .github/scripts/cloud_namespace.py | 1 + .github/workflows/ci.yml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/cloud_namespace.py b/.github/scripts/cloud_namespace.py index c0746a60f..c13d1e65e 100644 --- a/.github/scripts/cloud_namespace.py +++ b/.github/scripts/cloud_namespace.py @@ -92,6 +92,7 @@ async def delete(namespace: str) -> None: async def cloud_client() -> CloudOperationsClient: return await CloudOperationsClient.connect( api_key=os.environ["TEMPORAL_CLIENT_CLOUD_API_KEY"], + version=os.environ["TEMPORAL_CLIENT_CLOUD_API_VERSION"], ) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 467e73d27..b1098dcdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -253,6 +253,7 @@ jobs: run: uv run python .github/scripts/cloud_namespace.py create env: TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 - run: mkdir junit-xml - run: poe test -s --workflow-environment envconfig --junit-xml=junit-xml/cloud.xml timeout-minutes: 15 @@ -261,13 +262,14 @@ jobs: TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} TEMPORAL_IS_CLOUD_TESTS: true TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} - TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} - name: Delete Cloud namespace if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }} run: uv run python .github/scripts/cloud_namespace.py delete "${{ steps.create-cloud-namespace.outputs.namespace }}" env: TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} + TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 - name: "Upload junit-xml artifacts" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always()