From 487f36376e5db4e6bd460e9cc26ed80548a7738f Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sun, 11 Oct 2026 16:06:39 +0000 Subject: [PATCH] Prepare PHP SDK 2.3.0-rc.2 and published scoped-worker proof --- .../service-mode-published-smoke.yml | 52 +++++++++++++- composer.json | 2 +- docs/quickstart-contract.json | 4 +- .../smoke/cooperative-worker-credentials.php | 68 +++++++++++++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 tests/smoke/cooperative-worker-credentials.php diff --git a/.github/workflows/service-mode-published-smoke.yml b/.github/workflows/service-mode-published-smoke.yml index b25646a4..7f0bd93e 100644 --- a/.github/workflows/service-mode-published-smoke.yml +++ b/.github/workflows/service-mode-published-smoke.yml @@ -11,6 +11,16 @@ on: description: Exact qualified Server version declared by the published SDK required: true type: string + cooperative_server_image: + description: Optional immutable published Server image for worker-only cooperative discovery + required: false + type: string + default: '' + cooperative_server_version: + description: Exact Server version represented by the cooperative image + required: false + type: string + default: '' permissions: contents: read @@ -47,6 +57,7 @@ jobs: - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: '8.3' + extensions: pcntl, posix tools: composer:v2 coverage: none @@ -241,10 +252,47 @@ jobs: exit "$worker_status" fi + - name: Check out only the smoke runner + if: inputs.cooperative_server_image != '' + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Verify published cooperative discovery with a scoped worker token + if: inputs.cooperative_server_image != '' + env: + SDK_VERSION: ${{ inputs.sdk_version }} + COOPERATIVE_SERVER_IMAGE: ${{ inputs.cooperative_server_image }} + COOPERATIVE_SERVER_VERSION: ${{ inputs.cooperative_server_version }} + run: | + set -euo pipefail + [[ "$COOPERATIVE_SERVER_IMAGE" =~ ^durableworkflow/server@sha256:[0-9a-f]{64}$ ]] + [[ "$COOPERATIVE_SERVER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + docker pull "$COOPERATIVE_SERVER_IMAGE" + docker run --detach --name published-sdk-cooperative-runtime \ + --user 1000:1000 --publish 127.0.0.1:8081:8080 \ + --env DB_CONNECTION=sqlite --env CACHE_STORE=file \ + --env DW_SERVER_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \ + --env DW_OPERATOR_TOKEN="$DURABLE_WORKFLOW_CLIENT_TOKEN" \ + --env DW_WORKER_TOKEN="$DURABLE_WORKFLOW_WORKER_TOKEN" \ + --env DW_ADMIN_TOKEN=published-smoke-admin \ + --env DW_TASK_DISPATCH_MODE=poll --env DW_WORKER_POLL_TIMEOUT=1 \ + "$COOPERATIVE_SERVER_IMAGE" + docker exec --user 1000:1000 published-sdk-cooperative-runtime server-bootstrap + for attempt in {1..30}; do + if curl --fail --silent http://127.0.0.1:8081/api/ready > /dev/null; then break; fi + sleep 2 + done + curl --fail --silent http://127.0.0.1:8081/api/ready > /dev/null + consumer="$(cat "$RUNNER_TEMP/service-mode-consumer")" + php tests/smoke/cooperative-worker-credentials.php "$consumer/vendor/autoload.php" + - name: Show runtime diagnostics on failure if: failure() - run: docker logs --tail 100 published-sdk-runtime || true + run: | + docker logs --tail 100 published-sdk-runtime || true + docker logs --tail 100 published-sdk-cooperative-runtime || true - name: Remove the disposable runtime if: always() - run: docker rm --force --volumes published-sdk-runtime || true + run: docker rm --force --volumes published-sdk-runtime published-sdk-cooperative-runtime || true diff --git a/composer.json b/composer.json index e4a2f1cd..c06830ec 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,7 @@ } }, "durable-workflow": { - "product-train": "2.3.0-rc.1", + "product-train": "2.3.0-rc.2", "supported-server-versions": "2.5.0", "worker-protocol-version": "1.19", "control-plane-version": "2", diff --git a/docs/quickstart-contract.json b/docs/quickstart-contract.json index 4188a52b..f6eb7297 100644 --- a/docs/quickstart-contract.json +++ b/docs/quickstart-contract.json @@ -3,8 +3,8 @@ "schema_version": 2, "package": { "name": "durable-workflow/sdk", - "published_version": "2.3.0-rc.1", - "composer_requirement": "2.3.0-rc.1", + "published_version": "2.3.0-rc.2", + "composer_requirement": "2.3.0-rc.2", "onboarding_requirement": "^2.3@RC" }, "runtime_targets": { diff --git a/tests/smoke/cooperative-worker-credentials.php b/tests/smoke/cooperative-worker-credentials.php new file mode 100644 index 00000000..cfe0b8e1 --- /dev/null +++ b/tests/smoke/cooperative-worker-credentials.php @@ -0,0 +1,68 @@ +getFileName()); +if ($version !== getenv('SDK_VERSION') || $root === false || $implementation === false + || !str_starts_with($implementation, $root.'/')) { + throw new RuntimeException('The smoke must execute the selected registry SDK, without source fallback.'); +} + +$url = 'http://127.0.0.1:8081'; +$token = getenv('DURABLE_WORKFLOW_WORKER_TOKEN'); +$client = new Client($url, namespace: 'default', workerToken: $token, workerProtocolVersion: '1.20'); +$info = $client->workerClusterInfo(); +if ($info->version !== getenv('COOPERATIVE_SERVER_VERSION')) { + throw new RuntimeException('Discovery returned the wrong published runtime version.'); +} + +// Intentionally send the same worker-role credential through control requests. +// The isolated Server must reject both, proving its actual role is restricted. +$restricted = new Client($url, namespace: 'default', controlToken: $token); +foreach (['diagnostics', 'start'] as $operation) { + try { + if ($operation === 'diagnostics') { + $restricted->clusterInfo(includeDiagnostics: true); + } else { + $restricted->startWorkflow('smoke.worker_discovery', 'must-refuse-'.bin2hex(random_bytes(8)), 'smoke'); + } + throw new RuntimeException('The worker credential unexpectedly authorized '.$operation.'.'); + } catch (ServerException $failure) { + if ($failure->status !== 403) { + throw $failure; + } + } +} + +$registered = $deregistered = false; +$workerId = 'published-discovery-'.bin2hex(random_bytes(8)); +$worker = new Worker($client, $workerId, workerId: $workerId, enableCooperativeCancellation: true, + diagnosticListener: static function (string $event, array $details) use (&$worker, &$registered, &$deregistered): void { + if ($event === 'worker.registered') { + $registered = true; + $worker->requestShutdown(); + } elseif ($event === 'worker.deregistered') { + $deregistered = true; + } + }); +$worker->registerWorkflow('smoke.worker_discovery', static fn (WorkflowContext $context): string => 'unused'); +$worker->run(0); +if (!$registered || !$deregistered) { + throw new RuntimeException('The cooperative worker did not register and deregister successfully.'); +} +echo json_encode([ + 'sdk_version' => $version, 'sdk_reference' => Composer\InstalledVersions::getReference($package), + 'server_version' => $info->version, 'server_image' => getenv('COOPERATIVE_SERVER_IMAGE'), + 'worker_protocol' => '1.20', 'worker_registered' => $registered, 'worker_deregistered' => $deregistered, + 'operator_diagnostics_refused_by_server' => true, 'control_mutation_refused_by_server' => true, +], JSON_THROW_ON_ERROR).PHP_EOL;