Skip to content

ENG-3146: Support Jira DSR lifecycle with pending_external status#7772

Merged
JadeCara merged 5 commits intomainfrom
ENG-3146-jira-dsr-lifecycle
Mar 26, 2026
Merged

ENG-3146: Support Jira DSR lifecycle with pending_external status#7772
JadeCara merged 5 commits intomainfrom
ENG-3146-jira-dsr-lifecycle

Conversation

@JadeCara
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara commented Mar 26, 2026

Ticket ENG-3146

Description Of Changes

Add infrastructure in fides to support the Jira DSR integration lifecycle:

  1. Auto-create ManualTaskConfig — When a jira_ticket connection is created, automatically generate access and erasure configs with a "complete" checkbox field. Without these, the DSR graph node completes immediately because it finds no configs to wait on.

  2. Use pending_external status — Jira manual tasks now set pending_external instead of requires_input, distinguishing "waiting on Jira" from "operator needs to fill in fields in Fides." The Admin UI already has labels and badges for this status.

  3. Treat pending_external like requires_input — The watchdog, duplication detection, and interrupted task requeue all now include pending_external alongside requires_input to prevent false-positive error transitions.

Code Changes

  • Auto-create default access + erasure ManualTaskConfig with a "complete" checkbox field when a jira_ticket ManualTask is created in ConnectionService._create_default_jira_configs
  • Set pending_external instead of requires_input for jira_ticket task types in ManualTaskGraphTask._set_submitted_data_or_raise_awaiting_async_task_callback
  • Include pending_external in the watchdog's in-progress query and the no-auto-error guard in request_service.py
  • Include pending_external in ACTIONED_REQUEST_STATUSES for duplication detection

Steps to Confirm

  1. Create a jira_ticket connection config via the Admin UI or API
  2. Verify a ManualTask is created with two ManualTaskConfig records (access + erasure), each with a "complete" checkbox field
  3. Approve an access DSR — it should pause at pending_external (not requires_input)
  4. Approve an erasure DSR — it should also pause at pending_external
  5. The pending_external badge should appear in the Admin UI request details

Note: Full end-to-end Jira ticket creation and completion requires the companion fidesplus PR.

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

🤖 Generated with Claude Code

Auto-create default ManualTaskConfig (access + erasure) with a "complete"
checkbox when a jira_ticket connection is created, so the DSR graph blocks
until the Jira ticket is closed or the operator submits manually.

Use pending_external (not requires_input) for Jira manual tasks, and treat
it equivalently in the watchdog and duplication detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Mar 26, 2026 9:16pm
fides-privacy-center Ignored Ignored Mar 26, 2026 9:16pm

Request Review

@JadeCara JadeCara force-pushed the ENG-3146-jira-dsr-lifecycle branch from 8a24b62 to e0fe93b Compare March 26, 2026 20:59
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JadeCara JadeCara force-pushed the ENG-3146-jira-dsr-lifecycle branch from e0fe93b to e6746ca Compare March 26, 2026 21:01
@JadeCara JadeCara marked this pull request as ready for review March 26, 2026 21:01
@JadeCara JadeCara requested a review from a team as a code owner March 26, 2026 21:01
@JadeCara JadeCara requested review from erosselli and removed request for a team March 26, 2026 21:01
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

The overall approach is sound — using pending_external to distinguish "waiting on Jira" from "waiting on operator input" is a clean semantic improvement, and the watchdog/duplication detection guards follow the same pattern as the existing requires_input handling. The docstring on _create_default_jira_configs is clear and the reasoning for access-only is well explained.

Critical (Must Fix)

  • Import ordering (isort violation) in connection_service.pyfrom fides.api.schemas.policy import ActionType is placed between two fides.api.models imports. This will likely fail CI linting. See inline comment.

Suggestions

  • Unnecessary single-element loopfor action_type in (ActionType.access,): loops exactly once by design. A direct call without the loop would be clearer. See inline comment.
  • Stale log message — the warning in request_service.py still says "may be waiting for manual input" even though it now covers pending_external (an external system). See inline comment.
  • Mixed-task status overwrite — the status guard in manual_task_graph_task.py could overwrite a sibling task's status if a request ever had both regular manual tasks and Jira tasks. Probably not a real scenario today, but worth a comment noting that assumption. See inline comment.

Nice to Have

  • Test coverage for _create_default_jira_configs — the new method has no direct tests. Adding assertions for the created config and field (and verifying the update-path idempotency via the not connection_config.manual_task guard) would give confidence this wiring holds. See inline comment.

Also note: the PR description's "Steps to Confirm" (step 2) says "Verify a ManualTask is created with two ManualTaskConfig records (access + erasure)" — but the code only creates one access config. The code and docstring are correct; the PR description should be updated to match.

