Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/durabletask-azurefunctions-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions) Dev Release

on:
workflow_run:
workflows: ["Durable Task Scheduler SDK (durabletask-azurefunctions)"]
types:
- completed
branches:
- main

jobs:
publish-dev:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/azurefunctions-v}" >> $GITHUB_ENV # Extract version from the tag

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14" # Adjust Python version as needed

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine

- name: Append dev to version in pyproject.toml
working-directory: durabletask-azurefunctions
run: |
sed -i 's/^version = "\(.*\)"/version = "\1.dev${{ github.run_number }}"/' pyproject.toml

- name: Build package from directory durabletask-azurefunctions
working-directory: durabletask-azurefunctions
run: |
python -m build

- name: Check package
working-directory: durabletask-azurefunctions
run: |
twine check dist/*

- name: Publish package to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_AZUREFUNCTIONS }} # Store your PyPI API token in GitHub Secrets
working-directory: durabletask-azurefunctions
run: |
twine upload dist/*
Comment on lines +13 to +52

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 11 minutes ago

The recommended fix is to explicitly add a permissions: block to the workflow. This block should be placed at the top level of the workflow (directly after the name: field), thereby applying to all jobs unless jobs override it. The safest minimum is permissions: contents: read, which will suffice unless more elevated privileges are required (not evidenced in any of the given steps). No additional imports, methods, or new dependencies are needed; this is a YAML configuration change. Edit .github/workflows/durabletask-azurefunctions-dev.yml to add:

permissions:
  contents: read

immediately after the name: field at line 1, before the on: trigger.


Suggested changeset 1
.github/workflows/durabletask-azurefunctions-dev.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/durabletask-azurefunctions-dev.yml b/.github/workflows/durabletask-azurefunctions-dev.yml
--- a/.github/workflows/durabletask-azurefunctions-dev.yml
+++ b/.github/workflows/durabletask-azurefunctions-dev.yml
@@ -1,4 +1,6 @@
 name: Durable Task Scheduler SDK (durabletask-azurefunctions) Dev Release
+permissions:
+  contents: read
 
 on:
   workflow_run:
EOF
@@ -1,4 +1,6 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions) Dev Release
permissions:
contents: read

on:
workflow_run:
Copilot is powered by AI and may make mistakes. Always verify output.
50 changes: 50 additions & 0 deletions .github/workflows/durabletask-azurefunctions-experimental.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions) Experimental Release

on:
push:
branches-ignore:
- main
- release/*

jobs:
publish-experimental:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/azurefunctions-v}" >> $GITHUB_ENV # Extract version from the tag

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14" # Adjust Python version as needed

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine

- name: Change the version in pyproject.toml to 0.0.0dev{github.run_number}
working-directory: durabletask-azurefunctions
run: |
sed -i 's/^version = ".*"/version = "0.0.0.dev${{ github.run_number }}"/' pyproject.toml

- name: Build package from directory durabletask-azurefunctions
working-directory: durabletask-azurefunctions
run: |
python -m build

- name: Check package
working-directory: durabletask-azurefunctions
run: |
twine check dist/*

- name: Publish package to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_AZUREFUNCTIONS }} # Store your PyPI API token in GitHub Secrets
working-directory: durabletask-azurefunctions
run: |
twine upload dist/*
Comment on lines +11 to +50

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 11 minutes ago

To fix the problem, an explicit permissions block should be added to the workflow file. This can be done either at the root of the workflow (affecting all jobs) or inside the publish-experimental job (to restrict just that job). Since there is only one job, it is best practice to add the permissions block at the root. The minimal permission needed for read-only access is:

permissions:
  contents: read

If the workflow ever needs to write release or package information back to the repository, the permissions should specify only those specific privileges. For now, since all steps run locally and publish externally to PyPI (using Twine and a PyPI token), only contents: read is needed.

Only one edit is needed: insert the permissions block at the top level, just below the workflow name and before the on: block in .github/workflows/durabletask-azurefunctions-experimental.yml.

Suggested changeset 1
.github/workflows/durabletask-azurefunctions-experimental.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/durabletask-azurefunctions-experimental.yml b/.github/workflows/durabletask-azurefunctions-experimental.yml
--- a/.github/workflows/durabletask-azurefunctions-experimental.yml
+++ b/.github/workflows/durabletask-azurefunctions-experimental.yml
@@ -1,7 +1,8 @@
 name: Durable Task Scheduler SDK (durabletask-azurefunctions) Experimental Release
+permissions:
+  contents: read
 
 on:
-  push:
     branches-ignore:
       - main
       - release/*
EOF
@@ -1,7 +1,8 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions) Experimental Release
permissions:
contents: read

on:
push:
branches-ignore:
- main
- release/*
Copilot is powered by AI and may make mistakes. Always verify output.
126 changes: 126 additions & 0 deletions .github/workflows/durabletask-azurefunctions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions)

on:
push:
branches:
- "main"
tags:
- "azurefunctions-v*" # Only run for tags starting with "azurefunctions-v"
pull_request:
branches:
- "main"

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: 3.14
- name: Install dependencies
working-directory: durabletask-azurefunctions
run: |
python -m pip install --upgrade pip
pip install setuptools wheel tox
pip install flake8
- name: Run flake8 Linter
working-directory: durabletask-azurefunctions
run: flake8 .
- name: Run flake8 Linter
working-directory: tests/durabletask-azurefunctions
run: flake8 .

run-docker-tests:
Comment on lines +15 to +35

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
env:
EMULATOR_VERSION: "latest"
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Pull Docker image
run: docker pull mcr.microsoft.com/dts/dts-emulator:$EMULATOR_VERSION

- name: Run Docker container
run: |
docker run --name dtsemulator -d -p 8080:8080 mcr.microsoft.com/dts/dts-emulator:$EMULATOR_VERSION

- name: Wait for container to be ready
run: sleep 10 # Adjust if your service needs more time to start

- name: Set environment variables
run: |
echo "TASKHUB=default" >> $GITHUB_ENV
echo "ENDPOINT=http://localhost:8080" >> $GITHUB_ENV

- name: Install durabletask dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -r requirements.txt

- name: Install durabletask-azurefunctions dependencies
working-directory: examples
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Install durabletask-azurefunctions locally
working-directory: durabletask-azurefunctions
run: |
pip install . --no-deps --force-reinstall

- name: Install durabletask locally
run: |
pip install . --no-deps --force-reinstall

- name: Run the tests
working-directory: tests/durabletask-azurefunctions
run: |
pytest -m "dts" --verbose

publish-release:
if: startsWith(github.ref, 'refs/tags/azurefunctions-v') # Only run if a matching tag is pushed
needs: run-docker-tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/azurefunctions-v}" >> $GITHUB_ENV # Extract version from the tag

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14" # Adjust Python version as needed

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine

- name: Build package from directory durabletask-azurefunctions
working-directory: durabletask-azurefunctions
run: |
python -m build

- name: Check package
working-directory: durabletask-azurefunctions
run: |
twine check dist/*

- name: Publish package to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_AZUREFUNCTIONS }} # Store your PyPI API token in GitHub Secrets
working-directory: durabletask-azurefunctions
run: |
twine upload dist/*
Comment on lines +90 to +126

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 11 minutes ago

To fix this issue, explicitly set a permissions block for the whole workflow or for each job as needed. The ideal minimum is to set contents: read, unless the job requires other types of access. For the publish-release job, releasing to PyPI and not creating tags, releases, or manipulating pull requests, contents: read is sufficient (it only reads package files, not writing to the repo).

We should therefore add the following block near the top of the workflow (to affect all jobs, unless overridden):

permissions:
  contents: read

Alternatively, it could be added inside the publish-release job if only that job requires explicit restriction, but in almost all cases, setting it globally is clearer and safer (unless other jobs in the workflow require additional write privileges).

This change is made at the root of the YAML file, directly after the name: block and before the on: block.

No other code changes or external dependencies are required.

Suggested changeset 1
.github/workflows/durabletask-azurefunctions.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/durabletask-azurefunctions.yml b/.github/workflows/durabletask-azurefunctions.yml
--- a/.github/workflows/durabletask-azurefunctions.yml
+++ b/.github/workflows/durabletask-azurefunctions.yml
@@ -1,4 +1,6 @@
 name: Durable Task Scheduler SDK (durabletask-azurefunctions)
+permissions:
+  contents: read
 
 on:
   push:
EOF
@@ -1,4 +1,6 @@
name: Durable Task Scheduler SDK (durabletask-azurefunctions)
permissions:
contents: read

on:
push:
Copilot is powered by AI and may make mistakes. Always verify output.
10 changes: 10 additions & 0 deletions durabletask-azurefunctions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v0.1.0

- Initial implementation
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from durabletask.azurefunctions.decorators.durable_app import Blueprint, DFApp
from durabletask.azurefunctions.client import DurableFunctionsClient

__all__ = ["Blueprint", "DFApp", "DurableFunctionsClient"]
105 changes: 105 additions & 0 deletions durabletask-azurefunctions/durabletask/azurefunctions/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json

from datetime import timedelta
from typing import Any, Optional
import azure.functions as func
from urllib.parse import urlparse, quote

from durabletask.entities import EntityInstanceId
from durabletask.client import TaskHubGrpcClient
from durabletask.azurefunctions.internal.azurefunctions_grpc_interceptor import AzureFunctionsDefaultClientInterceptorImpl
from durabletask.azurefunctions.http import HttpManagementPayload


# Client class used for Durable Functions
class DurableFunctionsClient(TaskHubGrpcClient):
"""A gRPC client passed to Durable Functions durable client bindings.

Connects to the Durable Functions runtime using gRPC and provides methods
for creating and managing Durable orchestrations, interacting with Durable entities,
and creating HTTP management payloads and check status responses for use with Durable Functions invocations.
"""
taskHubName: str
connectionName: str
creationUrls: dict[str, str]
managementUrls: dict[str, str]
Comment on lines +27 to +28
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Potential compatibility issue with type hint syntax. The use of dict[str, str] (PEP 585 style) requires Python 3.9+. While pyproject.toml specifies requires-python = ">=3.9", consider whether this is the intended minimum version or if Dict[str, str] from typing should be used for broader compatibility.

Copilot uses AI. Check for mistakes.
baseUrl: str
requiredQueryStringParameters: str
rpcBaseUrl: str
httpBaseUrl: str
maxGrpcMessageSizeInBytes: int
grpcHttpClientTimeout: timedelta

def __init__(self, client_as_string: str):
"""Initializes a DurableFunctionsClient instance from a JSON string.

This string will be provided by the Durable Functions host extension upon invocation of the client trigger.

Args:
client_as_string (str): A JSON string containing the Durable Functions client configuration.

Raises:
json.JSONDecodeError: If the provided string is not valid JSON.
"""
client = json.loads(client_as_string)

self.taskHubName = client.get("taskHubName", "")
self.connectionName = client.get("connectionName", "")
self.creationUrls = client.get("creationUrls", {})
self.managementUrls = client.get("managementUrls", {})
self.baseUrl = client.get("baseUrl", "")
self.requiredQueryStringParameters = client.get("requiredQueryStringParameters", "")
self.rpcBaseUrl = client.get("rpcBaseUrl", "")
self.httpBaseUrl = client.get("httpBaseUrl", "")
self.maxGrpcMessageSizeInBytes = client.get("maxGrpcMessageSizeInBytes", 0)
# TODO: convert the string value back to timedelta - annoying regex?
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout", timedelta(seconds=30))
interceptors = [AzureFunctionsDefaultClientInterceptorImpl(self.taskHubName, self.requiredQueryStringParameters)]

# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
super().__init__(
host_address=self.rpcBaseUrl,
secure_channel=False,
metadata=None,
interceptors=interceptors)

def create_check_status_response(self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
"""Creates an HTTP response for checking the status of a Durable Function instance.

Args:
request (func.HttpRequest): The incoming HTTP request.
instance_id (str): The ID of the Durable Function instance.
"""
location_url = self._get_instance_status_url(request, instance_id)
return func.HttpResponse(
body=str(self._get_client_response_links(request, instance_id)),
status_code=501,
headers={
'content-type': 'application/json',
'Location': location_url,
},
)

def create_http_management_payload(self, request: func.HttpRequest, instance_id: str) -> HttpManagementPayload:
"""Creates an HTTP management payload for a Durable Function instance.

Args:
instance_id (str): The ID of the Durable Function instance.
"""
return self._get_client_response_links(request, instance_id)

def _get_client_response_links(self, request: func.HttpRequest, instance_id: str) -> HttpManagementPayload:
instance_status_url = self._get_instance_status_url(request, instance_id)
return HttpManagementPayload(instance_id, instance_status_url, self.requiredQueryStringParameters)

@staticmethod
def _get_instance_status_url(request: func.HttpRequest, instance_id: str) -> str:
request_url = urlparse(request.url)
location_url = f"{request_url.scheme}://{request_url.netloc}"
encoded_instance_id = quote(instance_id)
location_url = location_url + "/runtime/webhooks/durabletask/instances/" + encoded_instance_id
return location_url
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Constants used to determine the local running context."""
ORCHESTRATION_TRIGGER = "orchestrationTrigger"
ACTIVITY_TRIGGER = "activityTrigger"
ENTITY_TRIGGER = "entityTrigger"
DURABLE_CLIENT = "durableClient"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Loading
Loading