feat(issue-filing): enforce pre-create duplicate title gate (#207)
Add server-side duplicate detection in gitea_create_issue that re-queries open and recently closed issues at mutation time, blocks normalized title matches unless the operator explicitly approves a split, and validates LLM duplicate-search summaries via review_proofs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+31
-2
@@ -170,6 +170,7 @@ from gitea_auth import ( # noqa: E402
|
||||
)
|
||||
import gitea_audit # noqa: E402
|
||||
import gitea_config # noqa: E402
|
||||
import issue_duplicate_gate # noqa: E402
|
||||
import role_session_router # noqa: E402
|
||||
|
||||
|
||||
@@ -436,6 +437,8 @@ def gitea_create_issue(
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
allow_duplicate_override: bool = False,
|
||||
split_from_issue: int | None = None,
|
||||
) -> dict:
|
||||
"""Create a new issue on a Gitea repository.
|
||||
|
||||
@@ -446,6 +449,8 @@ def gitea_create_issue(
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
allow_duplicate_override: Operator-approved split after duplicate found.
|
||||
split_from_issue: Existing duplicate issue number when overriding.
|
||||
|
||||
Returns:
|
||||
dict with 'number' of the created issue ('url' only with the reveal opt-in).
|
||||
@@ -462,9 +467,33 @@ def gitea_create_issue(
|
||||
}
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/issues"
|
||||
base = repo_api_url(h, o, r)
|
||||
open_issues = api_get_all(f"{base}/issues?state=open&type=issues", auth)
|
||||
closed_issues = api_get_all(
|
||||
f"{base}/issues?state=closed&type=issues", auth, limit=100
|
||||
)
|
||||
gate = issue_duplicate_gate.pre_create_issue_duplicate_gate(
|
||||
title,
|
||||
list(open_issues) + list(closed_issues),
|
||||
allow_override=allow_duplicate_override,
|
||||
split_from_issue=split_from_issue,
|
||||
)
|
||||
if not gate.get("performed"):
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"number": None,
|
||||
"duplicate_gate": gate["verdict"],
|
||||
"matches": gate.get("matches", []),
|
||||
"reasons": gate.get("reasons", []),
|
||||
}
|
||||
issue_body = body
|
||||
if gate.get("override_applied") and split_from_issue is not None:
|
||||
rel = f"Operator-approved split from #{split_from_issue}."
|
||||
issue_body = f"{rel}\n\n{body}" if body else rel
|
||||
url = f"{base}/issues"
|
||||
try:
|
||||
data = api_request("POST", url, auth, {"title": title, "body": body})
|
||||
data = api_request("POST", url, auth, {"title": title, "body": issue_body})
|
||||
except Exception as exc:
|
||||
_audit("create_issue", host=h, remote=remote, org=o, repo=r,
|
||||
result=gitea_audit.FAILED, reason=_redact(str(exc)),
|
||||
|
||||
Reference in New Issue
Block a user