fix(jira): close deleted findings#15050
Conversation
|
@valentijnscholten Didn't you do something similar recently? |
|
Yeah, I raised #14567 :-) |
valentijnscholten
left a comment
There was a problem hiding this comment.
Thanks for the PR. I think this is a usefule/needed addition. Some improvement areas:
- currently all pushing to JIRA is guarded by either the user explicitly selecting to push to JIRA in the UI or API request. Or by checking the value of
finding_jira_syncorpush_all_isues. At first glance the delete feature in this PR doesn't seem to align with that? - it could be possible that the new original already has a JIRA issues linked. Can you make sure that scenario is handled correctly (by skipping the reassign)
- could you add a comment to each JIRA issue that is closed or reassigned?
2fb57ff to
a46a20e
Compare
|
hello @valentijnscholten I made an update regarding your feedback. please review thanks :) |
valentijnscholten
left a comment
There was a problem hiding this comment.
Two things I don't see accomplished yet:
- users cannot delete a finding via UI or API and instruct Defect Dojo to "push_to_jira"
- if the new original already has an existing Jira_Issue, the current code might crash
30152a6 to
c6382d3
Compare
|
@valentijnscholten could you review this again thanks |
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
|
Thanks for tackling this — the close/reassign-on-delete flow is genuinely useful and the helper/services layering + unit tests are nicely done. A few things before it's ready, two of them blocking. Major1. Cascade deletes auto-close issues and bypass both the opt-in and the reassign logic
Net effect: deleting a product/engagement/test with JIRA-synced findings silently closes all their tickets. This is the riskiest path and no test covers it. Please route cascade deletes through the same opt-in/reassign logic, or explicitly exclude them. 2. Synchronous blocking JIRA calls inside the delete path
Design / behavioral3.
|
58c040e to
40fc33c
Compare
|
Conflicts have been resolved. A maintainer will review the pull request shortly. |
40fc33c to
c4de653
Compare
|
Conflicts have been resolved. A maintainer will review the pull request shortly. |
|
@valentijnscholten could give it a review I just pushed an update thanks |
|
@samiat4911 Did you test this latest version? I don't think the on_commit thing will work as it will fire the close task after the finding has been deleted. |
valentijnscholten
left a comment
There was a problem hiding this comment.
Thanks for iterating on this — the earlier feedback is addressed well: the JIRA push is now gated by is_delete_sync_allowed (proper three-state push_to_jira, plus the keep-in-sync/push-all fallback), users can opt in via the delete form / API param, the "new original already has a linked JIRA issue" case is guarded so it no longer crashes, and both the close and reassign paths post an explanatory comment. Test coverage for those scenarios is solid.
There are a few things to change before this can merge.
1. (Blocking) The close path doesn't actually close the issue in the real flow
JIRA_Issue.finding is OneToOneField(..., on_delete=models.CASCADE), so deleting the finding cascade-deletes the linked JIRA_Issue row in the same transaction.
The close is queued with transaction.on_commit(...), which fires after the delete commits — i.e. after the JIRA_Issue row is already gone. The task then does get_object_or_none(JIRA_Issue, id=jira_issue_id), gets None, and returns "Local JIRA issue ... was not found." without closing anything. So in production the linked issue is never closed.
The reassign path works only because it re-parents the JIRA_Issue to the new original before the cascade, so that row survives. The close path leaves it pointed at the doomed finding.
Fix: dispatch the close task before the delete (no on_commit) and pass the values it needs as arguments, so the task never re-queries the deleted row:
# while the JIRA_Issue is still alive (pre-delete):
dojo_dispatch_task(
close_deleted_finding_jira_issue,
jira_issue.jira_id, # remote issue id
get_jira_project(jira_issue).jira_instance_id, # JIRA_Instance survives the delete
finding.id,
)
@app.task
def close_deleted_finding_jira_issue(jira_id, jira_instance_id, finding_id):
jira_instance = JIRA_Instance.objects.get(id=jira_instance_id)
jira = get_jira_connection(jira_instance)
issue = jira.issue(jira_id)
if issue_from_jira_is_active(issue) and jira_transition(jira, issue, jira_instance.close_status_key):
jira.add_comment(jira_id, f"DefectDojo finding {finding_id} was deleted. Closed automatically.")The task can stay async — it just must not depend on a row that gets cascade-deleted. (JIRA_Instance/JIRA_Project are fine to load: they survive the delete.) The jira_issue.save(update_fields=["jira_change"]) can be dropped — that row is about to be deleted anyway.
2. (Blocking) Tests don't catch the above — please add an end-to-end test
The current tests pass because they mock the gap away: test_close_jira_issue_for_deleted_finding_queues_close_after_commit patches on_commit to run inline and patches dojo_dispatch_task, with a Mock finding that's never deleted; test_close_deleted_finding_jira_issue_closes_active_issue patches get_object_or_none to return a live issue. Neither exercises "real finding.delete() -> cascade -> task runs -> issue actually transitions."
Please add a test that deletes a real finding with a linked JIRA_Issue and asserts the JIRA transition was actually invoked (mocking only the JIRA HTTP boundary, not the DB lookups).
3. Replace the instance-attribute flags with parameters on delete()
The _jira_delete_sync_requested / _push_to_jira_on_delete / _skip_jira_close_on_delete attributes stashed on the instance (via set_push_to_jira_on_delete) only exist to smuggle parameters into the pre_delete signal. All three user-facing paths already call the model's Finding.delete() (single, bulk loop, and API perform_destroy), so this can be a normal parameter instead:
def delete(self, *args, push_to_jira=<UNSET sentinel>, product_grading_option=True, **kwargs):
finding_helper.finding_delete(self, push_to_jira=push_to_jira)
super().delete(*args, **kwargs)finding_delete then owns the reassign-vs-close decision directly (no _skip_* attribute — it's just if reassigned: ... else: close), and the JIRA block can move out of finding_pre_delete (the found_by.clear() cleanup stays). Use a sentinel default so delete() with no argument (programmatic deletes, cascades that bypass the method) means "no JIRA sync", preserving today's behavior, while None/True/False keep the three-state. Bulk delete just becomes find.delete(push_to_jira=push_to_jira).
4. Make the reassign comment async too
_reassign_jira_issue_to_new_original calls add_simple_comment(...) synchronously on the request/delete path (and before commit, so a rolled-back delete leaves an orphan comment in JIRA). For consistency with the close path and to keep deletes off the JIRA round-trip, dispatch the reassign comment the same async way.
Minor
- The UI checkbox is two-state, so it can only send
True/False, never theNone/"automatic" mode the API supports. Worth a line in the upgrade note so keep-in-sync users know they must tick the box in the UI.
44ea813 to
d7e56ae
Compare
|
@valentijnscholten I made an update could you pls review? thanks |
| transaction.on_commit( | ||
| lambda: jira_services.add_simple_comment_async( | ||
| jira_id, | ||
| jira_instance_id, | ||
| comment, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
can you disconnect this from on_commit? It brings more risks/complications than it solves.
d7e56ae to
472fd5e
Compare
valentijnscholten
left a comment
There was a problem hiding this comment.
Thanks for the thorough revision — all the earlier feedback is addressed. 👍
- The close no longer no-ops:
close_deleted_finding_jira_issueis dispatched before the delete with durable primitives (jira_id,jira_instance_id,finding_id) and the task usesget_object_or_none(JIRA_Instance, …), so it never reloads the cascade-deletedJIRA_Issuerow. - Real end-to-end coverage:
test_deleting_finding_with_push_to_jira_closes_linked_jira_issuedeletes an actual finding and asserts the JIRA transition fires (mocking only the JIRA HTTP boundary), plus no-push / cascade-safety / pre-delete-cleanup tests. - The instance-attribute flags are gone — replaced by a
push_to_jiraparameter onFinding.delete()with theDELETE_JIRA_SYNC_UNSETsentinel; reassign-vs-close is now plain control flow and the JIRA logic is out of thepre_deletesignal. - Both delete-sync paths are async and symmetric (primitive args, graceful
get_object_or_none), andtransaction.on_commitis removed from both the close and reassign paths as requested.
JIRA_Instance is on_delete=PROTECT, so deleting a finding/engagement/product never removes the row the tasks load, and a missing instance degrades to a logged no-op rather than a crash.
One thing to keep in mind (not blocking): with on_commit removed, a rolled-back delete would still close the ticket / post the reassign comment. That's the intended consequence of the simpler async dispatch.
LGTM.
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Description
Fixes a Jira synchronization bug where deleting a Finding in DefectDojo did not close the linked Jira issue.
This PR updates the Finding delete flow so that:
finding_pre_delete.Key implementation added:
When a deleted original Finding has a duplicate cluster and a new original is selected, the Jira issue is moved instead of closed:
The Jira helper now closes active linked Jira issues using the configured close transition and updates the local Jira change timestamp after a successful transition.
Test results
Added unit coverage in
unittests/test_jira_helper.pyfor:finding_pre_delete.Tested locally:
Also ran:
Documentation
No documentation changes were needed because this fixes existing Jira delete behavior and does not add user-facing configuration.
Checklist
This checklist is for your information.
dev.dev.bugfixbranch.