Skip to content
Merged
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
77 changes: 77 additions & 0 deletions docs/HOTFIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Hotfix Guide

This guide describes how to apply a hotfix to a previously released version without including unreleased changes from `main`.

## When to Use This

Use this process when a critical bug is found in a published release and `main` already contains unreleased changes that must not be included in the fix.

## Steps

### 1. Create a branch from the release tag's commit

Find the commit hash the release tag points to:

```bash
git log --oneline --decorate | grep "tag: v"
```

Or look it up directly:

```bash
git rev-list -n 1 vX.Y.Z
```

Create a branch from that commit:

```bash
git checkout -b hotfix/vX.Y.Z+1 <commit-hash>
```

For example, if `v0.36.0` is at `eb6648d`:

```bash
git checkout -b hotfix/v0.36.1 eb6648d
```

### 2. Apply the fix

Make the necessary changes, then bump the patch version in `pyproject.toml`:

```toml
[project]
version = "X.Y.Z+1"
```

### 3. Commit and push

```bash
git add pyproject.toml
git commit -m "fix: <description of the fix>"
git push -u origin hotfix/vX.Y.Z+1
```

### 4. Open a PR against `main` (for tracking)

Even though the hotfix branch is not based on `main`, open a PR so the fix is reviewed and later cherry-picked into `main`.

Cherry-pick the fix commit into `main` after the hotfix is released:

```bash
git checkout main
git cherry-pick <fix-commit-hash>
```

### 5. Create the GitHub Release from the hotfix branch

- Go to the repository's **Releases** page
- Click **"Draft a new release"**
- Create a new tag `vX.Y.Z+1` pointing to the **tip of the hotfix branch** (not `main`)
- Fill in the release title and notes
- Click **"Publish release"**

> **Important:** When creating the tag, make sure it points to the hotfix branch commit, not to `main`. The release workflow checks out the tagged commit, so `pyproject.toml` and all code will be taken from that commit.

### 6. Verify the release

The [Publish Package to PyPI](../.github/workflows/release.yml) workflow will trigger automatically. Monitor the **Actions** tab to confirm the correct version was published.
64 changes: 32 additions & 32 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.36.0"
version = "0.36.1"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand All @@ -11,29 +11,29 @@ requires-python = ">=3.11"
dependencies = [
"minio~=7.2.16",
"setuptools~=80.9.0",
"requests>=2.33.0",
"requests>=2.33.0,<3",
"requests-oauthlib~=2.0.0",
"pydantic~=2.12.3",
"hatchling~=1.27.0",
"opentelemetry-exporter-otlp-proto-grpc~=1.42.1",
"opentelemetry-exporter-otlp-proto-http~=1.42.1",
"traceloop-sdk~=0.61.0",
"opentelemetry-instrumentation-langchain>=0.61.0",
"httpx>=0.27.0",
"PyJWT>=2.13.0",
"protobuf>=4.25.0",
"protovalidate>=0.13.0",
"grpcio>=1.60.0",
"opentelemetry-api>=1.42.1",
"opentelemetry-sdk>=1.42.1",
"mcp>=1.1.0",
"opentelemetry-instrumentation-langchain>=0.61.0,<1",
"httpx>=0.27.0,<1",
"PyJWT>=2.13.0,<3",
"protobuf>=5.0.0,<8",
"protovalidate>=0.13.0,<1",
"grpcio>=1.60.0,<2",
"opentelemetry-api>=1.42.1,<2",
"opentelemetry-sdk>=1.42.1,<2",
"mcp>=1.1.0,<2",
]

[project.optional-dependencies]
extensibility = ["a2a-sdk>=0.2.0"]
starlette = ["starlette>=0.40.0"]
langchain = ["langchain-core>=1.2.7"]
langgraph = ["langgraph>=1.0.0"]
extensibility = ["a2a-sdk>=0.2.0,<1"]
starlette = ["starlette>=0.40.0,<1"]
langchain = ["langchain-core>=1.2.7,<2"]
langgraph = ["langgraph>=1.0.0,<2"]

[build-system]
requires = ["hatchling"]
Expand All @@ -44,23 +44,23 @@ packages = ["src/sap_cloud_sdk", "src/buf"]

[dependency-groups]
dev = [
"pytest>=8.4.2",
"pytest-cov>=7.0.0",
"pytest-asyncio>=1.0.0",
"pytest-bdd>=7.2.0",
"python-dotenv>=1.0.0",
"ty>=0.0.21",
"cryptography>=46.0.3",
"ruff>=0.8.0",
"starlette>=0.40.0",
"anyio>=3.6.2",
"httpx>=0.27.0",
"a2a-sdk>=0.2.0",
"langchain-core>=1.2.7",
"langgraph>=0.2.0",
"langchain-community>=0.3.0",
"litellm>=1.40.0",
"langchain-litellm>=0.6.6",
"pytest>=8.4.2,<9",
"pytest-cov>=7.0.0,<8",
"pytest-asyncio>=1.0.0,<2",
"pytest-bdd>=8.1.0,<9",
"python-dotenv>=1.0.0,<2",
"ty>=0.0.21,<1",
"cryptography>=46.0.3,<47",
"ruff>=0.8.0,<1",
"starlette>=0.40.0,<1",
"anyio>=4.5,<5",
"httpx>=0.27.0,<1",
"a2a-sdk>=0.2.0,<1",
"langchain-core>=1.2.7,<2",
"langgraph>=1.0.0,<2",
"langchain-community>=0.3.0,<1",
"litellm>=1.40.0,<2",
"langchain-litellm>=0.6.6,<1",
]

[tool.pytest.ini_options]
Expand Down
6 changes: 4 additions & 2 deletions tests/extensibility/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ def _success_payload(workflow_id: str = "wf-001") -> str:
"json": {
"message_id": "msg-1",
"context_id": "ctx-1",
"role": 2,
"role": "agent",
"parts": [{"kind": "text", "text": ""}],
}
}
]
Expand Down Expand Up @@ -311,7 +312,8 @@ def _poll_success_payload() -> str:
"json": {
"message_id": "msg-2",
"context_id": "ctx-1",
"role": 2,
"role": "agent",
"parts": [{"kind": "text", "text": ""}],
}
}
]
Expand Down
8 changes: 4 additions & 4 deletions tests/objectstore/unit/test_s3_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def test_get_object_success(self, mock_minio_class):
@patch('sap_cloud_sdk.objectstore._s3.Minio')
def test_get_object_not_found(self, mock_minio_class):
mock_minio = Mock()
s3_error = S3Error("NoSuchKey", "Key not found", "test.txt", "123", "456", Mock())
s3_error = S3Error(Mock(), "NoSuchKey", "Key not found", "test.txt", "123", "456")
mock_minio.get_object.side_effect = s3_error
mock_minio_class.return_value = mock_minio

Expand All @@ -200,7 +200,7 @@ def test_delete_object_success(self, mock_minio_class):
@patch('sap_cloud_sdk.objectstore._s3.Minio')
def test_delete_object_not_found_ignored(self, mock_minio_class):
mock_minio = Mock()
s3_error = S3Error("NoSuchKey", "Key not found", "test.txt", "123", "456", Mock())
s3_error = S3Error(Mock(), "NoSuchKey", "Key not found", "test.txt", "123", "456")
mock_minio.remove_object.side_effect = s3_error
mock_minio_class.return_value = mock_minio

Expand Down Expand Up @@ -273,7 +273,7 @@ def test_head_object_success(self, mock_minio_class):
@patch('sap_cloud_sdk.objectstore._s3.Minio')
def test_head_object_not_found(self, mock_minio_class):
mock_minio = Mock()
s3_error = S3Error("NoSuchKey", "Key not found", "test.txt", "123", "456", Mock())
s3_error = S3Error(Mock(), "NoSuchKey", "Key not found", "test.txt", "123", "456")
mock_minio.stat_object.side_effect = s3_error
mock_minio_class.return_value = mock_minio

Expand All @@ -296,7 +296,7 @@ def test_object_exists_true(self, mock_minio_class):
@patch('sap_cloud_sdk.objectstore._s3.Minio')
def test_object_exists_false(self, mock_minio_class):
mock_minio = Mock()
s3_error = S3Error("NoSuchKey", "Key not found", "test.txt", "123", "456", Mock())
s3_error = S3Error(Mock(), "NoSuchKey", "Key not found", "test.txt", "123", "456")
mock_minio.stat_object.side_effect = s3_error
mock_minio_class.return_value = mock_minio

Expand Down
Loading
Loading