Skip to content

UN-3217 [FEAT] Show specific pipeline/API names in workflow deletion error message#1784

Merged
hari-kuriakose merged 15 commits intomainfrom
feat/improve-workflow-deletion-error-message
Feb 26, 2026
Merged

UN-3217 [FEAT] Show specific pipeline/API names in workflow deletion error message#1784
hari-kuriakose merged 15 commits intomainfrom
feat/improve-workflow-deletion-error-message

Conversation

@pk-zipstack
Copy link
Copy Markdown
Contributor

What

  • Updated the workflow deletion error message to display the actual names of API deployments and ETL/Task pipelines that reference the workflow, instead of a generic message.

Why

  • The previous error message ("Cannot delete this Workflow, since it is used in one or many of the API/ETL/Task pipelines") did not tell the user which specific pipelines or API deployments were preventing deletion, making it difficult to take corrective action.

How

  • Backend (workflow_helper.py): Modified can_update_workflow to return pipeline_names and api_names lists alongside the existing can_update boolean, by querying Pipeline and APIDeployment models for their display names.
  • Frontend (Workflows.jsx): Added getUsageMessage helper that constructs a contextual message showing the first pipeline/API name and a count of remaining ones. Examples:
    • 1 usage: Cannot delete "A" as it is used in "B".
    • 2+ usages: Cannot delete "A" as it is used in "B" and 2 other API/ETL/Task pipelines.
  • Also fixed a subtle bug where the original backend logic would skip checking API deployments if any pipelines existed, potentially underreporting usage.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. The can_update boolean in the API response is unchanged in behavior. The two new fields (pipeline_names, api_names) are additive. The frontend gracefully defaults to empty arrays if the new fields are absent, so backward compatibility is preserved.

Database Migrations

  • N/A

Env Config

  • N/A

Relevant Docs

  • N/A

Related Issues or PRs

Dependencies Versions

  • N/A

Notes on Testing

  • Create a workflow and associate it with one or more API deployments and/or ETL/Task pipelines.
  • Attempt to delete the workflow and verify the error message shows the specific names.
  • Test with 1 usage, 2 usages, and 3+ usages to verify singular/plural wording.
  • Test deleting a workflow with no usages to verify it still deletes successfully.

Screenshots

image

Checklist

I have read and understood the Contribution Guidelines.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Backend now returns structured workflow usage data (can_update, pipeline_count, api_count, pipelines, api_names) when checking update/delete eligibility; frontend calls checkWorkflowUsage, formats contextual messages with getUsageMessage, and conditions deletion on usage.can_update with updated error handling.

Changes

Cohort / File(s) Summary
Backend Workflow Usage
backend/workflow_manager/workflow_v2/workflow_helper.py
Added USAGE_DISPLAY_LIMIT constant; reworked can_update_workflow to return { can_update, pipeline_count, api_count, pipelines, api_names }; counts pipelines and API deployments, limits displayed items, and fixes error logging to reference workflow_id.
Frontend Workflow Deletion
frontend/src/components/workflows/workflow/Workflows.jsx
Replaced canDeleteProject(id) with checkWorkflowUsage(id) consuming richer payload; added getUsageMessage(workflowName, usage) to format detailed usage warnings; conditioned deletion on usage.can_update and wrapped flows in try/catch to surface contextual messages.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Frontend as "Frontend\n(Workflows.jsx)"
  participant Backend as "Backend\n(workflow_helper.py)"
  participant DB as "Database"

  User->>Frontend: request delete workflow
  Frontend->>Backend: checkWorkflowUsage(workflowId)
  Backend->>DB: query/count pipelines (limit USAGE_DISPLAY_LIMIT)
  Backend->>DB: query/count API deployments (limit USAGE_DISPLAY_LIMIT)
  DB-->>Backend: pipeline_count, api_count, pipelines[], api_names[]
  Backend-->>Frontend: { can_update, pipeline_count, api_count, pipelines, api_names }
  alt can_update == true
    Frontend->>Backend: delete workflow
    Backend->>DB: delete workflow records
    Backend-->>Frontend: success
    Frontend-->>User: success message
  else can_update == false
    Frontend->>Frontend: getUsageMessage(workflowName, usage)
    Frontend-->>User: show contextual usage message
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: improving workflow deletion error messages by displaying specific pipeline and API names instead of generic messages.
Description check ✅ Passed The description is comprehensive and follows the template structure with all major sections completed: What, Why, How, compatibility assessment, testing notes, and required sections like Database Migrations and Env Config marked as N/A.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improve-workflow-deletion-error-message

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Around line 149-159: checkWorkflowUsage currently mixes await with .then() and
swallows API errors by returning a default result; change checkWorkflowUsage to
use await/try and rethrow on failure (or return a rejected promise) so callers
can detect errors, and update deleteProject to wrap the call to
checkWorkflowUsage in a try/catch that on error invokes handleException and sets
setAlertDetails with the returned error (including a contextual message like
"Unable to delete workflow ${project.id}"); reference checkWorkflowUsage for the
API call change and deleteProject for adding the try/catch and
setAlertDetails(handleException(...)) error path.
🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/workflow_helper.py (1)

