"""「这个组织现在能不能自动操作」—— 一个入口,三种答案。 上层(申请链路 / 离职回收 / 对账)都需要同一件事:拿到一个能操作某个 组织的驱动。它们**不**需要各自去处理「凭证还没配」「集成被停用」「私钥 解不开」这三种失败,更不该各自决定这时候该怎么办 —— 那样每加一个调用方 就多一份可能走岔的降级逻辑。 所以这里把结果收成一个 :class:`OrgDriverResolution`:要么拿到驱动,要么 拿到一个**可以直接说给人听**的原因。降级到人工的判断留在调用方(离职 回收和一张审批单该做的事不一样),但「为什么降级」这件事只在这里算一次。 刻意不抛异常:凭证没配好是一个**预期中的运行状态**(登记先行、凭证后到 是正常的上线顺序),不是一个错误。用异常表达它,会逼每个调用方写 try/except,而其中总有一个会把它和真正的故障一起吞掉。 """ from __future__ import annotations from dataclasses import dataclass from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.integrations.driver_base import GithubOrgDriver from app.integrations.models import Integration from .models import GithubOrg #: 没配凭证。台账照常记,动作转人工。 REASON_NOT_CONFIGURED = "not_configured" #: 配了,但那个集成行被停用了。与「没配」分开:管理员看到的处置动作不同。 REASON_DISABLED = "disabled" #: 配了、启用着,但驱动构造不出来(私钥解不开、driver 被下线、类型不对)。 REASON_BROKEN = "broken" @dataclass class OrgDriverResolution: """要么 ``driver`` 有值,要么 ``reason`` 有值 —— 两者不会同时为空。""" org: GithubOrg driver: GithubOrgDriver | None = None reason: str | None = None detail: str | None = None @property def automatic(self) -> bool: """能不能自动干。调用方唯一需要问的那个问题。""" return self.driver is not None async def resolve_org_driver( db: AsyncSession, org: GithubOrg, ) -> OrgDriverResolution: """给一个组织找到能操作它的 GitHub 驱动。 组织自己的 ``integration_id`` 优先;没指定时回落到**唯一**一个启用的 ``kind='github_org'`` 集成 —— 只有一个凭证时让每个组织都去填一遍它, 是在制造一个必然会漂移的重复。但**有多个时不猜**:两个凭证覆盖不同 的组织集合是完全正常的配置,替管理员挑一个,挑错的后果是往错误的 组织里加人。 """ if org.integration_id: row = await db.get(Integration, org.integration_id) if row is None: return OrgDriverResolution( org=org, reason=REASON_NOT_CONFIGURED, detail=( f"organization '{org.slug}' points at integration " f"{org.integration_id} which no longer exists" ), ) else: rows = list( ( await db.execute( select(Integration).where( Integration.kind == GithubOrgDriver.kind, Integration.enabled.is_(True), ) ) ).scalars().all() ) if not rows: return OrgDriverResolution( org=org, reason=REASON_NOT_CONFIGURED, detail=( "no enabled GitHub organization integration is configured " "— register credentials at /admin/integrations" ), ) if len(rows) > 1: return OrgDriverResolution( org=org, reason=REASON_NOT_CONFIGURED, detail=( f"{len(rows)} GitHub organization integrations are " f"enabled and '{org.slug}' does not say which one to use " f"— set it on the organization" ), ) row = rows[0] if not row.enabled: return OrgDriverResolution( org=org, reason=REASON_DISABLED, detail=( f"integration '{row.slug or row.id}' is disabled — " f"re-enable it at /admin/integrations" ), ) try: from app.integrations.service import ( # noqa: PLC0415 build_driver_by_id, ) driver = await build_driver_by_id(row.id, session=db) except Exception as e: # noqa: BLE001 — 见模块文档:这不是异常路径 return OrgDriverResolution( org=org, reason=REASON_BROKEN, detail=f"{type(e).__name__}: {e}", ) if not isinstance(driver, GithubOrgDriver): # 集成行的 kind 被改成了别的东西,或者驱动被下线了。 return OrgDriverResolution( org=org, reason=REASON_BROKEN, detail=( f"integration '{row.slug or row.id}' resolves to " f"{type(driver).__name__}, which is not a GitHub " f"organization driver" ), ) return OrgDriverResolution(org=org, driver=driver) async def resolve_orgless_driver(db: AsyncSession) -> GithubOrgDriver | None: """A driver for GitHub calls that belong to no organization. ``GET /users/{login}`` — "does this handle exist" — is the only such call today. :func:`resolve_org_driver` cannot serve it: its whole contract is per-organization (App installation id, per-org credential routing), and picking an arbitrary org just to borrow its credentials would make an unrelated org's misconfiguration look like "this GitHub user does not exist". Returns ``None`` — never a reason string — because every caller of this is an ADVISORY check. "We could not ask GitHub" and "GitHub says no such user" must not collapse into one answer, so the caller renders the former as 未校验 rather than as a red flag against the employee. Deliberately requires EXACTLY one enabled integration, same as the org-less fallback above: with two credentials configured there is no principled way to choose. """ rows = list( ( await db.execute( select(Integration).where( Integration.kind == GithubOrgDriver.kind, Integration.enabled.is_(True), ) ) ).scalars().all() ) if len(rows) != 1: return None try: from app.integrations.service import ( # noqa: PLC0415 build_driver_by_id, ) driver = await build_driver_by_id(rows[0].id, session=db) except Exception: # noqa: BLE001 — advisory: an unbuildable driver is 未校验 return None return driver if isinstance(driver, GithubOrgDriver) else None