"""GitHub 组织:把「机器账号」和「别家公司的人」分成两种确认。 ## 为什么不能只用一个「已确认外部」 583 给了「认不出是谁」一条出路:人确认「这不是我们的员工」。上线后发现那 一条出路把两种完全不同的东西塞进了同一格: * ``outside_human`` —— 别家公司/基金会/高校的**人**。他持有访问权,访问 复核时要看的是「这个外部人还需要这个权限吗」; * ``bot`` —— 机器账号(``sre-bot`` / ``ti-chi-bot`` / ``tidb-ci-bot``)。 它永远不会是某个员工,而要问的问题是完全不同的一个:「这个自动化还在用 吗、它的 token 谁在管」。 把两者混在一格里,「已确认外部 331 条」这个数就再也回答不了任何问题。 ## 为什么 GitHub 自己的 type 字段用不了 第一反应是按 ``GET /users/{login}`` 的 ``type`` 判定,权威又省事。**实测 不行**:生产上 ``sre-bot`` / ``TestInfra-Bot`` / ``ti-chi-bot`` / ``ti-srebot`` / ``tidb-ci-bot`` / ``tidbcloud-bot`` / ``pingcap-zendesk-bot`` **七个全部报 ``type=User``**。GitHub 的 ``Bot`` 类型只给 GitHub App 身份(``dependabot[bot]`` 那种),而这些都是人建的普通 账号拿来当机器人用的 —— 那是这家公司的惯例,不是 GitHub 的概念。 所以判定只能来自**命名约定**(``-bot`` / ``bot-`` / ``-srebot`` / ``-ci``) 和资料名(``SRE bot`` / ``Ti Chi Robot``),而两者都是启发式。 **因此这一列永远由人确认写入,策略只负责「建议 + 批量」**,不自动落库 —— 把一个真名叫 ``robot-liu`` 的员工标成机器账号,后果是他离职时不被回收。 Revision ID: 585 Revises: 584 (原本编号 584。#3376 的 `584_cost_facts_raw_retention` 在我合并前先落到了 main —— 这是本次会话第二次撞号(上一次是 582)。**迁移号要对着 `origin/main` 抢,而且合并前必须再核一次**:第一次抢到时 main 最高是 583, 抢的那一刻没冲突,冲突是别人在我干活期间产生的。两个同号都 revises 同一个 父节点 ⇒ 重复 revision id ⇒ 两个 head ⇒ 每个后端测试分片和镜像构建全红。) """ import sqlalchemy as sa from alembic import op revision = "585" down_revision = "584" branch_labels = None depends_on = None TABLE = "github_org_memberships" SETTING_KEY = "github_org.bot_handle_patterns" #: 默认策略。来自生产台账里真实存在的那 8 个 handle 的共同形状, #: 不是凭想象列的通用词表 —— `ci` 单独一个词太宽(`cicada-lewis` 是个人)。 DEFAULT_PATTERNS = [ "*-bot", "bot-*", "*-bot-*", "*bot", "*robot*", "*-ci-bot", "*-srebot", ] def _has_table(bind, name: str) -> bool: return name in sa.inspect(bind).get_table_names() def _has_col(bind, table: str, col: str) -> bool: if not _has_table(bind, table): return False return col in {c["name"] for c in sa.inspect(bind).get_columns(table)} def upgrade() -> None: bind = op.get_bind() if _has_table(bind, TABLE) and not _has_col(bind, TABLE, "external_kind"): # 只在 external_confirmed_at 非空时有意义。刻意不加 CHECK 约束: # TiDB 对 CHECK 的支持历史上反复,而这条不变量由 service 层守 # (confirm_external 是唯一写入口)。 op.add_column( TABLE, sa.Column("external_kind", sa.String(16), nullable=True), ) # 策略默认值。存 platform_settings 而不是写死在代码里:新的 bot 命名 # 惯例会出现,而那不该等一次发布。 if _has_table(bind, "platform_settings"): import json bind.execute( sa.text( "INSERT INTO platform_settings (`key`, value, updated_at) " "SELECT :k, :v, NOW() FROM (SELECT 1) t " "WHERE NOT EXISTS (" " SELECT 1 FROM platform_settings WHERE `key` = :k)" ), {"k": SETTING_KEY, "v": json.dumps(DEFAULT_PATTERNS)}, ) def downgrade() -> None: bind = op.get_bind() if _has_table(bind, "platform_settings"): bind.execute( sa.text("DELETE FROM platform_settings WHERE `key` = :k"), {"k": SETTING_KEY}, ) if _has_col(bind, TABLE, "external_kind"): op.drop_column(TABLE, "external_kind")