feat: apply search release plans atomically (Closes #817)
This commit is contained in:
1 parent
9a24aff537
commit
53539e0cdf
5 files changed
+184
-25
No files matched your search
@@ -1969,6 +1969,58 @@ async def update_assigned_issue_milestone(
|
||||
}
|
||||
|
||||
|
||||
async def update_assigned_issue_release_plan(
|
||||
repository: str, number: int, milestone_id: int, due_date: str | None
|
||||
) -> dict:
|
||||
path = f"repos/{repository}/issues/{number}"
|
||||
login, issue = await _current_login_and_target(path)
|
||||
if (
|
||||
issue.get("state") != "open"
|
||||
or isinstance(issue.get("pull_request"), dict)
|
||||
or not _login_in_users(login, issue.get("assignees"))
|
||||
):
|
||||
raise IssueNotAvailableError("assigned issue not found")
|
||||
|
||||
selected = next(
|
||||
(item for item in await repo_milestones(repository) if item["id"] == milestone_id),
|
||||
None,
|
||||
)
|
||||
if selected is None:
|
||||
raise ValueError("Unknown open repository milestone")
|
||||
|
||||
payload: dict = {"milestone": milestone_id}
|
||||
if due_date is not None:
|
||||
payload["due_date"] = due_date
|
||||
response = await _get_client().patch(
|
||||
f"/api/v1/{path}", headers=_auth(), json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
confirmed = response.json()
|
||||
milestone_value = confirmed.get("milestone") if isinstance(confirmed, dict) else None
|
||||
normalized = (
|
||||
{"id": milestone_value["id"], "title": milestone_value["title"]}
|
||||
if isinstance(milestone_value, dict)
|
||||
and isinstance(milestone_value.get("id"), int)
|
||||
and isinstance(milestone_value.get("title"), str)
|
||||
else None
|
||||
)
|
||||
expected_due_date = due_date if due_date is not None else issue.get("due_date")
|
||||
if (
|
||||
not isinstance(confirmed, dict)
|
||||
or confirmed.get("number") != number
|
||||
or normalized != selected
|
||||
or confirmed.get("due_date") != expected_due_date
|
||||
):
|
||||
raise ValueError("Gitea did not confirm the issue release plan")
|
||||
return {
|
||||
"repository": repository,
|
||||
"number": number,
|
||||
"state": confirmed.get("state", "open"),
|
||||
"milestone": normalized,
|
||||
"due_date": confirmed.get("due_date"),
|
||||
}
|
||||
|
||||
|
||||
async def issue_conversation_page(
|
||||
repository: str,
|
||||
number: int,
|
||||
|
||||
+43
@@ -763,6 +763,15 @@ class IssueMilestoneUpdate(BaseModel):
|
||||
milestone_id: PositiveInt | None = None
|
||||
|
||||
|
||||
class IssueReleasePlanUpdate(BaseModel):
|
||||
milestone_id: PositiveInt
|
||||
due_date: str | None = Field(
|
||||
default=None,
|
||||
pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$",
|
||||
max_length=20,
|
||||
)
|
||||
|
||||
|
||||
class IssueHandoff(BaseModel):
|
||||
recipient: str = Field(
|
||||
min_length=1, max_length=255, pattern=r"^[A-Za-z0-9_.-]+$"
|
||||
@@ -4374,6 +4383,40 @@ async def update_assigned_issue_milestone(
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.patch("/api/v1/repos/{owner}/{repo}/issues/{number}/release-plan")
|
||||
async def update_assigned_issue_release_plan(
|
||||
update: IssueReleasePlanUpdate,
|
||||
owner: str,
|
||||
repo: str,
|
||||
number: int = PathParam(gt=0),
|
||||
):
|
||||
repository = f"{owner}/{repo}"
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
gitea_proxy.update_assigned_issue_release_plan(
|
||||
repository, number, update.milestone_id, update.due_date
|
||||
),
|
||||
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
|
||||
)
|
||||
except gitea_proxy.IssueNotAvailableError:
|
||||
raise HTTPException(status_code=404, detail="Assigned issue not found")
|
||||
except ValueError as exc:
|
||||
if str(exc) == "Unknown open repository milestone":
|
||||
raise HTTPException(status_code=422, detail=str(exc))
|
||||
return JSONResponse(
|
||||
{"error": "The release plan could not be confirmed. Your selection is safe; please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "The release plan could not be updated. Your selection is safe; please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.get("/api/v1/repos/{owner}/{repo}/milestones")
|
||||
async def repository_milestones(owner: str, repo: str):
|
||||
repository = f"{owner}/{repo}"
|
||||
|
||||
Reference in new issue
Block a user