998-1002: Pre-existing: redundant guard and wrong variable in error log.

Two minor pre-existing issues in this method:

  1. Line 1001: if not workflow or workflow is None: is dead code — Workflow.objects.get() raises DoesNotExist on missing rows and never returns None/falsy. This branch is unreachable.
  2. Line 1022: The error log references id (Python built-in) instead of workflow_id.

Neither is introduced by this PR, but since you're touching this method, it's a good opportunity to clean them up.

Suggested cleanup
     def can_update_workflow(workflow_id: str) -> dict[str, Any]:
         try:
             workflow: Workflow = Workflow.objects.get(pk=workflow_id)
-            if not workflow or workflow is None:
-                raise WorkflowDoesNotExistError()
 
             pipeline_names = list(
         except Workflow.DoesNotExist:
-            logger.error(f"Error getting workflow: {id}")
+            logger.error(f"Error getting workflow: {workflow_id}")
             raise WorkflowDoesNotExistError()

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Around line 158-169: getUsageMessage currently returns an empty string when
can_update is false but backend returns no pipeline_names/api_names, causing a
blank error toast; update the getUsageMessage function to return a sensible
fallback message (e.g., `Cannot delete "<workflowName>" because it is in use.`
or similar) when total === 0 so the UI always displays a readable error; ensure
you update the return for the total === 0 branch in getUsageMessage and keep
existing firstName/remaining logic intact.
🧹 Nitpick comments (1)
frontend/src/components/workflows/workflow/Workflows.jsx (1)

171-204: Mixing await with .then()/.catch() inside deleteProject.

checkWorkflowUsage is properly awaited, but the deleteProject call on Line 175 uses .then()/.catch() without await. This works but is inconsistent. If you ever need to act after deletion completes (e.g., analytics), the missing await will bite. Consider awaiting it for consistency.

Suggested refactor
       if (usage.can_update) {
-        projectApiService
-          .deleteProject(project.id)
-          .then(() => {
-            getProjectList();
-            setAlertDetails({
-              type: "success",
-              content: "Workflow deleted successfully",
-            });
-          })
-          .catch((err) => {
-            setAlertDetails(
-              handleException(err, `Unable to delete workflow ${project.id}`)
-            );
+        try {
+          await projectApiService.deleteProject(project.id);
+          getProjectList();
+          setAlertDetails({
+            type: "success",
+            content: "Workflow deleted successfully",
           });
+        } catch (err) {
+          setAlertDetails(
+            handleException(err, `Unable to delete workflow ${project.id}`)
+          );
+        }
       } else {

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
frontend/src/components/workflows/workflow/Workflows.jsx (2)

168-168: displayLimit = 3 mismatches the backend's USAGE_DISPLAY_LIMIT = 5.

The backend fetches up to 5 names per type, but the frontend only renders 3. Items at positions 4–5 are always fetched and transmitted but silently discarded. Consider aligning both limits to the same value, or driving the frontend constant from a shared API contract, to avoid the wasted payload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/workflows/workflow/Workflows.jsx` at line 168, The
frontend constant displayLimit (declared as displayLimit = 3 in Workflows.jsx)
does not match the backend USAGE_DISPLAY_LIMIT = 5 causing extra items to be
fetched then discarded; update Workflows.jsx to use the same limit as the
backend (set displayLimit to 5) or, better, derive it from the API/response
(e.g., use a returned displayLimit field or the length capped by backend limit)
so the frontend and backend limits match and avoid wasted payloads.

203-217: Mixing .then()/.catch() inside an async function — deleteProject resolves before deletion completes.

Because projectApiService.deleteProject(project.id) is not await-ed, the async function returns immediately after scheduling the call. The .then() / .catch() callbacks may execute after the component unmounts, risking state-update warnings. Refactoring to await also makes the outer try/catch the single error-handling boundary, eliminating the inner .catch().

♻️ Proposed refactor
     if (usage.canUpdate) {
-        projectApiService
-          .deleteProject(project.id)
-          .then(() => {
-            getProjectList();
-            setAlertDetails({
-              type: "success",
-              content: "Workflow deleted successfully",
-            });
-          })
-          .catch((err) => {
-            setAlertDetails(
-              handleException(err, `Unable to delete workflow ${project.id}`)
-            );
-          });
+        await projectApiService.deleteProject(project.id);
+        getProjectList();
+        setAlertDetails({
+          type: "success",
+          content: "Workflow deleted successfully",
+        });
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/workflows/workflow/Workflows.jsx` around lines 203 -
217, The code calls projectApiService.deleteProject(project.id) using
.then()/.catch() inside an async handler which can return before deletion
completes; change this to await projectApiService.deleteProject(project.id) and
let the enclosing try/catch handle errors, then after awaiting call
getProjectList() and setAlertDetails({type: "success", content: "Workflow
deleted successfully"}); remove the inner .then()/.catch() and in the catch use
setAlertDetails(handleException(err, `Unable to delete workflow ${project.id}`))
so you reference the same symbols (usage.canUpdate,
projectApiService.deleteProject, getProjectList, setAlertDetails,
handleException, project.id).
backend/workflow_manager/workflow_v2/workflow_helper.py (3)

997-997: Consider moving USAGE_DISPLAY_LIMIT to the top of the class.

Class-level constants are conventionally placed at the beginning of the class body for discoverability. Its current placement mid-class (between two methods) is valid Python but surprising to readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` at line 997, Move
the class-level constant USAGE_DISPLAY_LIMIT from its current mid-class location
to the top of the class body (immediately after the class declaration and any
class docstring), so it is declared alongside other class-level constants for
discoverability; ensure any references to USAGE_DISPLAY_LIMIT in methods remain
unchanged and adjust surrounding whitespace/comments as needed to keep style
consistent.

1027-1029: Use logger.exception instead of logger.error inside except blocks.

logger.exception automatically captures and appends the current exception traceback, which aids debugging with no extra cost. logger.error here silently discards the DoesNotExist traceback.

♻️ Proposed fix
         except Workflow.DoesNotExist:
-            logger.error(f"Error getting workflow: {workflow_id}")
+            logger.exception(f"Error getting workflow: {workflow_id}")
             raise WorkflowDoesNotExistError()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` around lines 1027 -
1029, Replace the logger.error call inside the Workflow.DoesNotExist except
block with logger.exception so the caught exception traceback is recorded;
locate the except Workflow.DoesNotExist block in workflow_helper.py (where
workflow_id is used) and change logger.error(f"Error getting workflow:
{workflow_id}") to logger.exception(...) while keeping the subsequent raise
WorkflowDoesNotExistError() intact so the original error context is preserved in
logs.

1002-1004: Dead code — Workflow.objects.get() never returns None.

Workflow.objects.get(pk=workflow_id) either returns an instance or raises Workflow.DoesNotExist (caught below at line 1027). The if not workflow or workflow is None branch is unreachable.

🧹 Proposed cleanup
         workflow: Workflow = Workflow.objects.get(pk=workflow_id)
-        if not workflow or workflow is None:
-            raise WorkflowDoesNotExistError()
-

         limit = WorkflowHelper.USAGE_DISPLAY_LIMIT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` around lines 1002 -
1004, The conditional checking `if not workflow or workflow is None` after
calling `Workflow.objects.get(pk=workflow_id)` is dead code because
`Workflow.objects.get` either returns an instance or raises
`Workflow.DoesNotExist`; remove that unreachable branch and rely on the existing
exception handling (the `Workflow.DoesNotExist` catch and/or raising
`WorkflowDoesNotExistError()` where appropriate) so you don't duplicate or hide
error flow for the `Workflow` retrieval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Around line 164-166: The empty-usage edge case is already handled: when
totalCount === 0 the component returns a fallback message string using the
workflowName variable; ensure the return in Workflows.jsx (the block referencing
totalCount and workflowName) remains and is used by the toast/error handler so
the user sees "Cannot delete `workflowName` as it is currently in use." (keep
the interpolation/backticks as shown).

---

Nitpick comments:
In `@backend/workflow_manager/workflow_v2/workflow_helper.py`:
- Line 997: Move the class-level constant USAGE_DISPLAY_LIMIT from its current
mid-class location to the top of the class body (immediately after the class
declaration and any class docstring), so it is declared alongside other
class-level constants for discoverability; ensure any references to
USAGE_DISPLAY_LIMIT in methods remain unchanged and adjust surrounding
whitespace/comments as needed to keep style consistent.
- Around line 1027-1029: Replace the logger.error call inside the
Workflow.DoesNotExist except block with logger.exception so the caught exception
traceback is recorded; locate the except Workflow.DoesNotExist block in
workflow_helper.py (where workflow_id is used) and change logger.error(f"Error
getting workflow: {workflow_id}") to logger.exception(...) while keeping the
subsequent raise WorkflowDoesNotExistError() intact so the original error
context is preserved in logs.
- Around line 1002-1004: The conditional checking `if not workflow or workflow
is None` after calling `Workflow.objects.get(pk=workflow_id)` is dead code
because `Workflow.objects.get` either returns an instance or raises
`Workflow.DoesNotExist`; remove that unreachable branch and rely on the existing
exception handling (the `Workflow.DoesNotExist` catch and/or raising
`WorkflowDoesNotExistError()` where appropriate) so you don't duplicate or hide
error flow for the `Workflow` retrieval.

In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Line 168: The frontend constant displayLimit (declared as displayLimit = 3 in
Workflows.jsx) does not match the backend USAGE_DISPLAY_LIMIT = 5 causing extra
items to be fetched then discarded; update Workflows.jsx to use the same limit
as the backend (set displayLimit to 5) or, better, derive it from the
API/response (e.g., use a returned displayLimit field or the length capped by
backend limit) so the frontend and backend limits match and avoid wasted
payloads.
- Around line 203-217: The code calls
projectApiService.deleteProject(project.id) using .then()/.catch() inside an
async handler which can return before deletion completes; change this to await
projectApiService.deleteProject(project.id) and let the enclosing try/catch
handle errors, then after awaiting call getProjectList() and
setAlertDetails({type: "success", content: "Workflow deleted successfully"});
remove the inner .then()/.catch() and in the catch use
setAlertDetails(handleException(err, `Unable to delete workflow ${project.id}`))
so you reference the same symbols (usage.canUpdate,
projectApiService.deleteProject, getProjectList, setAlertDetails,
handleException, project.id).

Copy link
Copy Markdown
Contributor

@muhammad-ali-e muhammad-ali-e left a comment

Choose a reason for hiding this comment

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

Left a NIT comment. It would be better if it were addressed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
backend/workflow_manager/workflow_v2/workflow_helper.py (3)

997-998: Move USAGE_DISPLAY_LIMIT to the top of the class.

Defining a class-level constant between method definitions is valid Python but makes it hard to discover. Grouping constants at the top of the class is the conventional pattern and improves readability.

♻️ Proposed refactor
 class WorkflowHelper:
+    USAGE_DISPLAY_LIMIT = 5
+
     `@staticmethod`
     def get_workflow_by_id(id: str) -> Workflow:
         ...

And remove it from its current position between make_async_result and can_update_workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` around lines 997 -
998, Move the class-level constant USAGE_DISPLAY_LIMIT from its current position
between the methods make_async_result and can_update_workflow to the top of the
class (with other class-level constants) so it's easy to discover; update the
class definition to declare USAGE_DISPLAY_LIMIT = 5 immediately after the class
declaration and remove the existing declaration between make_async_result and
can_update_workflow.

1030-1036: Move the success return to an else block (Ruff TRY300).

The return statement inside a try block means any exception it raises (however unlikely) would be silently caught by the except Workflow.DoesNotExist below, masking errors unrelated to workflow existence. Moving it to an else block tightens the exception scope.

♻️ Proposed refactor
         try:
             workflow: Workflow = Workflow.objects.get(pk=workflow_id)
             if not workflow or workflow is None:
                 raise WorkflowDoesNotExistError()

             pipeline_count = Pipeline.objects.filter(workflow=workflow).count()
             api_count = APIDeployment.objects.filter(workflow=workflow).count()

             if (pipeline_count + api_count) == 0:
                 return {
                     "can_update": True,
                     "pipelines": [],
                     "api_names": [],
                     "pipeline_count": 0,
                     "api_count": 0,
                 }

             limit = WorkflowHelper.USAGE_DISPLAY_LIMIT
             pipelines = list(
                 Pipeline.objects.filter(workflow=workflow).values(
                     "pipeline_name", "pipeline_type"
                 )[:limit]
             )
             api_names = list(
                 APIDeployment.objects.filter(workflow=workflow).values_list(
                     "display_name", flat=True
                 )[:limit]
             )

-            return {
-                "can_update": False,
-                "pipelines": pipelines,
-                "api_names": api_names,
-                "pipeline_count": pipeline_count,
-                "api_count": api_count,
-            }
         except Workflow.DoesNotExist:
             logger.error(f"Error getting workflow: {workflow_id}")
             raise WorkflowDoesNotExistError()
+        else:
+            return {
+                "can_update": False,
+                "pipelines": pipelines,
+                "api_names": api_names,
+                "pipeline_count": pipeline_count,
+                "api_count": api_count,
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` around lines 1030 -
1036, The early return that constructs the response dict is inside a try block
that catches Workflow.DoesNotExist, which can mask unrelated exceptions; move
that return into an else block paired with the try/except so the try only wraps
the code that may raise Workflow.DoesNotExist (the lookup/operation using
Workflow), then in the else return the dict ({ "can_update": False, "pipelines":
pipelines, "api_names": api_names, "pipeline_count": pipeline_count,
"api_count": api_count }) — reference the try that catches Workflow.DoesNotExist
and the returned variables pipelines, api_names, pipeline_count, api_count to
locate where to relocate the return.

1038-1038: Prefer logger.exception over logger.error in except blocks (Ruff TRY400).

logger.exception automatically attaches the current exception's traceback to the log record, which aids debugging. logger.error without exc_info=True silently drops it.

♻️ Proposed fix
-            logger.error(f"Error getting workflow: {workflow_id}")
+            logger.exception(f"Error getting workflow: {workflow_id}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` at line 1038, In the
except block that currently logs "Error getting workflow: {workflow_id}" using
logger.error, switch to logger.exception (or add exc_info=True) so the traceback
is included; specifically replace the call to logger.error(f"Error getting
workflow: {workflow_id}") with logger.exception(f"Error getting workflow:
{workflow_id}") in the workflow helper where logger and workflow_id are used
(the error handling around getting the workflow).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/workflow_manager/workflow_v2/workflow_helper.py`:
- Around line 997-998: Move the class-level constant USAGE_DISPLAY_LIMIT from
its current position between the methods make_async_result and
can_update_workflow to the top of the class (with other class-level constants)
so it's easy to discover; update the class definition to declare
USAGE_DISPLAY_LIMIT = 5 immediately after the class declaration and remove the
existing declaration between make_async_result and can_update_workflow.
- Around line 1030-1036: The early return that constructs the response dict is
inside a try block that catches Workflow.DoesNotExist, which can mask unrelated
exceptions; move that return into an else block paired with the try/except so
the try only wraps the code that may raise Workflow.DoesNotExist (the
lookup/operation using Workflow), then in the else return the dict ({
"can_update": False, "pipelines": pipelines, "api_names": api_names,
"pipeline_count": pipeline_count, "api_count": api_count }) — reference the try
that catches Workflow.DoesNotExist and the returned variables pipelines,
api_names, pipeline_count, api_count to locate where to relocate the return.
- Line 1038: In the except block that currently logs "Error getting workflow:
{workflow_id}" using logger.error, switch to logger.exception (or add
exc_info=True) so the traceback is included; specifically replace the call to
logger.error(f"Error getting workflow: {workflow_id}") with
logger.exception(f"Error getting workflow: {workflow_id}") in the workflow
helper where logger and workflow_id are used (the error handling around getting
the workflow).

@github-actions
Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@github-actions
Copy link
Copy Markdown
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 63 passed, 0 failed (63 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{63}}$$ $$\textcolor{#23d18b}{\tt{63}}$$

@sonarqubecloud
Copy link
Copy Markdown

@hari-kuriakose hari-kuriakose merged commit 80b62cb into main Feb 26, 2026
8 checks passed
@hari-kuriakose hari-kuriakose deleted the feat/improve-workflow-deletion-error-message branch February 26, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants