From 473f1b2da8d1ea849ea65cb586edda219f2206ef Mon Sep 17 00:00:00 2001 From: inference-bot Date: Sun, 13 Sep 2026 13:33:30 -0600 Subject: [PATCH] Fix fetch_members GraphQL (email needs inline fragment); add fail-safe against empty-member-list deactivation --- app/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 4295feb..657ea4d 100644 --- a/app/main.py +++ b/app/main.py @@ -157,7 +157,7 @@ async def fetch_members() -> list[dict]: logger.warning("OC_PERSONAL_TOKEN not set; cannot fetch members") return [] query = ( - '{ collective(slug: "%s") { members(limit: 100) { nodes { role account { email name slug } } } } }' + '{ collective(slug: "%s") { members(limit: 100) { nodes { role account { name slug ... on Individual { email } } } } } }' % OC_COLLECTIVE_SLUG ) async with httpx.AsyncClient() as client: @@ -783,6 +783,13 @@ async def reconcile_memberships() -> dict: active = await fetch_members() active_emails = {m["email"] for m in active} + # FAIL-SAFE: if the OC fetch returned nothing (token expired, OC outage, + # or a query error), we must NOT deactivate everyone — that would be a + # catastrophic false-positive. Skip reconciliation entirely in that case. + if not active: + logger.warning("Sweep: fetch_members returned empty; skipping reconciliation (fail-safe)") + return {"status": "skipped", "reason": "empty_member_list", "provisioned": 0, "deactivated": 0} + # Currently provisioned members (from our DB). conn = get_db() rows = conn.execute("SELECT email, cloudron_user_id FROM members WHERE active = 1").fetchall()