Compare commits
No commits in common. "b11ec6a732d7cf0ad44ae9a0b8f51c83c5042a26" and "e0543b2007ad558d1b7b4784da90107564442cd8" have entirely different histories.
b11ec6a732
...
e0543b2007
|
|
@ -139,11 +139,7 @@ function createPullSheet({ fetchJson, storage, createConversationPager = globalT
|
||||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ expected_head_sha: expectedHeadSha }),
|
body: JSON.stringify({ expected_head_sha: expectedHeadSha }),
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
if (!result?.merged) {
|
if (!result?.merged) throw new Error('Pull request merge was not confirmed.');
|
||||||
throw new Error(result?.confirmation_pending && result?.error
|
|
||||||
? result.error
|
|
||||||
: 'Pull request merge was not confirmed.');
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}).finally(() => { mergeRequest = null; });
|
}).finally(() => { mergeRequest = null; });
|
||||||
return mergeRequest;
|
return mergeRequest;
|
||||||
|
|
|
||||||
|
|
@ -1470,21 +1470,6 @@ async def pull_completion_review(repository: str, number: int) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def is_pull_merged_at_head(
|
|
||||||
repository: str, number: int, expected_head_sha: str
|
|
||||||
) -> bool:
|
|
||||||
pull = await fetch(f"repos/{repository}/pulls/{number}")
|
|
||||||
if not isinstance(pull, dict):
|
|
||||||
return False
|
|
||||||
head_value = pull.get("head")
|
|
||||||
head = head_value if isinstance(head_value, dict) else {}
|
|
||||||
return (
|
|
||||||
pull.get("merged") is True
|
|
||||||
and pull.get("state") == "closed"
|
|
||||||
and head.get("sha") == expected_head_sha
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def merge_assigned_pull(
|
async def merge_assigned_pull(
|
||||||
repository: str, number: int, expected_head_sha: str
|
repository: str, number: int, expected_head_sha: str
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
|
||||||
21
src/main.py
21
src/main.py
|
|
@ -2068,26 +2068,9 @@ async def merge_assigned_pull(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
|
||||||
merged = await asyncio.wait_for(
|
|
||||||
gitea_proxy.is_pull_merged_at_head(
|
|
||||||
repository, number, submission.expected_head_sha
|
|
||||||
),
|
|
||||||
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
merged = False
|
|
||||||
if merged:
|
|
||||||
return {"number": number, "merged": True, "state": "closed"}
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{"error": "The pull request could not be merged. It remains in My Work; please retry."},
|
||||||
"number": number,
|
status_code=503,
|
||||||
"merged": False,
|
|
||||||
"state": "unknown",
|
|
||||||
"confirmation_pending": True,
|
|
||||||
"error": "Merge confirmation is pending. Check its status before retrying.",
|
|
||||||
},
|
|
||||||
status_code=202,
|
|
||||||
headers={"Retry-After": "1"},
|
headers={"Retry-After": "1"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2528,31 +2528,6 @@ Promise.all([first, duplicate]).then(async comments => {{
|
||||||
assert len(output["comments"]) == 2 and len(output["merges"]) == 2
|
assert len(output["comments"]) == 2 and len(output["merges"]) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_pull_sheet_surfaces_pending_merge_confirmation_guidance():
|
|
||||||
script = f"""
|
|
||||||
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
|
|
||||||
const controller = createPullSheet({{
|
|
||||||
storage: null,
|
|
||||||
fetchJson: () => Promise.resolve({{
|
|
||||||
number: 7,
|
|
||||||
merged: false,
|
|
||||||
state: 'unknown',
|
|
||||||
confirmation_pending: true,
|
|
||||||
error: 'Merge confirmation is pending. Check its status before retrying.',
|
|
||||||
}}),
|
|
||||||
}});
|
|
||||||
controller.merge({{repository:'stackchain/api', number:7}}, 'abc123')
|
|
||||||
.then(() => process.stdout.write(JSON.stringify({{resolved:true}})))
|
|
||||||
.catch(error => process.stdout.write(JSON.stringify({{resolved:false, message:error.message}})));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
||||||
|
|
||||||
assert json.loads(result.stdout) == {
|
|
||||||
"resolved": False,
|
|
||||||
"message": "Merge confirmation is pending. Check its status before retrying.",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_pull_sheet_enables_merge_only_for_current_safe_state():
|
def test_pull_sheet_enables_merge_only_for_current_safe_state():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
|
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -249,108 +247,6 @@ async def test_assigned_pull_merge_requires_current_eligible_head(monkeypatch):
|
||||||
assert calls == [("stackchain/api", 7, "abc123")]
|
assert calls == [("stackchain/api", 7, "abc123")]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_assigned_pull_merge_reconciles_acceptance_before_timeout(monkeypatch):
|
|
||||||
calls = []
|
|
||||||
merged = False
|
|
||||||
|
|
||||||
async def assigned(repository, number):
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def merge(repository, number, expected_head_sha):
|
|
||||||
nonlocal merged
|
|
||||||
calls.append(("merge", repository, number, expected_head_sha))
|
|
||||||
merged = True
|
|
||||||
await asyncio.sleep(0.05)
|
|
||||||
return {"number": number, "merged": True, "state": "closed"}
|
|
||||||
|
|
||||||
async def confirm(repository, number, expected_head_sha):
|
|
||||||
calls.append(("confirm", repository, number, expected_head_sha))
|
|
||||||
return merged
|
|
||||||
|
|
||||||
monkeypatch.setattr(main, "ISSUE_ACTION_TIMEOUT_SECONDS", 0.01)
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "is_assigned_pull", assigned)
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "merge_assigned_pull", merge)
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "is_pull_merged_at_head", confirm, raising=False)
|
|
||||||
transport = httpx.ASGITransport(app=main.app)
|
|
||||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
||||||
response = await client.post(
|
|
||||||
"/api/v1/repos/stackchain/api/pulls/7/merge",
|
|
||||||
json={"expected_head_sha": "abc123"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == {"number": 7, "merged": True, "state": "closed"}
|
|
||||||
assert calls == [
|
|
||||||
("merge", "stackchain/api", 7, "abc123"),
|
|
||||||
("confirm", "stackchain/api", 7, "abc123"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_gitea_merge_confirmation_requires_merged_expected_head():
|
|
||||||
requests = []
|
|
||||||
|
|
||||||
async def handler(request):
|
|
||||||
requests.append((request.method, request.url.path))
|
|
||||||
return httpx.Response(200, json={
|
|
||||||
"number": 7,
|
|
||||||
"state": "closed",
|
|
||||||
"merged": True,
|
|
||||||
"head": {"sha": "abc123"},
|
|
||||||
})
|
|
||||||
|
|
||||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
|
||||||
try:
|
|
||||||
confirmed = await gitea_proxy.is_pull_merged_at_head(
|
|
||||||
"stackchain/api", 7, "abc123"
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await gitea_proxy.stop_client()
|
|
||||||
|
|
||||||
assert confirmed is True
|
|
||||||
assert requests == [("GET", "/api/v1/repos/stackchain/api/pulls/7")]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_assigned_pull_merge_reports_unresolved_confirmation_without_false_state(monkeypatch):
|
|
||||||
calls = []
|
|
||||||
|
|
||||||
async def assigned(repository, number):
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def merge(repository, number, expected_head_sha):
|
|
||||||
calls.append(("merge", repository, number, expected_head_sha))
|
|
||||||
raise httpx.ReadTimeout("upstream response was lost")
|
|
||||||
|
|
||||||
async def confirm(repository, number, expected_head_sha):
|
|
||||||
calls.append(("confirm", repository, number, expected_head_sha))
|
|
||||||
return False
|
|
||||||
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "is_assigned_pull", assigned)
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "merge_assigned_pull", merge)
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "is_pull_merged_at_head", confirm)
|
|
||||||
transport = httpx.ASGITransport(app=main.app)
|
|
||||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
||||||
response = await client.post(
|
|
||||||
"/api/v1/repos/stackchain/api/pulls/7/merge",
|
|
||||||
json={"expected_head_sha": "abc123"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 202
|
|
||||||
assert response.json() == {
|
|
||||||
"number": 7,
|
|
||||||
"merged": False,
|
|
||||||
"state": "unknown",
|
|
||||||
"confirmation_pending": True,
|
|
||||||
"error": "Merge confirmation is pending. Check its status before retrying.",
|
|
||||||
}
|
|
||||||
assert calls == [
|
|
||||||
("merge", "stackchain/api", 7, "abc123"),
|
|
||||||
("confirm", "stackchain/api", 7, "abc123"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_assigned_pull_merge_returns_conflict_without_mutating_stale_head(monkeypatch):
|
async def test_assigned_pull_merge_returns_conflict_without_mutating_stale_head(monkeypatch):
|
||||||
async def assigned(repository, number):
|
async def assigned(repository, number):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user