ManualTaskParentEntityType,
ManualTaskType,
)
from fides.api.schemas.policy import ActionType
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Import ordering (isort violation): from fides.api.schemas.policy import ActionType is inserted between two fides.api.models imports. models.saas_template_dataset on line 36 should appear before this schemas import. The correct order is:

from fides.api.models.manual_task import (...)
from fides.api.models.saas_template_dataset import SaasTemplateDataset
from fides.api.schemas.policy import ActionType
from fides.api.schemas.connection_configuration import (...)

This will likely fail isort CI checks.

and the erasure node finds no erasure config so it completes immediately.
The Jira ticket tracks the whole request, not individual action types.
"""
for action_type in (ActionType.access,):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unnecessary loop over a single-element tuple: for action_type in (ActionType.access,): loops exactly once. The docstring clearly states only an access config is created by design, so this should just be a direct call without the loop. The loop implies iteration that never actually happens and leaves the reader wondering if other action types were intended.

# Instead of the for loop:
config = ManualTaskConfig.create(
    db=self.db,
    data={
        "task_id": manual_task.id,
        "config_type": ActionType.access,
        ...
    },
)
ManualTaskConfigField.create(...)

f"(privacy request {privacy_request.id}) in requires_input status - "
f"(privacy request {privacy_request.id}) in {privacy_request.status.value} status - "
f"keeping request in current status as it may be waiting for manual input"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale log message: The warning still says "keeping request in current status as it may be waiting for manual input" even after the status was generalized. For pending_external, "waiting for manual input" is misleading — it's waiting on an external system (Jira), not a human operator filling in a form. Consider something like "keeping request in current status as it is intentionally waiting (manual input or external system)" to match the comment above it.

if manual_task.task_type == ManualTaskType.jira_ticket
else PrivacyRequestStatus.requires_input
)
if self.resources.request.status != awaiting_status:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Status transition guard may produce unexpected behavior for mixed-task requests: If a request has both a regular manual task (setting requires_input) and a Jira task (setting pending_external), the last task to run will overwrite the status set by the first. The current guard only skips the save if the status already equals the new awaiting status — it doesn't protect against overwriting a sibling task's status.

This may not be a real scenario today, but worth calling out or adding a comment explaining that mixed task types on a single connection aren't expected.


return ConnectionConfigurationResponse.model_validate(connection_config)

def _create_default_jira_configs(self, manual_task: ManualTask) -> None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No tests for _create_default_jira_configs: This new method has no direct test coverage in this PR. The existing tests/service/test_jira_ticket_connection.py doesn't appear to cover this path. It would be valuable to add tests verifying:

  • A ManualTaskConfig with config_type=access is created when a jira_ticket connection is set up.
  • A ManualTaskConfigField with field_key="complete" and field_type=checkbox is created.
  • That re-creating the connection (update path) does not create duplicate configs (gated by not connection_config.manual_task).

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR wires up the Jira DSR lifecycle in Fides by (1) auto-creating a ManualTaskConfig + ManualTaskConfigField when a jira_ticket connection is created, (2) setting pending_external instead of requires_input for Jira manual tasks so the two waiting states are visually distinguishable in the Admin UI, and (3) treating pending_external identically to requires_input in the watchdog, interrupted-task requeue, and duplication-detection subsystems.

  • connection_service.py_create_default_jira_configs creates a single access-type config with a "complete" checkbox; guard on not connection_config.manual_task prevents duplicate creation on updates.
  • manual_task_graph_task.pyawaiting_status is now derived from the task type, cleanly separating Jira vs. operator-input semantics with a minimal conditional.
  • request_service.pypending_external added to three distinct status-list checks (watchdog query, requeue query, no-auto-error guard), all consistent with each other.
  • duplication_detection.pypending_external appended to ACTIONED_REQUEST_STATUSES, preventing a Jira-paused request from being treated as a duplicate submission target.

Two minor style findings in connection_service.py: the new ActionType import is placed between model-layer imports rather than with the other schema imports, and _create_default_jira_configs uses a single-element for loop despite the docstring stating only one config type is ever created.

Confidence Score: 4/5

PR is safe to merge; the functional logic is correct and all three status-list sites are updated consistently.

The core changes — status discrimination, watchdog/requeue/duplication updates, and auto-config creation — are all logically sound and internally consistent. The only findings are two non-blocking style issues in connection_service.py (import ordering and an unnecessary single-element loop). Neither affects runtime behaviour.

src/fides/service/connection/connection_service.py — minor import grouping and loop-style issues worth tidying before merge.

Important Files Changed

Filename Overview
src/fides/service/connection/connection_service.py Adds auto-creation of ManualTaskConfig + ManualTaskConfigField for jira_ticket connections; logic is sound but has a minor import ordering issue and an unnecessary single-element loop.
src/fides/api/task/manual/manual_task_graph_task.py Correctly sets pending_external vs requires_input based on task type; logic is clean and the status-equality guard is preserved.
src/fides/api/service/privacy_request/request_service.py pending_external added to the watchdog in-progress query and the no-auto-error guard; correctly mirrors the existing requires_input treatment in all three locations.
src/fides/api/service/privacy_request/duplication_detection.py pending_external added to ACTIONED_REQUEST_STATUSES alongside requires_input; semantically correct and consistent with the other status-list updates in this PR.
changelog/7772-jira-dsr-lifecycle.yaml Standard changelog entry; accurate description of changes.

Reviews (1): Last reviewed commit: "Merge branch 'main' into ENG-3146-jira-d..." | Re-trigger Greptile

Comment on lines 35 to 36
from fides.api.schemas.policy import ActionType
from fides.api.models.saas_template_dataset import SaasTemplateDataset
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Schema import breaks model import grouping

ActionType was added between two models.* imports, interrupting the import grouping convention in this file. It should be moved to sit alongside the other schemas.* imports (lines 37–51).

Suggested change
from fides.api.schemas.policy import ActionType
from fides.api.models.saas_template_dataset import SaasTemplateDataset
from fides.api.models.saas_template_dataset import SaasTemplateDataset
from fides.api.schemas.policy import ActionType

Rule Used: Python imports should always be placed at the top ... (source)

Learnt From
ethyca/fides#6536

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

and the erasure node finds no erasure config so it completes immediately.
The Jira ticket tracks the whole request, not individual action types.
"""
for action_type in (ActionType.access,):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unnecessary single-element loop

