From e7faea88e07151f92a55819315c28556a2822258 Mon Sep 17 00:00:00 2001 From: drockparashar Date: Sat, 27 Sep 2025 11:24:10 +0530 Subject: [PATCH 1/5] fix: ensure consistent workflow output path for scheduled runs --- application_sdk/activities/common/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/application_sdk/activities/common/utils.py b/application_sdk/activities/common/utils.py index 163febf6a..56f8aba27 100644 --- a/application_sdk/activities/common/utils.py +++ b/application_sdk/activities/common/utils.py @@ -10,6 +10,7 @@ from datetime import timedelta from functools import wraps from typing import Any, Awaitable, Callable, List, Optional, TypeVar, cast +import re from temporalio import activity @@ -71,9 +72,15 @@ def build_output_path() -> str: >>> build_output_path() "artifacts/apps/appName/workflows/wf-123/run-456" """ + # Sanitize workflow_id to remove any schedule/timestamp suffix + raw_workflow_id = get_workflow_id() + + # If workflow_id contains a timestamp (e.g., '-YYYY-MM-DDTHH:MM:SSZ'), remove it + sanitized_workflow_id = re.sub(r'-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$', '', raw_workflow_id) + return WORKFLOW_OUTPUT_PATH_TEMPLATE.format( application_name=APPLICATION_NAME, - workflow_id=get_workflow_id(), + workflow_id=sanitized_workflow_id, run_id=get_workflow_run_id(), ) From 0e76789c5386a38fe8e5152cb6d2ab28e395895f Mon Sep 17 00:00:00 2001 From: drockparashar Date: Tue, 30 Sep 2025 08:19:24 +0530 Subject: [PATCH 2/5] fix: ensure consistent workflow output path for scheduled runs - Move timestamp regex to module-level constant for maintainability - Use compiled regex pattern for performance - Add fallback to raw workflow_id if sanitization results in empty string --- application_sdk/activities/common/utils.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/application_sdk/activities/common/utils.py b/application_sdk/activities/common/utils.py index 56f8aba27..e8778c843 100644 --- a/application_sdk/activities/common/utils.py +++ b/application_sdk/activities/common/utils.py @@ -7,10 +7,10 @@ import asyncio import glob import os +import re from datetime import timedelta from functools import wraps from typing import Any, Awaitable, Callable, List, Optional, TypeVar, cast -import re from temporalio import activity @@ -23,6 +23,9 @@ logger = get_logger(__name__) +# Compiled regex pattern for removing timestamp suffix from workflow IDs +TIMESTAMP_PATTERN = re.compile(r"-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$") + F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) @@ -64,6 +67,7 @@ def build_output_path() -> str: """Build a standardized output path for workflow artifacts. This method creates a consistent output path format across all workflows using the WORKFLOW_OUTPUT_PATH_TEMPLATE constant. + For scheduled workflows, it removes any timestamp suffix from the workflow_id to ensure consistent output paths. Returns: str: The standardized output path. @@ -74,10 +78,14 @@ def build_output_path() -> str: """ # Sanitize workflow_id to remove any schedule/timestamp suffix raw_workflow_id = get_workflow_id() - - # If workflow_id contains a timestamp (e.g., '-YYYY-MM-DDTHH:MM:SSZ'), remove it - sanitized_workflow_id = re.sub(r'-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$', '', raw_workflow_id) - + + # Remove timestamp suffix (e.g., '-YYYY-MM-DDTHH:MM:SSZ') if present + sanitized_workflow_id = TIMESTAMP_PATTERN.sub("", raw_workflow_id) + + # Fallback to raw workflow_id if sanitization results in empty string + if not sanitized_workflow_id: + sanitized_workflow_id = raw_workflow_id + return WORKFLOW_OUTPUT_PATH_TEMPLATE.format( application_name=APPLICATION_NAME, workflow_id=sanitized_workflow_id, From c10ff4183bc2a7ef2d04c9ebe2da0c4ed5021edb Mon Sep 17 00:00:00 2001 From: drockparashar Date: Tue, 30 Sep 2025 15:46:01 +0530 Subject: [PATCH 3/5] test: add unit tests for build_output_path including empty workflow ID case --- tests/unit/activities/common/test_utils.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/activities/common/test_utils.py b/tests/unit/activities/common/test_utils.py index 9db41d555..b6710e715 100644 --- a/tests/unit/activities/common/test_utils.py +++ b/tests/unit/activities/common/test_utils.py @@ -8,6 +8,7 @@ from application_sdk.activities.common.utils import ( auto_heartbeater, + build_output_path, get_object_store_prefix, get_workflow_id, send_periodic_heartbeat, @@ -36,6 +37,37 @@ def test_get_workflow_id_activity_error(self, mock_activity): get_workflow_id() +class TestBuildOutputPath: + """Test cases for build_output_path function.""" + + @patch("application_sdk.constants.APPLICATION_NAME", "test-app") + @patch("application_sdk.activities.common.utils.activity") + def test_build_output_path_standard(self, mock_activity): + """Standard case: typical workflow and run IDs.""" + mock_activity.info.return_value.workflow_id = "wf-123" + mock_activity.info.return_value.workflow_run_id = "run-456" + result = build_output_path() + assert result == "artifacts/apps/test-app/workflows/wf-123/run-456" + + @patch("application_sdk.constants.APPLICATION_NAME", "test-app") + @patch("application_sdk.activities.common.utils.activity") + def test_build_output_path_scheduled(self, mock_activity): + """Scheduled run: workflow ID with timestamp suffix.""" + mock_activity.info.return_value.workflow_id = "wf-123-2025-09-30T12:34:56Z" + mock_activity.info.return_value.workflow_run_id = "run-456" + result = build_output_path() + assert result == "artifacts/apps/test-app/workflows/wf-123/run-456" + + @patch("application_sdk.constants.APPLICATION_NAME", "test-app") + @patch("application_sdk.activities.common.utils.activity") + def test_build_output_path_empty_workflow_id(self, mock_activity): + """Defensive: workflow ID is empty string.""" + mock_activity.info.return_value.workflow_id = "" + mock_activity.info.return_value.workflow_run_id = "run-000" + result = build_output_path() + assert result == "artifacts/apps/test-app/workflows//run-000" + + class TestGetObjectStorePrefix: """Test cases for get_object_store_prefix function - Real World Scenarios.""" From 6f39e65e741c00de1a5d7d916c1336fc67e11660 Mon Sep 17 00:00:00 2001 From: drockparashar Date: Mon, 6 Oct 2025 11:13:30 +0530 Subject: [PATCH 4/5] fix: use safe placeholder for empty workflow_id in build_output_path --- application_sdk/activities/common/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application_sdk/activities/common/utils.py b/application_sdk/activities/common/utils.py index e8778c843..3a6167ba0 100644 --- a/application_sdk/activities/common/utils.py +++ b/application_sdk/activities/common/utils.py @@ -84,7 +84,7 @@ def build_output_path() -> str: # Fallback to raw workflow_id if sanitization results in empty string if not sanitized_workflow_id: - sanitized_workflow_id = raw_workflow_id + sanitized_workflow_id = "unknown-workflow" return WORKFLOW_OUTPUT_PATH_TEMPLATE.format( application_name=APPLICATION_NAME, From b257dcc0dec7d8d2ee325e119c5ce1bc0a5ebf04 Mon Sep 17 00:00:00 2001 From: drockparashar Date: Mon, 6 Oct 2025 11:21:22 +0530 Subject: [PATCH 5/5] test: update build_output_path test to expect 'unknown-workflow' for empty workflow_id --- tests/unit/activities/common/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/activities/common/test_utils.py b/tests/unit/activities/common/test_utils.py index b6710e715..0bcb39ee1 100644 --- a/tests/unit/activities/common/test_utils.py +++ b/tests/unit/activities/common/test_utils.py @@ -65,7 +65,7 @@ def test_build_output_path_empty_workflow_id(self, mock_activity): mock_activity.info.return_value.workflow_id = "" mock_activity.info.return_value.workflow_run_id = "run-000" result = build_output_path() - assert result == "artifacts/apps/test-app/workflows//run-000" + assert result == "artifacts/apps/test-app/workflows/unknown-workflow/run-000" class TestGetObjectStorePrefix: