Azure persistent_finding_state never resolves findings because the head SHA is empty
Git provider
Azure DevOps
System info
- PR-Agent Docker image:
pragent/pr-agent:0.45.0 - Invocation: CLI from an Azure DevOps build-validation pipeline
- Reproduced against current
mainat8b09d86f0f50b586fb797cf7ab7ad1958fe34f37 - Relevant settings:
[pr_reviewer]
persistent_comment = true
persistent_finding_state = true
inline_key_issues = true
[azure_devops_server]
agent_identity = "<stable Azure identity UUID>"Bug details
persistent_finding_state cannot resolve findings across full Azure DevOps review runs because the current PR head SHA is always recorded as an empty string.
In a real Azure DevOps PR, the persistent review marker after multiple pushes contains:
{
"last_run": {
"complete": true,
"excluded_files": [],
"head_sha": "",
"kind": "full",
"run_id": "https://dev.azure.com/.../commit/<actual-head-sha>"
}
}The visible run_id advances to the latest commit, but head_sha remains empty. Previously reported findings therefore remain ACTIVE. If the model restates the same defect with different wording, its body-derived finding ID also changes, so Azure receives another inline thread even when the earlier thread was answered and marked fixed, closed, or byDesign.
Root cause
PRReviewer._review_head_sha() only reads git_provider.last_commit_id:
def _review_head_sha(self) -> str:
last_commit = getattr(self.git_provider, "last_commit_id", None)
if isinstance(last_commit, str):
return last_commit
for attribute in ("sha", "id"):
value = getattr(last_commit, attribute, None)
if isinstance(value, str):
return value
return ""AzureDevopsProvider never populates last_commit_id. The Azure PR object already exposes the head as:
provider.pr.last_merge_commit.commit_idThe reconciliation guard requires both the previous and current head SHA to be non-empty and different. With Azure's empty value, resolution_allowed can never become true.
Minimal reproduction
Run against current main:
from types import SimpleNamespace
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
from pr_agent.tools.pr_reviewer import PRReviewer
provider = AzureDevopsProvider.__new__(AzureDevopsProvider)
provider.pr = SimpleNamespace(
last_merge_commit=SimpleNamespace(commit_id="head-2")
)
reviewer = PRReviewer.__new__(PRReviewer)
reviewer.git_provider = provider
assert provider.pr.last_merge_commit.commit_id == "head-2"
assert reviewer._review_head_sha() == "head-2" # actual: ""The downstream effect can be reproduced directly:
from pr_agent.algo.review_finding_state import reconcile_review_findings
finding = {
"body": "Old finding",
"path": "app.py",
"line_start": 1,
"line_end": 1,
}
previous = reconcile_review_findings(
None,
[finding],
allow_resolution=True,
head_sha="head-1",
).state
result = reconcile_review_findings(
previous,
[],
allow_resolution=True,
head_sha="",
)
assert result.resolved_ids # actual: ()
assert result.state["findings"][0]["state"] == "RESOLVED" # actual: ACTIVEExpected behavior
- A full Azure review records the actual source/head commit SHA.
- On a later full review of a different head, absent findings can transition from
ACTIVEtoRESOLVED. - The regression test should use an Azure provider-shaped object rather than a generic mock that manually supplies
last_commit_id.
Possible fix
Populate a stable head-SHA contract for Azure, for example by assigning the source commit ID during set_pr():
self.last_commit_id = self.pr.last_merge_commit.commit_idAlternatively, introduce a provider method such as get_pr_head_sha() and have the review-state code call that instead of depending on the GitHub-shaped last_commit_id attribute.
Source: The-PR-Agent/pr-agent