Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion backend/routers/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,13 +333,23 @@ async def download_latest_desktop_release(
):
"""
Redirect to the latest desktop release DMG installer.
Resolves from GitHub release assets filtered by channel.
Stable resolves from the latest stable-tagged release.
Beta always resolves from the newest desktop GitHub release, regardless of channel metadata.
Defaults to stable channel (for macos.omi.me). Use channel=beta for QA.
"""
desktop_releases = await _get_live_desktop_releases(platform)
if not desktop_releases:
raise HTTPException(status_code=404, detail=f"No live desktop releases found for platform: {platform}")

if channel == "beta":
# Beta downloads should always expose the newest GitHub desktop build,
# even if the release-channel promotion metadata is stale.
for entry in desktop_releases:
dmg_url = _get_dmg_download_url(entry["release"])
if dmg_url:
return RedirectResponse(url=dmg_url, status_code=302)
raise HTTPException(status_code=404, detail="No DMG installer found for latest beta release")

# Find latest release matching the requested channel
for entry in desktop_releases:
if entry["channel"] != channel:
Expand Down
23 changes: 22 additions & 1 deletion backend/tests/unit/test_desktop_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,28 @@ async def test_beta_no_fallback(self):
with patch("routers.updates._get_live_desktop_releases", new_callable=AsyncMock, return_value=mock_releases):
async with AsyncClient(transport=ASGITransport(app=_test_app), base_url="http://test") as client:
resp = await client.get("/v2/desktop/download/latest?channel=beta")
assert resp.status_code == 404
assert resp.status_code == 302
assert resp.headers["location"] == "https://example.com/stable.dmg"

@pytest.mark.asyncio
async def test_beta_uses_latest_release_even_if_marked_stable(self):
mock_releases = [
{
"channel": "stable",
"release": {"assets": [_dmg_asset("https://example.com/latest.dmg")]},
},
{
"channel": "beta",
"release": {"assets": [_dmg_asset("https://example.com/older-beta.dmg")]},
},
]
with patch("routers.updates._get_live_desktop_releases", new_callable=AsyncMock, return_value=mock_releases):
async with AsyncClient(
transport=ASGITransport(app=_test_app), base_url="http://test", follow_redirects=False
) as client:
resp = await client.get("/v2/desktop/download/latest?channel=beta")
assert resp.status_code == 302
assert resp.headers["location"] == "https://example.com/latest.dmg"

@pytest.mark.asyncio
async def test_404_when_no_dmg_asset(self):
Expand Down