diff --git a/dload.xml b/dload.xml
index c4fb70136..23db489dd 100644
--- a/dload.xml
+++ b/dload.xml
@@ -4,8 +4,8 @@
temp-dir="./runtime"
>
-
-
+
+
diff --git a/src/Client/WorkflowOptions.php b/src/Client/WorkflowOptions.php
index 7edc60a10..3873a4981 100644
--- a/src/Client/WorkflowOptions.php
+++ b/src/Client/WorkflowOptions.php
@@ -23,6 +23,7 @@
use Temporal\Common\SearchAttributes\SearchAttributeKey;
use Temporal\Common\TypedSearchAttributes;
use Temporal\Common\Uuid;
+use Temporal\Common\Versioning\VersioningOverride;
use Temporal\Common\WorkflowIdConflictPolicy;
use Temporal\DataConverter\DataConverterInterface;
use Temporal\Internal\Marshaller\Meta\Marshal;
@@ -32,8 +33,8 @@
use Temporal\Internal\Marshaller\Type\NullableType;
use Temporal\Internal\Support\DateInterval;
use Temporal\Internal\Support\Options;
-use Temporal\Worker\WorkerFactoryInterface;
use Temporal\Worker\Worker;
+use Temporal\Worker\WorkerFactoryInterface;
/**
* WorkflowOptions configuration parameters for starting a workflow execution.
@@ -182,6 +183,16 @@ final class WorkflowOptions extends Options
#[Marshal(name: 'Priority')]
public Priority $priority;
+ /**
+ * Override the version of the workflow.
+ *
+ * @since SDK 2.16.0
+ * @since RoadRunner 2025.1.3
+ * @internal Experimental
+ */
+ #[Marshal(name: 'VersioningOverride')]
+ public ?VersioningOverride $versioningOverride = null;
+
/**
* @throws \Exception
*/
@@ -521,6 +532,20 @@ public function withStaticDetails(string $details): self
return $self;
}
+ /**
+ * Sets the versioning override to use when starting this workflow.
+ *
+ * @since SDK 2.16.0
+ * @since RoadRunner 2025.1.3
+ * @internal Experimental
+ */
+ public function withVersioningOverride(?VersioningOverride $override): self
+ {
+ $self = clone $this;
+ $self->versioningOverride = $override;
+ return $self;
+ }
+
/**
* @internal
*/
diff --git a/src/Common/Versioning/VersioningBehavior.php b/src/Common/Versioning/VersioningBehavior.php
new file mode 100644
index 000000000..da3080a3d
--- /dev/null
+++ b/src/Common/Versioning/VersioningBehavior.php
@@ -0,0 +1,77 @@
+ self::Unspecified,
+ 'Pinned' => self::Pinned,
+ 'AutoUpgrade' => self::AutoUpgrade,
+ default => null,
+ };
+ }
+}
diff --git a/src/Common/Versioning/VersioningOverride.php b/src/Common/Versioning/VersioningOverride.php
new file mode 100644
index 000000000..0073d5952
--- /dev/null
+++ b/src/Common/Versioning/VersioningOverride.php
@@ -0,0 +1,48 @@
+deploymentName}.{$this->buildId}";
+ }
+}
diff --git a/src/Internal/Client/WorkflowStarter.php b/src/Internal/Client/WorkflowStarter.php
index 93c75e3f0..04f58f548 100644
--- a/src/Internal/Client/WorkflowStarter.php
+++ b/src/Internal/Client/WorkflowStarter.php
@@ -12,12 +12,15 @@
namespace Temporal\Internal\Client;
use Temporal\Api\Common\V1\WorkflowType;
+use Temporal\Api\Deployment\V1\WorkerDeploymentVersion;
use Temporal\Api\Errordetails\V1\MultiOperationExecutionFailure;
use Temporal\Api\Errordetails\V1\WorkflowExecutionAlreadyStartedFailure;
use Temporal\Api\Failure\V1\MultiOperationExecutionAborted;
use Temporal\Api\Sdk\V1\UserMetadata;
use Temporal\Api\Taskqueue\V1\TaskQueue;
use Temporal\Api\Update\V1\Request as UpdateRequestMessage;
+use Temporal\Api\Workflow\V1\VersioningOverride;
+use Temporal\Api\Workflow\V1\VersioningOverride\PinnedOverride;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationRequest;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationRequest\Operation;
use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationResponse\Response;
@@ -30,6 +33,7 @@
use Temporal\Client\Update\UpdateOptions;
use Temporal\Client\WorkflowOptions;
use Temporal\Common\Uuid;
+use Temporal\Common\Versioning\VersioningBehavior;
use Temporal\DataConverter\DataConverterInterface;
use Temporal\DataConverter\EncodedValues;
use Temporal\Exception\Client\MultyOperation\OperationStatus;
@@ -333,6 +337,30 @@ private function configureExecutionRequest(
->setWorkflowTaskTimeout(DateInterval::toDuration($options->workflowTaskTimeout))
->setPriority($options->priority->toProto());
+ // Versioning override
+ if ($options->versioningOverride !== null) {
+ $value = new VersioningOverride();
+
+ if ($options->versioningOverride->behavior === VersioningBehavior::Pinned) {
+ $version = $options->versioningOverride->version;
+ \assert($version !== null);
+
+ $value->setPinned(
+ (new PinnedOverride())
+ ->setBehavior(VersioningBehavior::Pinned->value)
+ ->setVersion(
+ (new WorkerDeploymentVersion())
+ ->setBuildId($version->buildId)
+ ->setDeploymentName($version->deploymentName),
+ ),
+ );
+ } elseif ($options->versioningOverride->behavior === VersioningBehavior::AutoUpgrade) {
+ $value->setAutoUpgrade(true);
+ }
+
+ $req->setVersioningOverride($value);
+ }
+
// Retry Policy
$options->retryOptions === null or $req->setRetryPolicy($options->retryOptions->toWorkflowRetryPolicy());
diff --git a/src/Internal/Declaration/Graph/ClassNode.php b/src/Internal/Declaration/Graph/ClassNode.php
index dea4b08f0..996b894bc 100644
--- a/src/Internal/Declaration/Graph/ClassNode.php
+++ b/src/Internal/Declaration/Graph/ClassNode.php
@@ -58,7 +58,7 @@ public function count(): int
* Get a method with all the declared classes.
*
* @param non-empty-string $name
- * @return \Traversable
+ * @return \Traversable>
* @throws \ReflectionException
*/
public function getMethods(string $name, bool $reverse = true): \Traversable
@@ -190,6 +190,7 @@ private function getInheritance(): array
/**
* @param iterable $classes
+ * @return list
* @throws \ReflectionException
*/
private function boxMethods(iterable $classes, string $name): array
@@ -205,6 +206,10 @@ private function boxMethods(iterable $classes, string $name): array
return $result;
}
+ /**
+ * @param array $boxed
+ * @return \Traversable
+ */
private function unboxMethods(array $boxed): \Traversable
{
$unpack = static function () use ($boxed) {
diff --git a/src/Internal/Declaration/Prototype/WorkflowPrototype.php b/src/Internal/Declaration/Prototype/WorkflowPrototype.php
index 328d0716b..da5d310ac 100644
--- a/src/Internal/Declaration/Prototype/WorkflowPrototype.php
+++ b/src/Internal/Declaration/Prototype/WorkflowPrototype.php
@@ -13,6 +13,7 @@
use Temporal\Common\CronSchedule;
use Temporal\Common\MethodRetry;
+use Temporal\Common\Versioning\VersioningBehavior;
use Temporal\Internal\Declaration\EntityNameValidator;
use Temporal\Workflow\ReturnType;
use Temporal\Workflow\WorkflowInit;
@@ -43,12 +44,14 @@ final class WorkflowPrototype extends Prototype
private ?MethodRetry $methodRetry = null;
private ?ReturnType $returnType = null;
private bool $hasInitializer = false;
+ private VersioningBehavior $versioningBehavior;
public function __construct(
string $name,
?\ReflectionMethod $handler,
\ReflectionClass $class,
) {
+ $this->versioningBehavior = VersioningBehavior::Unspecified;
EntityNameValidator::validateWorkflow($name);
parent::__construct($name, $handler, $class);
}
@@ -96,6 +99,11 @@ public function setReturnType(?ReturnType $attribute): void
$this->returnType = $attribute;
}
+ public function setVersioningBehavior(VersioningBehavior $behavior): void
+ {
+ $this->versioningBehavior = $behavior;
+ }
+
public function addQueryHandler(QueryDefinition $definition): void
{
EntityNameValidator::validateQueryMethod($definition->name);
@@ -156,4 +164,9 @@ public function getValidateUpdateHandlers(): array
{
return $this->updateValidators;
}
+
+ public function getVersioningBehavior(): VersioningBehavior
+ {
+ return $this->versioningBehavior;
+ }
}
diff --git a/src/Internal/Declaration/Reader/WorkflowReader.php b/src/Internal/Declaration/Reader/WorkflowReader.php
index af64f6473..d97877a22 100644
--- a/src/Internal/Declaration/Reader/WorkflowReader.php
+++ b/src/Internal/Declaration/Reader/WorkflowReader.php
@@ -27,6 +27,7 @@
use Temporal\Workflow\WorkflowInit;
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
+use Temporal\Workflow\WorkflowVersioningBehavior;
/**
* @template-extends Reader
@@ -292,14 +293,16 @@ private function getAttributedMethod(ClassNode $graph, \ReflectionMethod $handle
}
/**
+ * Walk through the method hierarchy and build the prototype for the workflow method.
+ *
* @throws \ReflectionException
*/
private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?WorkflowPrototype
{
- $cronSchedule = $previousRetry = $prototype = $returnType = null;
+ $cronSchedule = $previousRetry = $prototype = $returnType = $versionBehavior = null;
+ /** @var \Traversable $group */
foreach ($graph->getMethods($handler->getName()) as $group) {
- //
$contextualRetry = $previousRetry;
foreach ($group as $method) {
@@ -326,6 +329,11 @@ private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?Wo
?? $returnType
;
+ // Version Behavior
+ $versionBehavior = $this->reader->firstFunctionMetadata($method, WorkflowVersioningBehavior::class)
+ ?? $versionBehavior
+ ;
+
//
// In the future, workflow methods are available only in
// those classes that contain the attribute:
@@ -347,27 +355,18 @@ private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?Wo
}
}
- // In case
+ // Skip if no interface found
if ($interface === null) {
continue;
}
\assert($context !== null);
- if ($prototype === null) {
- $prototype = $this->findProto($handler, $method, $context, $graph->getReflection());
- }
-
- if ($prototype !== null && $retry !== null) {
- $prototype->setMethodRetry($retry);
- }
-
- if ($prototype !== null && $cronSchedule !== null) {
- $prototype->setCronSchedule($cronSchedule);
- }
+ $prototype ??= $this->findProto($handler, $method, $context, $graph->getReflection());
- if ($prototype !== null && $returnType !== null) {
- $prototype->setReturnType($returnType);
- }
+ $retry === null or $prototype?->setMethodRetry($retry);
+ $cronSchedule === null or $prototype?->setCronSchedule($cronSchedule);
+ $returnType === null or $prototype?->setReturnType($returnType);
+ $versionBehavior === null or $prototype?->setVersioningBehavior($versionBehavior->value);
}
$previousRetry = $contextualRetry;
diff --git a/src/Internal/Transport/Router/GetWorkerInfo.php b/src/Internal/Transport/Router/GetWorkerInfo.php
index 709257278..915141de7 100644
--- a/src/Internal/Transport/Router/GetWorkerInfo.php
+++ b/src/Internal/Transport/Router/GetWorkerInfo.php
@@ -44,10 +44,10 @@ public function handle(ServerRequestInterface $request, array $headers, Deferred
private function workerToArray(WorkerInterface $worker): array
{
$workflowMap = static fn(WorkflowPrototype $workflow): array => [
- 'Name' => $workflow->getID(),
- 'Queries' => \array_keys($workflow->getQueryHandlers()),
- 'Signals' => \array_keys($workflow->getSignalHandlers()),
- // 'Updates' => $this->keys($workflow->getUpdateHandlers()),
+ 'name' => $workflow->getID(),
+ 'queries' => \array_keys($workflow->getQueryHandlers()),
+ 'signals' => \array_keys($workflow->getSignalHandlers()),
+ 'versioning_behavior' => $workflow->getVersioningBehavior()->value,
];
$activityMap = static fn(ActivityPrototype $activity): array => [
diff --git a/src/Worker/Transport/RoadRunner.php b/src/Worker/Transport/RoadRunner.php
index 955a214a3..dea3a9c78 100644
--- a/src/Worker/Transport/RoadRunner.php
+++ b/src/Worker/Transport/RoadRunner.php
@@ -39,7 +39,7 @@ final class RoadRunner implements HostConnectionInterface
private RoadRunnerWorker $worker;
private CodecInterface $codec;
- public function __construct(RoadRunnerWorker $worker)
+ private function __construct(RoadRunnerWorker $worker)
{
$this->worker = $worker;
$this->codec = new JsonCodec();
diff --git a/src/Worker/WorkerDeploymentOptions.php b/src/Worker/WorkerDeploymentOptions.php
new file mode 100644
index 000000000..f8f2f7ef2
--- /dev/null
+++ b/src/Worker/WorkerDeploymentOptions.php
@@ -0,0 +1,79 @@
+useVersioning = false;
+ $this->version = null;
+ $this->defaultVersioningBehavior = VersioningBehavior::Unspecified;
+ }
+
+ public static function new(): self
+ {
+ return new self();
+ }
+
+ /**
+ * If set, opts this worker into the Worker Deployment Versioning feature.
+ *
+ * It will only operate on workflows it claims to be compatible with.
+ * You must also call {@see self::withVersion()} if this flag is true.
+ */
+ public function withUseVersioning(bool $value): self
+ {
+ /** @see self::$useVersioning */
+ return $this->with('useVersioning', $value);
+ }
+
+ /**
+ * Sets the version of the worker deployment.
+ *
+ * @param non-empty-string|WorkerDeploymentVersion $version
+ */
+ public function withVersion(string|WorkerDeploymentVersion $version): self
+ {
+ /** @see self::$version */
+ return $this->with('version', \is_string($version) ? WorkerDeploymentVersion::fromString($version) : $version);
+ }
+
+ /**
+ * Sets the default versioning behavior for this worker.
+ *
+ */
+ public function withDefaultVersioningBehavior(VersioningBehavior $behavior): self
+ {
+ /** @see self::$defaultVersioningBehavior */
+ return $this->with('defaultVersioningBehavior', $behavior);
+ }
+}
diff --git a/src/Worker/WorkerOptions.php b/src/Worker/WorkerOptions.php
index 2a287496f..edf86c4b6 100644
--- a/src/Worker/WorkerOptions.php
+++ b/src/Worker/WorkerOptions.php
@@ -22,6 +22,16 @@
/**
* @psalm-import-type DateIntervalValue from DateInterval
+ *
+ * todo: see {@see AutoscalingPollerBehavior}
+ * todo: see {@see SimplePollerBehavior}
+ * todo: see {@see \Temporal\Api\Sdk\V1\WorkerConfig}
+ * todo: see {@see \Temporal\Api\Taskqueue\V1\TaskQueueConfig}
+ * todo: see {@see \Temporal\Api\Worker\V1\PluginInfo}
+ * todo: see {@see \Temporal\Api\Worker\V1\WorkerInfo}
+ * todo: see {@see \Temporal\Api\Worker\V1\WorkerPollerInfo}
+ *
+ * @see WorkerOptions
*/
class WorkerOptions
{
@@ -295,7 +305,7 @@ class WorkerOptions
* and is used to provide a unique identifier for a set of worker code, and is necessary
* to opt in to the Worker Versioning feature. See {@see self::$useBuildIDForVersioning}.
*
- * @internal Experimental
+ * @deprecated
*/
#[Marshal(name: 'BuildID')]
public string $buildID = '';
@@ -305,12 +315,22 @@ class WorkerOptions
* It will only operate on workflows it claims to be compatible with.
* You must set {@see self::$buildID} if this flag is true.
*
- * @internal Experimental
* @note Cannot be enabled at the same time as {@see self::$enableSessionWorker}
+ * @deprecated
*/
#[Marshal(name: 'UseBuildIDForVersioning')]
public bool $useBuildIDForVersioning = false;
+ /**
+ * Optional: If set it configures Worker Versioning for this worker.
+ *
+ * @since SDK 2.16.0
+ * @since RoadRunner 2025.1.3
+ * @internal Experimental.
+ */
+ #[Marshal(name: 'DeploymentOptions')]
+ public WorkerDeploymentOptions $deploymentOptions;
+
#[Pure]
public static function new(): self
{
@@ -802,7 +822,7 @@ public function withDisableRegistrationAliasing(bool $disable = true): self
*
* @param non-empty-string $buildID
*
- * @internal Experimental
+ * @deprecated
*/
#[Pure]
public function withBuildID(string $buildID): self
@@ -817,8 +837,8 @@ public function withBuildID(string $buildID): self
* It will only operate on workflows it claims to be compatible with.
* You must set {@see self::$buildID} if this flag is true.
*
- * @internal Experimental
* @note Cannot be enabled at the same time as {@see self::$enableSessionWorker}
+ * @deprecated
*/
#[Pure]
public function withUseBuildIDForVersioning(bool $useBuildIDForVersioning = true): self
@@ -827,4 +847,19 @@ public function withUseBuildIDForVersioning(bool $useBuildIDForVersioning = true
$self->useBuildIDForVersioning = $useBuildIDForVersioning;
return $self;
}
+
+ /**
+ * Set deployment options for the worker.
+ *
+ * @since SDK 2.16.0
+ * @since RoadRunner 2025.1.3
+ * @internal Experimental.
+ */
+ #[Pure]
+ public function withDeploymentOptions(WorkerDeploymentOptions $deploymentOptions): self
+ {
+ $self = clone $this;
+ $self->deploymentOptions = $deploymentOptions;
+ return $self;
+ }
}
diff --git a/src/Workflow/WorkflowVersioningBehavior.php b/src/Workflow/WorkflowVersioningBehavior.php
new file mode 100644
index 000000000..605e9c020
--- /dev/null
+++ b/src/Workflow/WorkflowVersioningBehavior.php
@@ -0,0 +1,39 @@
+systemInfo->temporalCliExecutable,
"server", "start-dev",
"--port", $temporalPort,
- '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecution=true',
- '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true',
- '--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true',
'--log-level', 'error',
'--headless',
...$parameters,
@@ -202,4 +199,19 @@ public function stop(): void
$this->output->writeln('done.');
}
}
+
+ /**
+ * @internal
+ */
+ public function executeTemporalCommand(array|string $command, int $timeout = 10): void
+ {
+ $command = \array_merge(
+ [$this->systemInfo->temporalCliExecutable],
+ (array) $command,
+ );
+
+ $process = new Process($command);
+ $process->setTimeout($timeout);
+ $process->run();
+ }
}
diff --git a/tests/Acceptance/App/Attribute/Worker.php b/tests/Acceptance/App/Attribute/Worker.php
index 7530e98e3..819806921 100644
--- a/tests/Acceptance/App/Attribute/Worker.php
+++ b/tests/Acceptance/App/Attribute/Worker.php
@@ -19,11 +19,12 @@
final class Worker
{
/**
+ * @param array|null $options Callable that returns {@see WorkerOptions}
* @param array|null $pipelineProvider Callable that returns {@see PipelineProvider}
* @param array|null $logger Callable that returns {@see LoggerInterface}
*/
public function __construct(
- public readonly ?WorkerOptions $options = null,
+ public readonly ?array $options = null,
public readonly ?array $pipelineProvider = null,
public readonly ?array $logger = null,
) {}
diff --git a/tests/Acceptance/App/Feature/WorkerFactory.php b/tests/Acceptance/App/Feature/WorkerFactory.php
index f8ad4d260..b6c007a71 100644
--- a/tests/Acceptance/App/Feature/WorkerFactory.php
+++ b/tests/Acceptance/App/Feature/WorkerFactory.php
@@ -36,7 +36,7 @@ public function createWorker(
...$feature->activities,
);
if ($attr !== null) {
- $options = $attr->options;
+ $attr->options === null or $options = $this->invoker->invoke($attr->options);
$attr->pipelineProvider === null or $interceptorProvider = $this->invoker->invoke($attr->pipelineProvider);
$attr->logger === null or $logger = $this->invoker->invoke($attr->logger);
}
diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php
index f987db5a8..7ef1a530b 100644
--- a/tests/Acceptance/App/Runtime/TemporalStarter.php
+++ b/tests/Acceptance/App/Runtime/TemporalStarter.php
@@ -4,6 +4,7 @@
namespace Temporal\Tests\Acceptance\App\Runtime;
+use Symfony\Component\Process\Process;
use Temporal\Common\SearchAttributes\ValueType;
use Temporal\Testing\Environment;
@@ -26,8 +27,13 @@ public function start(): void
$this->environment->startTemporalServer(
parameters: [
+ '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecution=true',
+ '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true',
+ '--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true',
'--dynamic-config-value', 'system.enableEagerWorkflowStart=true',
'--dynamic-config-value', 'frontend.activityAPIsEnabled=true',
+ '--dynamic-config-value', 'frontend.workerVersioningWorkflowAPIs=true',
+ '--dynamic-config-value', 'system.enableDeploymentVersions=true',
],
searchAttributes: [
'foo' => ValueType::Text->value,
@@ -44,13 +50,25 @@ public function start(): void
$this->started = true;
}
- public function stop(): void
+ public function executeTemporalCommand(array|string $command, int $timeout = 10): void
+ {
+ $this->environment->executeTemporalCommand(
+ command: $command,
+ timeout: $timeout,
+ );
+ }
+
+ /**
+ * @return bool Returns true if the server was stopped successfully, false if it was not started.
+ */
+ public function stop(): bool
{
if (!$this->started) {
- return;
+ return false;
}
$this->environment->stop();
$this->started = false;
+ return true;
}
}
diff --git a/tests/Acceptance/Extra/Versioning/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json
similarity index 66%
rename from tests/Acceptance/Extra/Versioning/Versioning-default.json
rename to tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json
index 2457bebcb..768be5a75 100644
--- a/tests/Acceptance/Extra/Versioning/Versioning-default.json
+++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json
@@ -2,37 +2,38 @@
"events": [
{
"eventId": "1",
- "eventTime": "2025-05-10T11:08:43.136190100Z",
+ "eventTime": "2025-08-18T07:43:35.810544500Z",
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED",
- "taskId": "1048687",
+ "taskId": "1048849",
"workflowExecutionStartedEventAttributes": {
"workflowType": {
- "name": "Extra_Versioning_Versioning"
+ "name": "Extra_Versioning_Classic"
},
"taskQueue": {
- "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning",
+ "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic",
"kind": "TASK_QUEUE_KIND_NORMAL"
},
"workflowExecutionTimeout": "60s",
"workflowRunTimeout": "60s",
"workflowTaskTimeout": "10s",
- "originalExecutionRunId": "0196b9e2-1300-72e6-a156-df5a49685436",
- "identity": "42484@roxblnfk-book",
- "firstExecutionRunId": "0196b9e2-1300-72e6-a156-df5a49685436",
+ "originalExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556",
+ "identity": "14828@roxblnfk-book",
+ "firstExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556",
"attempt": 1,
- "workflowExecutionExpirationTime": "2025-05-10T11:09:43.136Z",
+ "workflowExecutionExpirationTime": "2025-08-18T07:44:35.810Z",
"firstWorkflowTaskBackoff": "0s",
- "workflowId": "b2c07b59-cb14-4e46-8650-0da7f50d6f0a"
+ "workflowId": "4a4cefaa-3615-4571-969a-d4e5eb489361",
+ "priority": {}
}
},
{
"eventId": "2",
- "eventTime": "2025-05-10T11:08:43.136190100Z",
+ "eventTime": "2025-08-18T07:43:35.810544500Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
- "taskId": "1048688",
+ "taskId": "1048850",
"workflowTaskScheduledEventAttributes": {
"taskQueue": {
- "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning",
+ "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic",
"kind": "TASK_QUEUE_KIND_NORMAL"
},
"startToCloseTimeout": "10s",
@@ -41,30 +42,30 @@
},
{
"eventId": "3",
- "eventTime": "2025-05-10T11:08:43.138391700Z",
+ "eventTime": "2025-08-18T07:43:35.811577500Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
- "taskId": "1048694",
+ "taskId": "1048856",
"workflowTaskStartedEventAttributes": {
"scheduledEventId": "2",
- "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:44882e6a-8930-4f81-b987-69210c955363",
- "requestId": "32693089-d58e-4058-be32-f10928a5ab0d",
- "historySizeBytes": "372",
+ "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316",
+ "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268",
+ "historySizeBytes": "373",
"workerVersion": {
- "buildId": "f56858783d7ba07ee263f7740e6a9993"
+ "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7"
}
}
},
{
"eventId": "4",
- "eventTime": "2025-05-10T11:08:43.160702200Z",
+ "eventTime": "2025-08-18T07:43:35.832299300Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
- "taskId": "1048698",
+ "taskId": "1048860",
"workflowTaskCompletedEventAttributes": {
"scheduledEventId": "2",
"startedEventId": "3",
- "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:44882e6a-8930-4f81-b987-69210c955363",
+ "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316",
"workerVersion": {
- "buildId": "f56858783d7ba07ee263f7740e6a9993"
+ "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7"
},
"sdkMetadata": {
"langUsedFlags": [
@@ -78,9 +79,9 @@
},
{
"eventId": "5",
- "eventTime": "2025-05-10T11:08:43.160702200Z",
+ "eventTime": "2025-08-18T07:43:35.832299300Z",
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED",
- "taskId": "1048699",
+ "taskId": "1048861",
"workflowExecutionCompletedEventAttributes": {
"result": {
"payloads": [
diff --git a/tests/Acceptance/Extra/Versioning/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json
similarity index 76%
rename from tests/Acceptance/Extra/Versioning/Versioning-v1.json
rename to tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json
index a9848983f..394c8175b 100644
--- a/tests/Acceptance/Extra/Versioning/Versioning-v1.json
+++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json
@@ -2,37 +2,38 @@
"events": [
{
"eventId": "1",
- "eventTime": "2025-05-10T10:58:43.335213500Z",
+ "eventTime": "2025-08-18T07:43:10.001148600Z",
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED",
- "taskId": "1048607",
+ "taskId": "1048829",
"workflowExecutionStartedEventAttributes": {
"workflowType": {
- "name": "Extra_Versioning_Versioning"
+ "name": "Extra_Versioning_Classic"
},
"taskQueue": {
- "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning",
+ "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic",
"kind": "TASK_QUEUE_KIND_NORMAL"
},
"workflowExecutionTimeout": "60s",
"workflowRunTimeout": "60s",
"workflowTaskTimeout": "10s",
- "originalExecutionRunId": "0196b9d8-ec07-7341-b56b-620f48a7eba9",
- "identity": "36696@roxblnfk-book",
- "firstExecutionRunId": "0196b9d8-ec07-7341-b56b-620f48a7eba9",
+ "originalExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880",
+ "identity": "40464@roxblnfk-book",
+ "firstExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880",
"attempt": 1,
- "workflowExecutionExpirationTime": "2025-05-10T10:59:43.335Z",
+ "workflowExecutionExpirationTime": "2025-08-18T07:44:10.001Z",
"firstWorkflowTaskBackoff": "0s",
- "workflowId": "31a01a98-7406-4130-b462-4adf7026b1db"
+ "workflowId": "2f535201-af15-477b-8759-a258f174b246",
+ "priority": {}
}
},
{
"eventId": "2",
- "eventTime": "2025-05-10T10:58:43.335213500Z",
+ "eventTime": "2025-08-18T07:43:10.001148600Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
- "taskId": "1048608",
+ "taskId": "1048830",
"workflowTaskScheduledEventAttributes": {
"taskQueue": {
- "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning",
+ "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic",
"kind": "TASK_QUEUE_KIND_NORMAL"
},
"startToCloseTimeout": "10s",
@@ -41,30 +42,30 @@
},
{
"eventId": "3",
- "eventTime": "2025-05-10T10:58:43.336783600Z",
+ "eventTime": "2025-08-18T07:43:10.002204400Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
- "taskId": "1048614",
+ "taskId": "1048836",
"workflowTaskStartedEventAttributes": {
"scheduledEventId": "2",
- "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:b7e7bdac-03cb-40fe-bc45-896345d005f9",
- "requestId": "29e46cf8-0357-40f7-9d82-e1b13b42dcdf",
- "historySizeBytes": "375",
+ "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88",
+ "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968",
+ "historySizeBytes": "367",
"workerVersion": {
- "buildId": "f56858783d7ba07ee263f7740e6a9993"
+ "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7"
}
}
},
{
"eventId": "4",
- "eventTime": "2025-05-10T10:58:43.378803600Z",
+ "eventTime": "2025-08-18T07:43:10.045812500Z",
"eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
- "taskId": "1048618",
+ "taskId": "1048840",
"workflowTaskCompletedEventAttributes": {
"scheduledEventId": "2",
"startedEventId": "3",
- "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:b7e7bdac-03cb-40fe-bc45-896345d005f9",
+ "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88",
"workerVersion": {
- "buildId": "f56858783d7ba07ee263f7740e6a9993"
+ "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7"
},
"sdkMetadata": {
"langUsedFlags": [
@@ -79,9 +80,9 @@
},
{
"eventId": "5",
- "eventTime": "2025-05-10T10:58:43.378803600Z",
+ "eventTime": "2025-08-18T07:43:10.045812500Z",
"eventType": "EVENT_TYPE_MARKER_RECORDED",
- "taskId": "1048619",
+ "taskId": "1048841",
"markerRecordedEventAttributes": {
"markerName": "Version",
"details": {
@@ -111,9 +112,9 @@
},
{
"eventId": "6",
- "eventTime": "2025-05-10T10:58:43.379333100Z",
+ "eventTime": "2025-08-18T07:43:10.046354200Z",
"eventType": "EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES",
- "taskId": "1048620",
+ "taskId": "1048842",
"upsertWorkflowSearchAttributesEventAttributes": {
"workflowTaskCompletedEventId": "4",
"searchAttributes": {
@@ -131,9 +132,9 @@
},
{
"eventId": "7",
- "eventTime": "2025-05-10T10:58:43.379333100Z",
+ "eventTime": "2025-08-18T07:43:10.046354200Z",
"eventType": "EVENT_TYPE_MARKER_RECORDED",
- "taskId": "1048621",
+ "taskId": "1048843",
"markerRecordedEventAttributes": {
"markerName": "SideEffect",
"details": {
@@ -163,9 +164,9 @@
},
{
"eventId": "8",
- "eventTime": "2025-05-10T10:58:43.379333100Z",
+ "eventTime": "2025-08-18T07:43:10.046354200Z",
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED",
- "taskId": "1048622",
+ "taskId": "1048844",
"workflowExecutionCompletedEventAttributes": {
"result": {
"payloads": [
@@ -173,7 +174,7 @@
"metadata": {
"encoding": "anNvbi9wbGFpbg=="
},
- "data": "InRlc3Qi"
+ "data": "InYxIg=="
}
]
},
diff --git a/tests/Acceptance/Extra/Versioning/VersioningTest.php b/tests/Acceptance/Extra/Versioning/ClassicTest.php
similarity index 70%
rename from tests/Acceptance/Extra/Versioning/VersioningTest.php
rename to tests/Acceptance/Extra/Versioning/ClassicTest.php
index 9ba4815a1..d46f278c6 100644
--- a/tests/Acceptance/Extra/Versioning/VersioningTest.php
+++ b/tests/Acceptance/Extra/Versioning/ClassicTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Temporal\Tests\Acceptance\Extra\Workflow\Versioning;
+namespace Temporal\Tests\Acceptance\Extra\Versioning\Classic;
use PHPUnit\Framework\Attributes\Test;
use Temporal\Activity\ActivityInterface;
@@ -16,20 +16,20 @@
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
-class VersioningTest extends TestCase
+class ClassicTest extends TestCase
{
#[Test]
- public function sendEmpty(
+ public function replayDifferentVersions(
#[Stub(
- type: 'Extra_Versioning_Versioning',
+ type: 'Extra_Versioning_Classic',
)] WorkflowStubInterface $stub,
): void {
$result = $stub->getResult();
self::assertSame('v2', $result);
$replayer = new WorkflowReplayer();
- $replayer->replayFromJSON('Extra_Versioning_Versioning', __DIR__ . '/Versioning-default.json');
- $replayer->replayFromJSON('Extra_Versioning_Versioning', __DIR__ . '/Versioning-v1.json');
+ $replayer->replayFromJSON('Extra_Versioning_Classic', __DIR__ . '/Classic/Versioning-default.json');
+ $replayer->replayFromJSON('Extra_Versioning_Classic', __DIR__ . '/Classic/Versioning-v1.json');
$replayer->replayFromServer($stub->getWorkflowType(), $stub->getExecution());
}
@@ -38,7 +38,7 @@ public function sendEmpty(
#[WorkflowInterface]
class TestWorkflow
{
- #[WorkflowMethod(name: "Extra_Versioning_Versioning")]
+ #[WorkflowMethod(name: "Extra_Versioning_Classic")]
public function handle()
{
$version = yield Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2);
@@ -50,7 +50,8 @@ public function handle()
if ($version === 2) {
return yield Workflow::executeActivity(
- 'Extra_Versioning_Versioning.handler',
+ /** @see TestActivity::handler() */
+ 'Extra_Versioning_Classic.handler',
args: ['v2'],
options: ActivityOptions::new()->withScheduleToCloseTimeout(5),
);
@@ -60,7 +61,7 @@ public function handle()
}
}
-#[ActivityInterface(prefix: 'Extra_Versioning_Versioning.')]
+#[ActivityInterface(prefix: 'Extra_Versioning_Classic.')]
class TestActivity
{
#[ActivityMethod]
diff --git a/tests/Acceptance/Extra/Versioning/DeploymentTest.php b/tests/Acceptance/Extra/Versioning/DeploymentTest.php
new file mode 100644
index 000000000..07365beb7
--- /dev/null
+++ b/tests/Acceptance/Extra/Versioning/DeploymentTest.php
@@ -0,0 +1,222 @@
+withWorkflowId($id),
+ );
+ # Check worker registration
+ self::assertSame(VersioningBehavior::Pinned, $behavior);
+
+ # Check Override from Search Attributes
+ $sa = $client->newUntypedRunningWorkflowStub(
+ workflowID: $id,
+ workflowType: 'Extra_Versioning_Deployment_Pinned',
+ )->describe()->info->searchAttributes->getValues();
+ self::assertSame('Pinned', $sa['TemporalWorkflowVersioningBehavior']);
+ self::assertSame('foo:baz', $sa['TemporalWorkerDeploymentVersion']);
+ }
+
+ #[Test]
+ public function versionBehaviorOverrideAutoUpgrade(
+ TemporalStarter $starter,
+ WorkflowClientInterface $client,
+ Feature $feature,
+ ): void {
+ $id = Uuid::v4();
+ $behavior = self::executeWorkflow(
+ $starter,
+ $client,
+ $feature,
+ /** @see PinnedWorkflow */
+ 'Extra_Versioning_Deployment_Pinned',
+ WorkflowOptions::new()
+ ->withWorkflowId($id)
+ ->withVersioningOverride(VersioningOverride::autoUpgrade()),
+ );
+
+ # Check worker registration
+ self::assertSame(VersioningBehavior::Pinned, $behavior);
+
+ # Check Override from Search Attributes
+ $sa = $client->newUntypedRunningWorkflowStub(
+ workflowID: $id,
+ workflowType: 'Extra_Versioning_Deployment_Pinned',
+ )->describe()->info->searchAttributes->getValues();
+ self::assertSame('AutoUpgrade', $sa['TemporalWorkflowVersioningBehavior']);
+ self::assertSame('foo:baz', $sa['TemporalWorkerDeploymentVersion']);
+ }
+
+ #[Test]
+ public function versionBehaviorOverridePinned(
+ TemporalStarter $starter,
+ WorkflowClientInterface $client,
+ Feature $feature,
+ ): void {
+ $behavior = self::executeWorkflow(
+ $starter,
+ $client,
+ $feature,
+ /** @see PinnedWorkflow */
+ 'Extra_Versioning_Deployment_Default',
+ WorkflowOptions::new()->withVersioningOverride(VersioningOverride::pinned(
+ version: WorkerDeploymentVersion::new(
+ deploymentName: WorkerFactory::DEPLOYMENT_NAME,
+ buildId: WorkerFactory::BUILD_ID,
+ ),
+ )),
+ );
+
+ # Check worker registration
+ self::assertSame(VersioningBehavior::AutoUpgrade, $behavior);
+ }
+
+ private static function executeWorkflow(
+ TemporalStarter $starter,
+ WorkflowClientInterface $client,
+ Feature $feature,
+ string $workflowType,
+ WorkflowOptions $options,
+ ): ?VersioningBehavior {
+ WorkerFactory::setCurrentDeployment($starter);
+
+ try {
+ # Create a Workflow stub with an execution timeout 12 seconds
+ $stub = $client
+ ->withTimeout(10)
+ ->newUntypedWorkflowStub(
+ /** @see PinnedWorkflow */
+ $workflowType,
+ $options
+ ->withTaskQueue($feature->taskQueue)
+ ->withWorkflowExecutionTimeout(20),
+ );
+
+ # Start the Workflow
+ $client->start($stub);
+
+ # Wait for the Workflow to complete
+ $stub->getResult(timeout: 5);
+
+ # Check the Workflow History
+ foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) {
+ if ($event->hasWorkflowTaskCompletedEventAttributes()) {
+ $version = $event->getWorkflowTaskCompletedEventAttributes()?->getDeploymentVersion();
+ self::assertNotNull($version);
+ self::assertSame(WorkerFactory::DEPLOYMENT_NAME, $version->getDeploymentName());
+ self::assertSame(WorkerFactory::BUILD_ID, $version->getBuildId());
+
+ return VersioningBehavior::tryFrom(
+ $event->getWorkflowTaskCompletedEventAttributes()?->getVersioningBehavior(),
+ );
+ }
+ }
+
+ throw new \RuntimeException('The WorkflowTaskCompletedEventAttributes not found in the Workflow history.');
+ } finally {
+ $starter->stop() and $starter->start();
+ }
+ }
+}
+
+class WorkerFactory
+{
+ public const DEPLOYMENT_NAME = 'foo';
+ public const BUILD_ID = 'baz';
+
+ public static function options(): WorkerOptions
+ {
+ return WorkerOptions::new()
+ ->withDeploymentOptions(
+ WorkerDeploymentOptions::new()
+ ->withUseVersioning(true)
+ ->withVersion(WorkerDeploymentVersion::new(self::DEPLOYMENT_NAME, self::BUILD_ID))
+ ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade),
+ );
+ }
+
+ public static function setCurrentDeployment(TemporalStarter $starter): void
+ {
+ $starter->executeTemporalCommand([
+ 'worker',
+ 'deployment',
+ 'set-current-version',
+ '--deployment-name', WorkerFactory::DEPLOYMENT_NAME,
+ '--build-id', WorkerFactory::BUILD_ID,
+ '--yes',
+ ], timeout: 5);
+ }
+}
+
+#[WorkflowInterface]
+class DefaultWorkflow
+{
+ #[WorkflowMethod(name: "Extra_Versioning_Deployment_Default")]
+ public function handle()
+ {
+ return 'default';
+ }
+}
+
+#[WorkflowInterface]
+class PinnedWorkflow
+{
+ #[WorkflowMethod(name: "Extra_Versioning_Deployment_Pinned")]
+ #[WorkflowVersioningBehavior(VersioningBehavior::Pinned)]
+ public function handle()
+ {
+ return 'pinned';
+ }
+}
diff --git a/tests/Unit/DTO/WorkerOptionsTestCase.php b/tests/Unit/DTO/WorkerOptionsTestCase.php
index d2b80ceca..ad004b50a 100644
--- a/tests/Unit/DTO/WorkerOptionsTestCase.php
+++ b/tests/Unit/DTO/WorkerOptionsTestCase.php
@@ -11,6 +11,9 @@
namespace Temporal\Tests\Unit\DTO;
+use Temporal\Common\Versioning\VersioningBehavior;
+use Temporal\Common\Versioning\WorkerDeploymentVersion;
+use Temporal\Worker\WorkerDeploymentOptions;
use Temporal\Worker\WorkerOptions;
use Temporal\Worker\WorkflowPanicPolicy;
@@ -50,10 +53,45 @@ public function testMarshalling(): void
'MaxConcurrentEagerActivityExecutionSize' => 0,
'DisableRegistrationAliasing' => false,
'BuildID' => "",
+ 'DeploymentOptions' => null,
'UseBuildIDForVersioning' => false,
];
- $this->assertSame($expected, $this->marshal($dto));
+ $this->assertEquals($expected, $this->marshal($dto));
+ }
+
+ public function testDeploymentOptionsNoUse(): void
+ {
+ $dto = new WorkerOptions();
+ $result = $dto->withDeploymentOptions(
+ WorkerDeploymentOptions::new()
+ ->withUseVersioning(false),
+ );
+
+ self::assertNotSame($dto, $result);
+ $options = $this->marshal($result)['DeploymentOptions'];
+
+ self::assertFalse($options['UseVersioning']);
+ self::assertSame(VersioningBehavior::Unspecified->value, $options['DefaultVersioningBehavior']);
+ self::assertNull($options['Version']);
+ }
+
+ public function testDeploymentOptionsUseVersion(): void
+ {
+ $dto = new WorkerOptions();
+ $result = $dto->withDeploymentOptions(
+ WorkerDeploymentOptions::new()
+ ->withUseVersioning(true)
+ ->withVersion(WorkerDeploymentVersion::new('foo', 'bar'))
+ ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade),
+ );
+
+ self::assertNotSame($dto, $result);
+ $options = $this->marshal($result)['DeploymentOptions'];
+
+ self::assertTrue($options['UseVersioning']);
+ self::assertSame(VersioningBehavior::AutoUpgrade->value, $options['DefaultVersioningBehavior']);
+ self::assertSame(['DeploymentName' => 'foo', 'BuildId' => 'bar'], $options['Version']);
}
public function testMaxConcurrentActivityExecutionSize(): void
diff --git a/tests/Unit/DTO/WorkflowOptionsTestCase.php b/tests/Unit/DTO/WorkflowOptionsTestCase.php
index b71054d18..074ea08c2 100644
--- a/tests/Unit/DTO/WorkflowOptionsTestCase.php
+++ b/tests/Unit/DTO/WorkflowOptionsTestCase.php
@@ -51,6 +51,7 @@ public function testMarshalling(): void
'Priority' => [
'priority_key' => 0,
],
+ 'VersioningOverride' => null,
];
$result = $this->marshal($dto);