修 GitHub App 安装在 Postgres 上崩:适配层译 INSERT OR IGNORE,安装后增减仓库同步
This commit is contained in:
1 parent
f07b3ccc7c
commit
9fa80bef89
3 files changed
+101
-8
No files matched your search
+25
-3
@@ -310,9 +310,10 @@ def _load_postgres_driver():
|
||||
# 不重构所有 store 的前提:现有代码到处用 ``?`` 占位符、
|
||||
# ``row["col"]``、``conn.execute().fetchall()``、``commit`` /
|
||||
# ``rollback`` / ``close``、``cursor.lastrowid``、``cursor.rowcount``,
|
||||
# 以及少量 SQLite 方言(``INSERT OR REPLACE``、``PRAGMA
|
||||
# table_info``、``AUTOINCREMENT``、FTS5 虚拟表)。适配层在
|
||||
# 执行前做最小翻译,翻译不了的方言明确报错,绝不静默改语义:
|
||||
# 以及少量 SQLite 方言(``INSERT OR REPLACE``、``INSERT OR
|
||||
# IGNORE``、``PRAGMA table_info``、``AUTOINCREMENT``、FTS5 虚拟表)。
|
||||
# 适配层在执行前做最小翻译,翻译不了的方言明确报错,绝不静默
|
||||
# 改语义:
|
||||
# - ``?`` -> ``%s``:只转换单引号字符串字面量之外的 ``?``
|
||||
# (扫描时跟踪单引号与 ``''`` 转义,字面量里的 ``?`` 不动);
|
||||
# SQL 里已有的 ``%s``(例如调用方自己拼的 ``IN (%s)`` 占位串
|
||||
@@ -322,6 +323,8 @@ def _load_postgres_driver():
|
||||
# _OR_REPLACE_PK;未知表退化为 ``ON CONFLICT DO NOTHING``
|
||||
# (语义弱于 REPLACE,调用点只有 project_members 与
|
||||
# slack_users 两张已知主键表,不会走到退化分支)。
|
||||
# - ``INSERT OR IGNORE`` -> ``ON CONFLICT DO NOTHING``:无冲突
|
||||
# 目标,任何唯一冲突都跳过,不改已有行。
|
||||
# - ``PRAGMA table_info(<表>)`` -> information_schema 查询,
|
||||
# 返回 (cid, name, type, notnull, dflt_value, pk) 六元组,
|
||||
# 与 sqlite 的 PRAGMA table_info 行形状一致,调用方取 row[1]
|
||||
@@ -411,6 +414,7 @@ def _convert_placeholders(sql):
|
||||
_OR_REPLACE_PK = {
|
||||
"project_members": ("project_id", "user_id"),
|
||||
"slack_users": ("slack_id",),
|
||||
"github_app_config": ("id",),
|
||||
}
|
||||
|
||||
_IDENTITY_TABLES = frozenset(
|
||||
@@ -422,6 +426,10 @@ _OR_REPLACE_RE = _re.compile(
|
||||
r"^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*"
|
||||
r"VALUES\s*\((.*)\)\s*$",
|
||||
_re.IGNORECASE | _re.DOTALL)
|
||||
_OR_IGNORE_RE = _re.compile(
|
||||
r"^\s*INSERT\s+OR\s+IGNORE\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*"
|
||||
r"VALUES\s*\((.*)\)\s*$",
|
||||
_re.IGNORECASE | _re.DOTALL)
|
||||
_INSERT_TABLE_RE = _re.compile(
|
||||
r"^\s*INSERT\s+INTO\s+(?:\w+\.)?(\w+)", _re.IGNORECASE)
|
||||
_PRAGMA_INFO_RE = _re.compile(
|
||||
@@ -477,6 +485,20 @@ def _translate_sql(sql):
|
||||
"INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING"
|
||||
% (table, ", ".join(cols), values))
|
||||
return _convert_placeholders(translated), "normal"
|
||||
m = _OR_IGNORE_RE.match(stripped)
|
||||
if m:
|
||||
# INSERT OR IGNORE(SQLite 方言)-> ON CONFLICT DO NOTHING:
|
||||
# 无冲突目标即可,任何唯一冲突都跳过,不改已有行。2026-10-26
|
||||
# 实弹教训:GitHub App 安装事件在生产 Postgres 上因这句未
|
||||
# 翻译直接报语法错,安装记录丢了(delivery 已认领,重试会
|
||||
# 判 duplicate,事件无法靠重发恢复,只能卸载重装)。
|
||||
table = m.group(1)
|
||||
cols = m.group(2)
|
||||
values = m.group(3)
|
||||
translated = (
|
||||
"INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING"
|
||||
% (table, cols, values))
|
||||
return _convert_placeholders(translated), "normal"
|
||||
translated = _AUTOINCREMENT_RE.sub(
|
||||
"BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY", sql)
|
||||
translated = _convert_placeholders(translated)
|
||||
|
||||
+8
-5
@@ -2324,9 +2324,9 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
仍是配置里的 repo,不是任意 GitHub 仓库的自动克隆,
|
||||
见 README)。check_run / check_suite 只做通知解析,
|
||||
返回解析结果而不建任务、不改云端任务状态(CI 状态回填
|
||||
由 pr-checks 主动拉取)。installation 事件记安装记录
|
||||
(created/added 增行、removed 减行、deleted 清安装),
|
||||
不建任务。手动 webhook 与 App webhook 共存时同一评论
|
||||
由 pr-checks 主动拉取)。installation / installation_repositories
|
||||
事件记安装记录(created/added 增行、removed 减行、deleted
|
||||
清安装),不建任务。手动 webhook 与 App webhook 共存时同一评论
|
||||
会送两遍(delivery_id 不同):/fix-review 另有内容去重
|
||||
(repo + PR + 评论 id),只建一次任务。
|
||||
"""
|
||||
@@ -2373,8 +2373,11 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
store.close()
|
||||
except Exception:
|
||||
pass
|
||||
# installation 事件:记安装记录,不建任务。
|
||||
if event == "installation":
|
||||
# installation / installation_repositories 事件:记安装记录,
|
||||
# 不建任务。后者是安装后增减仓库的同步事件(added 增行、
|
||||
# removed 减行),与 installation 事件共用同一解析与动作
|
||||
# 分发;new_permissions_accepted 无需改行。
|
||||
if event in ("installation", "installation_repositories"):
|
||||
inst = parse_installation_event(payload)
|
||||
if inst is None:
|
||||
self._send_json(200, {"result": None})
|
||||
|
||||
@@ -242,6 +242,47 @@ class SignatureAnyTestCase(unittest.TestCase):
|
||||
self.assertFalse(verify_github_signature_any([], raw, "sha256=x"))
|
||||
|
||||
|
||||
class DialectTranslationTestCase(unittest.TestCase):
|
||||
"""Postgres 适配层翻译:2026-10-26 实弹回归——安装事件在生产
|
||||
Postgres 上因 INSERT OR IGNORE 未翻译直接报语法错。
|
||||
|
||||
直接测 _translate_sql(纯函数):函数发出的 SQL 在这里翻
|
||||
译成合法 Postgres,SQLite 路径不受影响(翻译只走 Pg 游标)。
|
||||
"""
|
||||
|
||||
def test_or_ignore_translates(self):
|
||||
from cloud.db import _translate_sql
|
||||
sql, kind = _translate_sql(
|
||||
"INSERT OR IGNORE INTO github_app_installations("
|
||||
"installation_id, account_login, repo_full_name, created_at)"
|
||||
" VALUES(?, ?, ?, ?)")
|
||||
self.assertEqual(kind, "normal")
|
||||
self.assertNotIn("OR IGNORE", sql)
|
||||
self.assertIn("ON CONFLICT DO NOTHING", sql)
|
||||
self.assertIn("%s", sql)
|
||||
self.assertNotIn("?", sql)
|
||||
|
||||
def test_or_ignore_with_null_value_translates(self):
|
||||
from cloud.db import _translate_sql
|
||||
sql, _ = _translate_sql(
|
||||
"INSERT OR IGNORE INTO github_fix_review_dedup("
|
||||
"repo, pr_number, comment_id, task_id, created_at)"
|
||||
" VALUES(?, ?, ?, NULL, ?)")
|
||||
self.assertNotIn("OR IGNORE", sql)
|
||||
self.assertIn("ON CONFLICT DO NOTHING", sql)
|
||||
self.assertIn("NULL", sql)
|
||||
|
||||
def test_or_replace_app_config_updates_on_conflict(self):
|
||||
from cloud.db import _translate_sql
|
||||
sql, _ = _translate_sql(
|
||||
"INSERT OR REPLACE INTO github_app_config(id, app_id, slug,"
|
||||
" client_id, webhook_secret, pem, html_url, created_at)"
|
||||
" VALUES(1, ?, ?, ?, ?, ?, ?, ?)")
|
||||
self.assertIn("ON CONFLICT (id) DO UPDATE SET", sql)
|
||||
self.assertIn("slug = EXCLUDED.slug", sql)
|
||||
self.assertNotIn("OR REPLACE", sql)
|
||||
|
||||
|
||||
class AppHttpTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
@@ -391,6 +432,33 @@ class AppHttpTestCase(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
body["installations"][0]["repo_full_name"], "octo/a")
|
||||
|
||||
def test_installation_repositories_sync(self):
|
||||
# 安装后增减仓库的同步事件:added 增行。
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
save_app_config(conn, {
|
||||
"app_id": 424242, "slug": "fake-codex-app",
|
||||
"client_id": "Iv1.fake", "webhook_secret": "fake-app-secret",
|
||||
"pem": "fake-pem",
|
||||
"html_url": "https://github.com/apps/fake-codex-app"})
|
||||
finally:
|
||||
conn.close()
|
||||
payload = {
|
||||
"action": "added",
|
||||
"installation": {"id": 11, "account": {"login": "octo"}},
|
||||
"repositories_added": [{"full_name": "octo/c"}]}
|
||||
status, body = self._webhook(
|
||||
"installation_repositories", payload, "fake-app-secret",
|
||||
"d-inst-repos-1")
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(body["result"], "installation_recorded")
|
||||
status, body = self._req(
|
||||
"GET", "/github-app/status", key=self.key_a)
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(
|
||||
[r["repo_full_name"] for r in body["installations"]],
|
||||
["octo/c"])
|
||||
|
||||
def test_webhook_no_secret_is_503(self):
|
||||
os.environ.pop("GITHUB_WEBHOOK_SECRET", None)
|
||||
status, _ = self._webhook(
|
||||
|
||||
Reference in new issue
Block a user