From e9a4a4c7637cb2c8fed2edcf2d6d42a10744b603 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 8 Sep 2026 18:23:44 +0000 Subject: [PATCH 1/3] fix: authorize activity uploads by the current task lease --- app/Support/RuntimePayloadCompletionLease.php | 12 ++++-- .../RuntimePayloadCompletionUploadsTest.php | 43 +++++++++++++++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/Support/RuntimePayloadCompletionLease.php b/app/Support/RuntimePayloadCompletionLease.php index 2d20e4ed..27022196 100644 --- a/app/Support/RuntimePayloadCompletionLease.php +++ b/app/Support/RuntimePayloadCompletionLease.php @@ -18,10 +18,14 @@ public function __construct( public function expiresAt(string $namespace, RuntimePayloadCompletionContext $context): CarbonImmutable { - $worker = WorkerRegistration::query()->where('namespace', $namespace) - ->where('worker_id', $context->leaseOwner)->first(); - if (! $worker instanceof WorkerRegistration || ! WorkerPollFence::isFresh($worker)) { - throw self::rejected(); + // Activities heartbeat their own leases while application code runs; + // their worker roster can age or drain without revoking that authority. + if ($context->kind !== 'activity') { + $worker = WorkerRegistration::query()->where('namespace', $namespace) + ->where('worker_id', $context->leaseOwner)->first(); + if (! $worker instanceof WorkerRegistration || ! WorkerPollFence::isFresh($worker)) { + throw self::rejected(); + } } $expires = match ($context->kind) { diff --git a/tests/Feature/RuntimePayloadCompletionUploadsTest.php b/tests/Feature/RuntimePayloadCompletionUploadsTest.php index ba876afd..a32d7e2c 100644 --- a/tests/Feature/RuntimePayloadCompletionUploadsTest.php +++ b/tests/Feature/RuntimePayloadCompletionUploadsTest.php @@ -58,9 +58,11 @@ protected function tearDown(): void parent::tearDown(); } - public function test_owned_activity_result_can_upload_complete_and_reconcile_after_lease_closes(): void + #[DataProvider('completionRosterStates')] + public function test_owned_activity_result_can_upload_complete_and_reconcile_after_lease_closes(string $roster): void { $context = $this->activity(); + $this->setCompletionRoster($roster); $payload = Serializer::serializeWithCodec('avro', str_repeat('result', 200)); $this->observeStoragePressure('draining'); $reference = $this->upload($payload, $context)->assertCreated()->json('reference'); @@ -84,11 +86,20 @@ public function test_owned_activity_result_can_upload_complete_and_reconcile_aft ])->assertOk()->assertStreamedContent($payload); } - public function test_workflow_completion_upload_can_be_committed_during_draining(): void + #[DataProvider('completionRosterStates')] + public function test_workflow_completion_upload_can_be_committed_during_draining(string $roster): void { $context = $this->workflow(); + $this->setCompletionRoster($roster); $payload = Serializer::serializeWithCodec('avro', ['result' => str_repeat('x', 100)]); $this->observeStoragePressure('draining'); + if ($roster !== 'active') { + $this->upload($payload, $context)->assertStatus(409) + ->assertJsonPath('reason', 'external_payload_completion_lease_rejected'); + $this->assertDatabaseCount('runtime_payload_completion_budgets', 0); + + return; + } $reference = $this->upload($payload, $context)->assertCreated()->json('reference'); $this->postJson('/api/worker/workflow-tasks/'.$context['task_id'].'/complete', [ 'lease_owner' => 'worker', 'workflow_task_attempt' => $context['attempt'], @@ -100,7 +111,8 @@ public function test_workflow_completion_upload_can_be_committed_during_draining $this->upload($payload, $context)->assertCreated()->assertJsonPath('reference', $reference); } - public function test_query_completion_upload_uses_its_own_current_lease(): void + #[DataProvider('completionRosterStates')] + public function test_query_completion_upload_uses_its_own_current_lease(string $roster): void { $this->postJson('/api/workflows', ['workflow_id' => 'query-workflow', 'workflow_type' => 'tests.external-greeting-workflow', 'task_queue' => 'queue', 'input' => []], @@ -113,6 +125,7 @@ public function test_query_completion_upload_uses_its_own_current_lease(): void $task = $this->postJson('/api/worker/query-tasks/poll', ['worker_id' => 'worker', 'task_queue' => 'queue'], $this->workerHeaders())->assertOk()->json('task'); $this->assertIsArray($task); + $this->setCompletionRoster($roster); $context = ['schema' => RuntimePayloadCompletionContext::SCHEMA, 'kind' => 'query', 'task_id' => $task['query_task_id'], 'attempt' => $task['query_task_attempt'], 'lease_owner' => 'worker', 'operation' => 'complete', 'slot' => ['result_envelope']]; @@ -121,6 +134,13 @@ public function test_query_completion_upload_uses_its_own_current_lease(): void $wrong['attempt']++; $this->upload('bytes', $wrong)->assertStatus(409); $payload = Serializer::serializeWithCodec('avro', ['status' => str_repeat('ready', 50)]); + if ($roster !== 'active') { + $this->upload($payload, $context)->assertStatus(409) + ->assertJsonPath('reason', 'external_payload_completion_lease_rejected'); + $this->assertDatabaseCount('runtime_payload_completion_budgets', 0); + + return; + } $reference = $this->upload($payload, $context)->assertCreated()->json('reference'); $this->postJson('/api/worker/query-tasks/'.$context['task_id'].'/complete', [ 'lease_owner' => 'worker', 'query_task_attempt' => $context['attempt'], @@ -305,6 +325,23 @@ public function test_cleanup_does_not_reset_an_expired_but_renewable_activity_bu $this->assertDatabaseCount('runtime_payload_completion_budgets', 0); } + public static function completionRosterStates(): array + { + return [['active'], ['stale'], ['draining'], ['removed']]; + } + + private function setCompletionRoster(string $state): void + { + $worker = WorkerRegistration::query()->where('worker_id', 'worker'); + if ($state === 'removed') { + $worker->delete(); + } elseif ($state === 'stale') { + $worker->update(['last_heartbeat_at' => now()->subDay()]); + } else { + $worker->update(['status' => $state]); + } + } + private function activity(): array { $this->postJson('/api/activities', ['activity_id' => 'activity', From 3c9b2a87aec100fbc4648ede3efc770331d7e128 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 8 Sep 2026 18:38:26 +0000 Subject: [PATCH 2/3] fix: avoid missing-row gap locks for completion budgets --- .../RuntimePayloadCompletionUploads.php | 9 ++- .../RuntimePayloadCompletionProcessTest.php | 38 ++++++++++++- .../RuntimePayloadCompletionProcess.php | 56 ++++++++++++------- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/app/Support/RuntimePayloadCompletionUploads.php b/app/Support/RuntimePayloadCompletionUploads.php index c7deea05..72d3aaaa 100644 --- a/app/Support/RuntimePayloadCompletionUploads.php +++ b/app/Support/RuntimePayloadCompletionUploads.php @@ -48,9 +48,12 @@ public function upload( $scope = $context->scope($namespace); $this->locks->transaction($scope, function () use ($namespace, $context, $sha256, $data, $expiresAt, $scope): void { $this->requireWritable(); - $budget = RuntimePayloadCompletionBudget::query()->lockForUpdate()->find($scope) - ?? new RuntimePayloadCompletionBudget(['id' => $scope, 'namespace' => $namespace, - 'context' => $context->toArray(), 'slots' => [], 'objects' => []]); + // Insert before a locking read: missing-row locks on different IDs + // can share an InnoDB gap and deadlock concurrent first reservations. + $budget = RuntimePayloadCompletionBudget::query()->lockForUpdate()->createOrFirst( + ['id' => $scope], ['namespace' => $namespace, 'context' => $context->toArray(), + 'slots' => [], 'objects' => [], 'expires_at' => $expiresAt->addSeconds($this->retryRetention())], + ); $slots = $budget->slots; $objects = $budget->objects; $slot = $context->slotIdentity(); diff --git a/tests/Feature/RuntimePayloadCompletionProcessTest.php b/tests/Feature/RuntimePayloadCompletionProcessTest.php index e5f0257a..ea7180b2 100644 --- a/tests/Feature/RuntimePayloadCompletionProcessTest.php +++ b/tests/Feature/RuntimePayloadCompletionProcessTest.php @@ -74,6 +74,42 @@ public function test_concurrent_uploads_and_cold_retries_preserve_one_bounded_le self::assertSame(409, $this->runProbe('upload', $kind, $loser, '1')['status']); } + public static function nativeDatabases(): array + { + return [['mysql'], ['pgsql']]; + } + + #[DataProvider('nativeDatabases')] + public function test_independent_activity_budgets_can_be_created_concurrently(string $driver): void + { + $this->initialize($driver); + $this->runProbe('init-independent', 'activity'); + $uploads = []; + foreach (range(0, 7) as $member) { + $uploads[$member] = $this->probe('upload-independent', 'activity', sprintf('%05d', $member), + (string) $member, 'member-'.$member); + $uploads[$member]->start(); + } + $deadline = microtime(true) + 10; + foreach ($uploads as $member => $process) { + while (! is_file($this->directory.'/member-'.$member.'.ready') && $process->isRunning() && microtime(true) < $deadline) { + usleep(10000); + } + self::assertFileExists($this->directory.'/member-'.$member.'.ready', $process->getErrorOutput()); + } + touch($this->directory.'/go'); + $responses = []; + foreach ($uploads as $member => $process) { + $responses[$member] = $this->completedResult($process); + self::assertSame(201, $responses[$member]['status'], json_encode($responses[$member])); + } + foreach ($responses as $member => $response) { + self::assertSame($response, $this->runProbe('upload-independent', 'activity', sprintf('%05d', $member), (string) $member)); + } + self::assertSame(['budgets' => 8, 'slots' => 8, 'objects' => 8, 'rows' => 8], + $this->runProbe('status-independent', 'activity')); + } + protected function tearDown(): void { foreach ($this->processes as $process) { @@ -122,7 +158,7 @@ private function probe(string $action, string $kind, string $variant = 'alpha', { $process = new Process([PHP_BINARY, 'tests/Support/RuntimePayloadCompletionProcess.php', $action, $this->directory, $kind, $variant, $slot, $barrier], dirname(__DIR__, 2), $this->environment, - timeout: $action === 'init' ? 120 : 30); + timeout: str_starts_with($action, 'init') ? 120 : 30); $this->processes[] = $process; return $process; diff --git a/tests/Support/RuntimePayloadCompletionProcess.php b/tests/Support/RuntimePayloadCompletionProcess.php index 393e07b9..f58997be 100644 --- a/tests/Support/RuntimePayloadCompletionProcess.php +++ b/tests/Support/RuntimePayloadCompletionProcess.php @@ -39,38 +39,54 @@ return ['status' => $response->getStatusCode(), 'body' => json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR)]; }; - if ($action === 'init') { + if (in_array($action, ['init', 'init-independent'], true)) { file_put_contents($directory.'/pressure.json', json_encode(['schema' => StoragePressure::SCHEMA, 'source' => 'completion-process-test', 'observed_at' => time(), 'state' => 'normal'], JSON_THROW_ON_ERROR)); Artisan::call('migrate:fresh', ['--force' => true]); WorkflowNamespace::query()->updateOrCreate(['name' => 'default'], ['status' => 'active', 'retention_days' => 30, 'external_payload_storage' => ['driver' => 'local', 'enabled' => true, 'threshold_bytes' => 32, 'config' => ['uri' => 'file://'.$directory.'/objects']]]); - WorkerRegistration::query()->create(['worker_id' => 'worker', 'namespace' => 'default', 'task_queue' => 'queue', - 'runtime' => 'php', 'supported_workflow_types' => ['test.workflow'], 'supported_activity_types' => ['test.activity'], - 'last_heartbeat_at' => now(), 'status' => 'active']); - $plural = $kind === 'activity' ? 'activities' : 'workflows'; - $started = $request('/api/'.$plural, [$kind.'_id' => 'test', $kind.'_type' => 'test.'.$kind, - 'task_queue' => 'queue', 'input' => []]); - if ($started['status'] !== 201) { - throw new RuntimeException(json_encode($started, JSON_THROW_ON_ERROR)); - } - $polled = $request('/api/worker/'.$kind.'-tasks/poll', ['worker_id' => 'worker', 'task_queue' => 'queue']); - $task = $polled['body']['task'] ?? null; - if (! is_array($task)) { - throw new RuntimeException(json_encode($polled, JSON_THROW_ON_ERROR)); + foreach (range(0, $action === 'init-independent' ? 7 : 0) as $member) { + $worker = $member === 0 ? 'worker' : 'worker-'.$member; + WorkerRegistration::query()->create(['worker_id' => $worker, 'namespace' => 'default', 'task_queue' => 'queue', + 'runtime' => 'php', 'supported_workflow_types' => ['test.workflow'], 'supported_activity_types' => ['test.activity'], + 'last_heartbeat_at' => now(), 'status' => 'active']); + $plural = $kind === 'activity' ? 'activities' : 'workflows'; + $started = $request('/api/'.$plural, [$kind.'_id' => 'test-'.$member, $kind.'_type' => 'test.'.$kind, + 'task_queue' => 'queue', 'input' => []]); + if ($started['status'] !== 201) { + throw new RuntimeException(json_encode($started, JSON_THROW_ON_ERROR)); + } + $polled = $request('/api/worker/'.$kind.'-tasks/poll', ['worker_id' => $worker, 'task_queue' => 'queue']); + $task = $polled['body']['task'] ?? null; + if (! is_array($task)) { + throw new RuntimeException(json_encode($polled, JSON_THROW_ON_ERROR)); + } + $context = ['schema' => RuntimePayloadCompletionContext::SCHEMA, 'kind' => $kind, + 'task_id' => $task['task_id'], 'attempt' => $task[$kind === 'activity' ? 'activity_attempt_id' : 'workflow_task_attempt'], + 'lease_owner' => $worker, 'operation' => 'complete', + 'slot' => $kind === 'activity' ? ['result'] : ['commands', 0, 'result']]; + $contextFile = $action === 'init-independent' ? '/context-'.$member.'.json' : '/context.json'; + file_put_contents($directory.$contextFile, json_encode($context, JSON_THROW_ON_ERROR)); } - $context = ['schema' => RuntimePayloadCompletionContext::SCHEMA, 'kind' => $kind, - 'task_id' => $task['task_id'], 'attempt' => $task[$kind === 'activity' ? 'activity_attempt_id' : 'workflow_task_attempt'], - 'lease_owner' => 'worker', 'operation' => 'complete', - 'slot' => $kind === 'activity' ? ['result'] : ['commands', 0, 'result']]; - file_put_contents($directory.'/context.json', json_encode($context, JSON_THROW_ON_ERROR)); file_put_contents($directory.'/pressure.json', json_encode(['schema' => StoragePressure::SCHEMA, 'source' => 'completion-process-test', 'observed_at' => time(), 'state' => 'draining'], JSON_THROW_ON_ERROR)); echo '{}'; exit(0); } - $context = json_decode(file_get_contents($directory.'/context.json'), true, flags: JSON_THROW_ON_ERROR); + if ($action === 'status-independent') { + $budgets = RuntimePayloadCompletionBudget::query()->get(); + echo json_encode(['budgets' => $budgets->count(), 'slots' => $budgets->sum(fn ($budget) => count($budget->slots)), + 'objects' => $budgets->sum(fn ($budget) => count($budget->objects)), + 'rows' => RuntimeExternalPayload::query()->count()], JSON_THROW_ON_ERROR); + exit(0); + } + $contextFile = $action === 'upload-independent' ? '/context-'.$slot.'.json' : '/context.json'; + $context = json_decode(file_get_contents($directory.$contextFile), true, flags: JSON_THROW_ON_ERROR); + if ($action === 'upload-independent') { + $action = 'upload'; + $slot = '0'; + } if ($action === 'upload') { if ($slot !== '0') { if ($kind === 'activity') { From 9a660239086a95f0f21f4ecfc5074dcc1131d5cc Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 8 Sep 2026 18:43:51 +0000 Subject: [PATCH 3/3] release: prepare Server 2.3.6 completion corrections --- composer.json | 2 +- composer.lock | 2 +- docker-compose.dedicated-matching.yml | 4 ++-- docker-compose.memo-rolling.yml | 4 ++-- docker-compose.published.yml | 4 ++-- docker-compose.small-cluster.yml | 2 +- docker-compose.yml | 8 ++++---- k8s/README.md | 10 +++++----- k8s/helm/durable-workflow/Chart.yaml | 6 +++--- k8s/helm/durable-workflow/README.md | 2 +- .../durable-workflow/ci/existing-secrets-values.yaml | 2 +- .../durable-workflow/ci/ingress-and-hpa-values.yaml | 2 +- .../durable-workflow/ci/inline-secrets-values.yaml | 2 +- k8s/helm/durable-workflow/templates/_helpers.tpl | 2 +- k8s/helm/durable-workflow/values.yaml | 2 +- k8s/helm/examples/values-dev.yaml | 2 +- .../examples/values-external-secrets-operator.yaml | 2 +- .../examples/values-production-existing-secrets.yaml | 2 +- k8s/migration-job.yaml | 2 +- k8s/scheduler-cronjob.yaml | 2 +- k8s/secret.yaml | 2 +- k8s/server-deployment.yaml | 2 +- k8s/worker-deployment.yaml | 2 +- resources/release/source-release.json | 4 ++-- scripts/k8s-kind-smoke.sh | 2 +- 25 files changed, 38 insertions(+), 38 deletions(-) diff --git a/composer.json b/composer.json index 5340112d..ad7049a3 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ }, "extra": { "durable-workflow": { - "product-train": "2.3.5" + "product-train": "2.3.6" }, "laravel": { "dont-discover": [] diff --git a/composer.lock b/composer.lock index 7310934a..cf8b77bc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "113d9348b3c41acbd32018eb77476363", + "content-hash": "438d83a0e88390f2355367ba08459596", "packages": [ { "name": "apache/avro", diff --git a/docker-compose.dedicated-matching.yml b/docker-compose.dedicated-matching.yml index 9d34c18c..2527665a 100644 --- a/docker-compose.dedicated-matching.yml +++ b/docker-compose.dedicated-matching.yml @@ -32,13 +32,13 @@ name: durable-workflow-server # daemon reports `shape: dedicated`. # Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.3.5}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.3.6}} x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: ${APP_ENV:-local} DW_SERVER_KEY: ${DW_SERVER_KEY:-} - APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.3.5}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.3.6}} APP_DEBUG: ${APP_DEBUG:-false} DB_CONNECTION: mysql DB_HOST: mysql diff --git a/docker-compose.memo-rolling.yml b/docker-compose.memo-rolling.yml index 158058a0..8d4ad80e 100644 --- a/docker-compose.memo-rolling.yml +++ b/docker-compose.memo-rolling.yml @@ -49,14 +49,14 @@ services: command: ["server-bootstrap"] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.3.5} + APP_VERSION: ${APP_VERSION:-2.3.6} successor: image: ${DW_MEMO_SUCCESSOR_IMAGE:-durable-workflow/server-memo-rolling:local} ports: !override [] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.3.5} + APP_VERSION: ${APP_VERSION:-2.3.6} DW_SERVER_ID: memo-successor DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: server_http_node diff --git a/docker-compose.published.yml b/docker-compose.published.yml index 2203209e..5f313767 100644 --- a/docker-compose.published.yml +++ b/docker-compose.published.yml @@ -1,13 +1,13 @@ name: durable-workflow-server # Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.3.5}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.3.6}} x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: ${APP_ENV:-local} DW_SERVER_KEY: ${DW_SERVER_KEY:-} - APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.3.5}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.3.6}} APP_DEBUG: ${APP_DEBUG:-false} LOG_CHANNEL: ${LOG_CHANNEL:-stderr} LOG_LEVEL: ${LOG_LEVEL:-info} diff --git a/docker-compose.small-cluster.yml b/docker-compose.small-cluster.yml index 27fb87e9..807bbb76 100644 --- a/docker-compose.small-cluster.yml +++ b/docker-compose.small-cluster.yml @@ -12,7 +12,7 @@ x-server-build: &server-build x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: testing - APP_VERSION: ${APP_VERSION:-2.3.5} + APP_VERSION: ${APP_VERSION:-2.3.6} APP_DEBUG: "false" DW_SERVER_KEY: ${DW_SERVER_KEY:-base64:5Zt4nUhlCm3DD0nLXZJQdHiwPfb56yGo9gNV/g3jYbY=} DB_CONNECTION: ${DW_SMALL_CLUSTER_DB:-mysql} diff --git a/docker-compose.yml b/docker-compose.yml index d22bea7b..07773cc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: server_http_node - APP_VERSION: "${APP_VERSION:-2.3.5}" + APP_VERSION: "${APP_VERSION:-2.3.6}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -62,7 +62,7 @@ services: APP_NAME: "Durable Workflow Server" APP_ENV: local DW_SERVER_KEY: "${DW_SERVER_KEY:-}" - APP_VERSION: "${APP_VERSION:-2.3.5}" + APP_VERSION: "${APP_VERSION:-2.3.6}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -123,7 +123,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: worker_node - APP_VERSION: "${APP_VERSION:-2.3.5}" + APP_VERSION: "${APP_VERSION:-2.3.6}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 @@ -176,7 +176,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: scheduler_node - APP_VERSION: "${APP_VERSION:-2.3.5}" + APP_VERSION: "${APP_VERSION:-2.3.6}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 diff --git a/k8s/README.md b/k8s/README.md index 3bf96497..5b51e472 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -13,7 +13,7 @@ The checked-in manifests are synchronized with the repository's stable source release and pin its Docker Hub tag: ```text -durableworkflow/server:2.3.5 +durableworkflow/server:2.3.6 ``` Before production use, patch every workload image to the exact published tag or @@ -21,15 +21,15 @@ digest you intend to run: ```bash kubectl set image -n durable-workflow deploy/durable-workflow-server \ - server=durableworkflow/server:2.3.5 + server=durableworkflow/server:2.3.6 kubectl set image -n durable-workflow deploy/durable-workflow-worker \ - worker=durableworkflow/server:2.3.5 + worker=durableworkflow/server:2.3.6 kubectl set image -n durable-workflow cronjob/durable-workflow-scheduler \ - scheduler=durableworkflow/server:2.3.5 + scheduler=durableworkflow/server:2.3.6 ``` GitHub Container Registry publishes the same release line at -`ghcr.io/durable-workflow/server:2.3.5`. Digest pinning is preferred for strict +`ghcr.io/durable-workflow/server:2.3.6`. Digest pinning is preferred for strict change control. The manifests expect you to provide: diff --git a/k8s/helm/durable-workflow/Chart.yaml b/k8s/helm/durable-workflow/Chart.yaml index 4fdf850a..0486fc89 100644 --- a/k8s/helm/durable-workflow/Chart.yaml +++ b/k8s/helm/durable-workflow/Chart.yaml @@ -5,11 +5,11 @@ type: application # The chart's own semver version. Bumped on every chart release; treated as # independent of the server image version (appVersion). Breaking-change rules # for this version live in docs/helm-upgrading.md alongside the chart. -version: 0.1.84 +version: 0.1.85 # The immutable Durable Workflow Server identity this chart release packages. # The onboarding default in values.yaml and appVersion are generated from the # checked-in source release record. -appVersion: "2.3.5" +appVersion: "2.3.6" kubeVersion: ">=1.27.0-0" home: https://durable-workflow.github.io/docs/2.0/deployment sources: @@ -30,7 +30,7 @@ annotations: # exact commit that most recently changed the packaged chart. org.opencontainers.image.source: https://github.com/durable-workflow/server dev.durable-workflow.source-revision: "unreleased" - dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.3.5" + dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.3.6" artifacthub.io/license: MIT artifacthub.io/category: integration-delivery # Free-form changelog for the current chart release shown by Artifact Hub. diff --git a/k8s/helm/durable-workflow/README.md b/k8s/helm/durable-workflow/README.md index e39ca26c..e696809c 100644 --- a/k8s/helm/durable-workflow/README.md +++ b/k8s/helm/durable-workflow/README.md @@ -63,7 +63,7 @@ helm install durable-workflow ./k8s/helm/durable-workflow \ ```yaml image: - tag: "2.3.5" + tag: "2.3.6" # Pin a digest in production: # digest: "sha256:abc123..." # memoPayloadStorage: "raw-json-v1" # Required for a digest or custom image. diff --git a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml index 3559eb19..a12964ba 100644 --- a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml @@ -1,7 +1,7 @@ # CI fixture: GitOps / externally-managed-secret path. The chart consumes # existing Secrets and renders no Secret resources of its own. image: - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: pgsql diff --git a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml index 199d9de3..726aae85 100644 --- a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml +++ b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml @@ -1,6 +1,6 @@ # CI fixture: ingress + autoscaling enabled. Exercises optional templates. image: - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml index 57fad99a..bf4ba925 100644 --- a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml @@ -2,7 +2,7 @@ # chart's render path is exercised end-to-end. Real deployments should use # existingSecret instead. image: - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/templates/_helpers.tpl b/k8s/helm/durable-workflow/templates/_helpers.tpl index f75df08b..b6a78643 100644 --- a/k8s/helm/durable-workflow/templates/_helpers.tpl +++ b/k8s/helm/durable-workflow/templates/_helpers.tpl @@ -88,7 +88,7 @@ resolved by an explicit capability declaration or an existing workload marker. {{- define "durable-workflow.memoPayloadStorageForImage" -}} {{- $image := toString . -}} {{- $normalized := regexReplaceAll "^index\\.docker\\.io/" $image "docker.io/" -}} -{{- if eq $normalized "docker.io/durableworkflow/server:2.3.5" -}} +{{- if eq $normalized "docker.io/durableworkflow/server:2.3.6" -}} dual-v1 {{- else if regexMatch "^docker\\.io/durableworkflow/server:2\\.0\\.0-rc\\.[0-9]+$" $normalized -}} {{- $releaseCandidate := atoi (regexFind "[0-9]+$" $normalized) -}} diff --git a/k8s/helm/durable-workflow/values.yaml b/k8s/helm/durable-workflow/values.yaml index acc8d6e9..d986cf56 100644 --- a/k8s/helm/durable-workflow/values.yaml +++ b/k8s/helm/durable-workflow/values.yaml @@ -21,7 +21,7 @@ image: registry: docker.io repository: durableworkflow/server # Generated by scripts/ci/sync-source-release.mjs. Do not edit this default. - tag: "2.3.5" + tag: "2.3.6" # Optional digest pin. When set, takes precedence over tag for change control. # Example: "sha256:abc123..." digest: "" diff --git a/k8s/helm/examples/values-dev.yaml b/k8s/helm/examples/values-dev.yaml index f0fcc481..4026ec43 100644 --- a/k8s/helm/examples/values-dev.yaml +++ b/k8s/helm/examples/values-dev.yaml @@ -3,7 +3,7 @@ # shape in production. image: - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: mysql diff --git a/k8s/helm/examples/values-external-secrets-operator.yaml b/k8s/helm/examples/values-external-secrets-operator.yaml index f0558429..c9aa208a 100644 --- a/k8s/helm/examples/values-external-secrets-operator.yaml +++ b/k8s/helm/examples/values-external-secrets-operator.yaml @@ -5,7 +5,7 @@ # concern. image: - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: pgsql diff --git a/k8s/helm/examples/values-production-existing-secrets.yaml b/k8s/helm/examples/values-production-existing-secrets.yaml index 731483cf..42a00025 100644 --- a/k8s/helm/examples/values-production-existing-secrets.yaml +++ b/k8s/helm/examples/values-production-existing-secrets.yaml @@ -10,7 +10,7 @@ image: repository: durable-workflow/server # Pin a digest in production for change-control auditability. digest: "" # e.g. "sha256:abc123..." - tag: "2.3.5" + tag: "2.3.6" externalDatabase: connection: pgsql diff --git a/k8s/migration-job.yaml b/k8s/migration-job.yaml index 82ba17c1..4d881a24 100644 --- a/k8s/migration-job.yaml +++ b/k8s/migration-job.yaml @@ -13,7 +13,7 @@ spec: restartPolicy: OnFailure containers: - name: migrate - image: durableworkflow/server:2.3.5 + image: durableworkflow/server:2.3.6 command: ["server-entrypoint"] args: ["server-bootstrap"] envFrom: diff --git a/k8s/scheduler-cronjob.yaml b/k8s/scheduler-cronjob.yaml index 2cf08f79..beabdbbb 100644 --- a/k8s/scheduler-cronjob.yaml +++ b/k8s/scheduler-cronjob.yaml @@ -24,7 +24,7 @@ spec: restartPolicy: Never containers: - name: scheduler - image: durableworkflow/server:2.3.5 + image: durableworkflow/server:2.3.6 command: ["server-entrypoint"] args: ["sh", "-c", "php artisan schedule:evaluate --limit=100 --json; php artisan activity:timeout-enforce --limit=100; if php artisan list --raw | grep -q '^external-payloads:cleanup '; then php artisan external-payloads:cleanup --limit=100 --json; fi; php artisan history:prune --limit=100"] envFrom: diff --git a/k8s/secret.yaml b/k8s/secret.yaml index 43f6631b..ae72e9d2 100644 --- a/k8s/secret.yaml +++ b/k8s/secret.yaml @@ -12,7 +12,7 @@ metadata: app.kubernetes.io/name: durable-workflow data: APP_NAME: "Durable Workflow Server" - APP_VERSION: "2.3.5" + APP_VERSION: "2.3.6" APP_ENV: production APP_DEBUG: "false" DB_CONNECTION: mysql diff --git a/k8s/server-deployment.yaml b/k8s/server-deployment.yaml index d665b18b..e71fad5d 100644 --- a/k8s/server-deployment.yaml +++ b/k8s/server-deployment.yaml @@ -23,7 +23,7 @@ spec: spec: containers: - name: server - image: durableworkflow/server:2.3.5 + image: durableworkflow/server:2.3.6 ports: - containerPort: 8080 name: http diff --git a/k8s/worker-deployment.yaml b/k8s/worker-deployment.yaml index e518f591..49132bb2 100644 --- a/k8s/worker-deployment.yaml +++ b/k8s/worker-deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: worker - image: durableworkflow/server:2.3.5 + image: durableworkflow/server:2.3.6 command: ["server-entrypoint"] args: ["php", "artisan", "queue:work", "--sleep=1", "--tries=3", "--max-time=3600"] envFrom: diff --git a/resources/release/source-release.json b/resources/release/source-release.json index f4765b34..6aa8f6ce 100644 --- a/resources/release/source-release.json +++ b/resources/release/source-release.json @@ -1,9 +1,9 @@ { "schema": "durable-workflow.server.source-release/v1", "server": { - "version": "2.3.5" + "version": "2.3.6" }, "helm_chart": { - "version": "0.1.84" + "version": "0.1.85" } } diff --git a/scripts/k8s-kind-smoke.sh b/scripts/k8s-kind-smoke.sh index ffb6698d..9eb8fba9 100755 --- a/scripts/k8s-kind-smoke.sh +++ b/scripts/k8s-kind-smoke.sh @@ -7,7 +7,7 @@ cluster="${K8S_SMOKE_CLUSTER:-durable-workflow-server-smoke}" image="${K8S_SMOKE_IMAGE:-durableworkflow/server:k8s-smoke}" # Generated by scripts/ci/sync-source-release.mjs so the smoke replaces the # same default shipped by the public manifests. -manifest_image="durableworkflow/server:2.3.5" +manifest_image="durableworkflow/server:2.3.6" kind_node_image="${K8S_SMOKE_KIND_NODE_IMAGE:-kindest/node:v1.29.4}" artifact_dir="${K8S_SMOKE_ARTIFACT_DIR:-/tmp/durable-workflow-k8s-kind-smoke-artifacts}" rendered_dir="${artifact_dir}/rendered-manifests"