The docstring directly above explicitly states "Only an access config is needed," yet the code iterates over a single-element tuple (ActionType.access,). This adds cognitive overhead for no benefit — a reader will wonder whether this is intentional future-proofing or a bug. If there is a plan to add more action_type variants later, a short comment would clarify that intent; if not, the loop should be removed in favour of a direct call.

Suggested change
for action_type in (ActionType.access,):
config = ManualTaskConfig.create(
db=self.db,
data={
"task_id": manual_task.id,
"config_type": ActionType.access,
"version": 1,
"is_current": True,
},
)
ManualTaskConfigField.create(
db=self.db,
data={
"task_id": manual_task.id,
"config_id": config.id,
"field_key": "complete",
"field_type": ManualTaskFieldType.checkbox,
"field_metadata": {
"label": "Mark as complete",
"required": True,
},
},
)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Jade Wibbels and others added 2 commits March 26, 2026 15:12
- Fix stale log message to cover pending_external status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JadeCara
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough reviews @claude and @greptile-apps! Addressing each finding:

Import ordering — Already in the correct fides.api.schemas group at line 57, between other schema imports. No change needed.

Unnecessary single-element loop — Fixed — now loops (ActionType.access, ActionType.erasure) to create both configs, which we confirmed is needed for erasure DSRs to block correctly.

Stale log message — Fixed in 1dfc260 — now says "waiting for input or an external system."

PR description mismatch ("two configs") — Now accurate since we create both access + erasure configs.

Mixed-task status overwrite — Valid observation. Today a request can't have both regular manual tasks and Jira tasks on the same connection, so this is safe. Will add a comment if we ever support mixed types.

Test coverage — Agreed this would be valuable. Will address in a follow-up.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Thanks for the comprehensive fixes! I've verified everything:

Import ordering — Confirmed at line 58 (between other fides.api.schemas imports). You were off by one line but the placement is correct.

Loop fix — Verified in _create_default_jira_configs:

for action_type in (ActionType.access, ActionType.erasure):

Both configs are now created as needed.

Log message — Updated to "waiting for input or an external system" in request_service.py:681.

PR description — Now accurately reflects the two-config setup.

Mixed-task concern — Reasonable approach. The current architecture prevents mixing types per connection, so the status-setting logic is safe.

Test coverage — Follow-up approach makes sense given the scope.

All the critical issues have been addressed. The implementation looks solid for the Jira DSR lifecycle support!

@JadeCara JadeCara added this pull request to the merge queue Mar 26, 2026
Merged via the queue into main with commit 52dbdfc Mar 26, 2026
58 checks passed
@JadeCara JadeCara deleted the ENG-3146-jira-dsr-lifecycle branch March 26, 2026 23:41
JadeCara added a commit that referenced this pull request Mar 26, 2026
)

Co-authored-by: Jade Wibbels <jade@ethyca.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude claude bot mentioned this pull request Mar 31, 2026
18 tasks
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.

2 participants