From ae971631cde5f18dff4e4d7303e37c49c0ca2cce Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 09:18:21 +0000 Subject: [PATCH 1/4] Pin package build tools and require the policy YAML wheel --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0744f227..44b3e7b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: - name: Bootstrap patched pip trust uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - name: Install policy dependencies - run: python -m pip install PyYAML==6.0.2 + run: python -m pip install --only-binary=:all: PyYAML==6.0.2 - name: Require centrally approved immutable action commits run: >- python .central-action-policy/scripts/qualification_policy.py validate @@ -191,7 +191,7 @@ jobs: uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust with: clients: pip,python - - run: pip install build twine + - run: pip install build==1.6.1 twine==7.0.0 - run: python -m build - name: Compare built release metadata with the exact source commit run: python scripts/check_release_metadata.py --source-ref "$(git rev-parse HEAD)" --dist dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 43c9b715..ae5fc2d9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -113,7 +113,7 @@ jobs: clients: pip,python - name: Install build tools - run: pip install build twine + run: pip install build==1.6.1 twine==7.0.0 - name: Build package run: python -m build From 9f11fdeec474066c3fc49c7b0eb1dfba85fbcb73 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 09:23:13 +0000 Subject: [PATCH 2/4] Quote the binary-only install command as a YAML block --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44b3e7b7..e4e8eb1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,8 @@ jobs: - name: Bootstrap patched pip trust uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - name: Install policy dependencies - run: python -m pip install --only-binary=:all: PyYAML==6.0.2 + run: >- + python -m pip install --only-binary=:all: PyYAML==6.0.2 - name: Require centrally approved immutable action commits run: >- python .central-action-policy/scripts/qualification_policy.py validate From 6d5501e1225b44613dc205a76286aa96da1c37af Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 09:27:57 +0000 Subject: [PATCH 3/4] Use reviewed wheel-only installs for policy and package tools --- .github/workflows/ci.yml | 5 +++-- .github/workflows/publish.yml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e8eb1f..f8195e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - name: Install policy dependencies run: >- - python -m pip install --only-binary=:all: PyYAML==6.0.2 + python -m pip install --only-binary :all: PyYAML==6.0.2 - name: Require centrally approved immutable action commits run: >- python .central-action-policy/scripts/qualification_policy.py validate @@ -192,7 +192,8 @@ jobs: uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust with: clients: pip,python - - run: pip install build==1.6.1 twine==7.0.0 + - run: >- + pip install --only-binary :all: build==1.6.1 twine==7.0.0 - run: python -m build - name: Compare built release metadata with the exact source commit run: python scripts/check_release_metadata.py --source-ref "$(git rev-parse HEAD)" --dist dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ae5fc2d9..93e6e51d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -113,7 +113,8 @@ jobs: clients: pip,python - name: Install build tools - run: pip install build==1.6.1 twine==7.0.0 + run: >- + pip install --only-binary :all: build==1.6.1 twine==7.0.0 - name: Build package run: python -m build From 44a25a6cb4a59daa4f4423ec695970491b3db0e7 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 09:31:30 +0000 Subject: [PATCH 4/4] Prepare 2.17.1 and document retained update outcome reads --- CHANGELOG.md | 2 ++ docs/reference/client.md | 45 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 +++--- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b1875d..82c10ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.17.1] - 2026-11-13 + ### Fixed - Replay supports histories in which a run's own close settled the work it never awaited (server#1193, workflow#1117/#1121, Workflow 2.28.0). When a run completes, fails or continues as new, each still-open activity, such as a `select()` loser, gets an `ActivityCancelled` with `cancellation_reason: run_closed` just before the terminal event. Replay, query and update replay now skip that cancellation and leave the activity open, as the workflow saw it. It never resolves a selection member, never supplies replay time, and is never taken as a list group's first positional failure. Before this fix, a close's cancellation of a lower-index member could replace the failure the workflow handled. Replay also accepts the new `abandoned_activities` key on `WorkflowCompleted`, `WorkflowFailed` and `WorkflowContinuedAsNew`, which lists abandon-policy activities left running at close. diff --git a/docs/reference/client.md b/docs/reference/client.md index 42f9dff8..74408437 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -1,5 +1,50 @@ # Client +## Read an update outcome + +`Client.update_workflow()` returns a Server receipt. Its default wait stage is +`accepted`: the handler may still be running when the call returns. Keep the +receipt's `update_id` to read the result later without submitting another update. + +```python +from durable_workflow import UpdateStillPending + +receipt = await client.update_workflow( + "order-1", "rename", args=["new name"], wait_for="accepted", +) +update_id = receipt["update_id"] + +try: + result = await client.update_outcome("order-1", update_id, wait_timeout_seconds=30) +except UpdateStillPending as pending: + update_id = pending.update_id + print(f"Update {update_id} is still running; read its outcome again later.") +else: + print(result) # A completed handler may return None. +``` + +`update_outcome()` waits for completion by default. A pending update raises +`UpdateStillPending`, carrying the workflow, run, update and request identities. +It never returns pending work as `None`. To inspect the raw receipt without +waiting for completion, call `update_outcome_receipt()`; pass +`wait_for="completed"` to wait there too. Reads follow an update across +continue-as-new and never admit another update. + +| Outcome | Result | +| --- | --- | +| Completed | The decoded handler result, including `None` | +| Still running | `UpdateStillPending`; keep its `update_id` and read again | +| Handler failed or validator rejected | `UpdateFailed` or `UpdateRejected` | +| No retained update with this id | `UpdateNotFound`, including after pruning | +| Response does not identify the requested update or a recognized status | `UnrecognizedUpdateOutcome` | + +Both read methods require Server discovery to advertise +`capabilities.update_outcome_reads`. An unsupported Server raises +`RuntimeCapabilityUnsupported`; it does not fall back to resubmitting the update. +`WorkflowHandle` and `durable_workflow.sync.Client` provide the same methods. +See the [complete update reference](../sdk-reference.md#workflow-signals-queries-and-updates) +for wait stages, validation and response checks. + ## Trust a private certificate authority The `tls_context` option requires Python SDK 2.10.0 or later. diff --git a/pyproject.toml b/pyproject.toml index e20b8536..c71e776d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "durable-workflow" -version = "2.17.0" +version = "2.17.1" description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server" readme = "README.md" requires-python = ">=3.10" @@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main" durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main" [tool.durable-workflow] -product-train = "2.17.0" -registry-version = "2.17.0" +product-train = "2.17.1" +registry-version = "2.17.1" supported-server-versions = "2.8.0" supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" } worker-protocol-version = "1.20"