feat: track exact post-merge release progress (Closes #1092)
This commit is contained in:
1 parent
087300f9c3
commit
6db0802461
11 files changed
+374
-3
No files matched your search
@@ -733,3 +733,96 @@ async def test_gitea_pull_release_removes_current_login_case_insensitively():
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert released["assignees"] == ["sam"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_merge_returns_the_exact_merge_commit_for_release_tracking():
|
||||
async def handler(request):
|
||||
if request.url.path.endswith("/pulls/7"):
|
||||
return httpx.Response(200, json={
|
||||
"number": 7, "state": "open", "draft": False, "mergeable": True,
|
||||
"merged": False, "head": {"sha": "abc123"},
|
||||
})
|
||||
if request.url.path.endswith("/commits/abc123/status"):
|
||||
return httpx.Response(200, json={"state": "success"})
|
||||
if request.url.path.endswith("/pulls/7/merge"):
|
||||
return httpx.Response(200, json={"merged": True, "sha": "merge456"})
|
||||
raise AssertionError(request.url.path)
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.merge_assigned_pull("stackchain/api", 7, "abc123")
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert result == {
|
||||
"number": 7, "merged": True, "state": "closed", "merge_commit_sha": "merge456"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_release_receipt_matches_only_the_captured_commit_and_reports_checks(monkeypatch):
|
||||
async def receipt(repository, commit_sha):
|
||||
assert (repository, commit_sha) == ("stackchain/api", "merge456")
|
||||
return {
|
||||
"commit_sha": "merge456",
|
||||
"ci_state": "pending",
|
||||
"checks": [{"name": "browser", "state": "pending", "url": ""}],
|
||||
"release": None,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "release_receipt_status", receipt, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
"/api/v1/repos/stackchain/api/release-receipt/merge456"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json()["checks"] == [
|
||||
{"name": "browser", "state": "pending", "url": ""}
|
||||
]
|
||||
assert response.json()["release"] is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_release_receipt_ignores_other_commits_and_normalizes_matching_assets():
|
||||
forge = gitea_proxy.GITEA_URL.rstrip("/")
|
||||
|
||||
async def handler(request):
|
||||
if request.url.path.endswith("/commits/merge456/status"):
|
||||
return httpx.Response(200, json={
|
||||
"state": "success",
|
||||
"statuses": [{"context": "browser", "status": "success", "target_url": ""}],
|
||||
})
|
||||
if request.url.path.endswith("/releases"):
|
||||
return httpx.Response(200, json=[
|
||||
{"tag_name": "newer", "target_commitish": "other789", "html_url": f"{forge}/stackchain/api/releases/tag/newer"},
|
||||
{
|
||||
"tag_name": "rc-42", "target_commitish": "merge456",
|
||||
"html_url": f"{forge}/stackchain/api/releases/tag/rc-42",
|
||||
"assets": [
|
||||
{"name": "manifest.json", "browser_download_url": f"{forge}/stackchain/api/releases/download/rc-42/manifest.json"},
|
||||
],
|
||||
},
|
||||
])
|
||||
raise AssertionError(request.url.path)
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.release_receipt_status("stackchain/api", "merge456")
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert result["commit_sha"] == "merge456"
|
||||
assert result["ci_state"] == "success"
|
||||
assert result["checks"] == [{"name": "browser", "state": "success", "url": ""}]
|
||||
assert result["release"] == {
|
||||
"tag": "rc-42",
|
||||
"url": f"{forge}/stackchain/api/releases/tag/rc-42",
|
||||
"assets": [{
|
||||
"name": "manifest.json",
|
||||
"url": f"{forge}/stackchain/api/releases/download/rc-42/manifest.json",
|
||||
}],
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE = Path(__file__).parents[1] / "frontend" / "release-receipt.js"
|
||||
|
||||
|
||||
def run_node(body: str) -> dict:
|
||||
script = f"const createReleaseReceipt=require({json.dumps(str(SOURCE))});\n" + body
|
||||
completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_receipt_survives_reload_for_confirmed_account_only_and_tracks_exact_commit():
|
||||
output = run_node(r"""
|
||||
const values=new Map();
|
||||
const storage={getItem:key=>values.has(key)?values.get(key):null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
let login='timmy'; const calls=[];
|
||||
const first=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:async path=>{calls.push(path);return {commit_sha:'merge456',ci_state:'pending',checks:[{name:'browser',state:'pending',url:''}],release:null};}});
|
||||
const captured=first.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'});
|
||||
const restored=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:first.fetchJson}).restore();
|
||||
restored && first.refresh().then(status=>{
|
||||
login='alex';
|
||||
const other=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:first.fetchJson}).restore();
|
||||
process.stdout.write(JSON.stringify({captured,restored,status,other,calls,keys:[...values.keys()]}));
|
||||
});
|
||||
""")
|
||||
|
||||
assert output["captured"]["commit_sha"] == "merge456"
|
||||
assert output["restored"]["repository"] == "stackchain/api"
|
||||
assert output["status"]["label"] == "Checks running"
|
||||
assert output["status"]["checks"] == ["browser"]
|
||||
assert output["other"] is None
|
||||
assert output["calls"] == ["api/v1/repos/stackchain/api/release-receipt/merge456"]
|
||||
assert output["keys"] == ["stackchain.release-receipt.v1:timmy"]
|
||||
|
||||
|
||||
def test_receipt_distinguishes_failed_checks_waiting_for_release_and_exact_release():
|
||||
output = run_node(r"""
|
||||
const values=new Map();
|
||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
||||
const responses=[
|
||||
{commit_sha:'merge456',ci_state:'failure',checks:[{name:'unit',state:'success'},{name:'browser',state:'failure'}],release:null},
|
||||
{commit_sha:'merge456',ci_state:'success',checks:[{name:'browser',state:'success'}],release:null},
|
||||
{commit_sha:'merge456',ci_state:'success',checks:[{name:'browser',state:'success'}],release:{tag:'rc-42',url:'https://forge.example/git/x/releases/tag/rc-42',assets:[{name:'manifest.json',url:'https://forge.example/manifest'}]}},
|
||||
];
|
||||
const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',fetchJson:async()=>responses.shift()});
|
||||
receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'});
|
||||
(async()=>{
|
||||
const failed=await receipt.refresh(); const waiting=await receipt.refresh(); const released=await receipt.refresh();
|
||||
receipt.dismiss();
|
||||
process.stdout.write(JSON.stringify({failed,waiting,released,restored:receipt.restore()}));
|
||||
})();
|
||||
""")
|
||||
|
||||
assert output["failed"]["label"] == "Checks failed"
|
||||
assert output["failed"]["checks"] == ["browser"]
|
||||
assert output["waiting"]["label"] == "Checks passed · waiting for release"
|
||||
assert output["released"]["label"] == "Released · rc-42"
|
||||
assert output["released"]["release"]["assets"][0]["name"] == "manifest.json"
|
||||
assert output["restored"] is None
|
||||
|
||||
|
||||
def test_mobile_release_receipt_is_wired_into_the_merge_flow_and_phone_safe():
|
||||
root = Path(__file__).parents[1]
|
||||
html = (root / "frontend" / "index.html").read_text()
|
||||
css = (root / "frontend" / "dashboard.css").read_text()
|
||||
dashboard = (root / "frontend" / "dashboard.js").read_text()
|
||||
|
||||
assert '<script src="static/release-receipt.js"></script>' in html
|
||||
assert 'id="release-receipt-launcher"' in html
|
||||
assert 'id="release-receipt-sheet"' in html
|
||||
assert 'releaseReceipt.capture(merging, mergeResult)' in dashboard
|
||||
assert "min-height:44px" in css[css.index(".release-receipt-sheet"):]
|
||||
assert "overflow-x:hidden" in css[css.index(".release-receipt-sheet"):]
|
||||
@@ -1206,6 +1206,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/queue-today.js",
|
||||
"/dashboard/static/pull-sheet.js",
|
||||
"/dashboard/static/review-sheet.js",
|
||||
"/dashboard/static/release-receipt.js",
|
||||
"/dashboard/static/work-route.js",
|
||||
"/dashboard/static/task-overlay-history.js",
|
||||
"/dashboard/static/context-poller.js",
|
||||
|
||||
Reference in new issue
Block a user