From c99332d7705db516ed549e1d12fe60513a70581e Mon Sep 17 00:00:00 2001 From: hermes Date: Tue, 13 Oct 2026 10:27:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ci):=20=E9=80=9A=E7=9F=A5=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E6=94=B9=E7=94=A8=E9=9D=9E=E7=A7=98=E5=AF=86=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20CI=5FPUBLIC=5FBASE=5FURL=EF=BC=88run/=E6=8F=90?= =?UTF-8?q?=E4=BA=A4/=E5=8F=98=E6=9B=B4=E7=BB=9F=E4=B8=80=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E7=BC=BA=E5=A4=B1=E6=88=96=E9=9D=9E=E6=B3=95=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么要改:通知里的链接一直用 GITHUB_SERVER_URL 拼(内网地址),群里/换网就点不开。 做法(不覆盖 GITHUB_SERVER_URL、不碰检出与内部调用): - ci_report.py 新增 _link_base():CI_PUBLIC_BASE_URL 优先,缺/非法(非 http(s)、带 userinfo、带空白)⇒ 明确回退 GITHUB_SERVER_URL,并把来源写进 payload 的 link_base_source(可复盘)。 - 三类链接统一用它:run_url、commit_url(新增,提交行用短 SHA 展示、完整 SHA 在链接里)、compare_url;仓库必须是真 owner/repo、SHA 必须是完整 40 位,否则宁可不给链接。 - workflow 上报步骤加非秘密 CI_PUBLIC_BASE_URL: https://gitea.zheromiku.top(写在 YAML 里便于审查)。 - **不做消息文本的全局 URL 替换**:提交标题/摘要里的 URL 仍是文本(bridge 侧负例钉住)。 - 自检补 _link_base/_commit_url 分支(缺失回退、非法回退、尾斜杠、短 SHA 不拼链接)。 --- .gitea/scripts/ci_report.py | 94 ++++++++++++++++++++++++++++++++++--- .gitea/workflows/build.yml | 5 ++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/.gitea/scripts/ci_report.py b/.gitea/scripts/ci_report.py index 16381b9..205f66b 100644 --- a/.gitea/scripts/ci_report.py +++ b/.gitea/scripts/ci_report.py @@ -182,6 +182,37 @@ def self_test() -> int: _, got, _ = verify(rep, expect, in_ci) if len(got) != want: problems.append(f"{name}:期望 {want} 个问题,实际 {len(got)} 个 {got}") + # 通知链接基址:优先 CI_PUBLIC_BASE_URL,缺/非法都必须**明确回退**(不猜、不静默) + env_backup = {k: os.environ.get(k) for k in ("CI_PUBLIC_BASE_URL", "GITHUB_SERVER_URL")} + try: + os.environ["GITHUB_SERVER_URL"] = "http://internal:3000" + os.environ.pop("CI_PUBLIC_BASE_URL", None) + b, src = _link_base() + if b != "http://internal:3000" or "GITHUB_SERVER_URL" not in src: + problems.append(f"没配 CI_PUBLIC_BASE_URL 时没回退到 GITHUB_SERVER_URL:{b!r}/{src!r}") + os.environ["CI_PUBLIC_BASE_URL"] = "https://gitea.zheromiku.top/" + b2, src2 = _link_base() + if b2 != "https://gitea.zheromiku.top" or src2 != "CI_PUBLIC_BASE_URL": + problems.append(f"配了 CI_PUBLIC_BASE_URL 没用它(或没去掉尾斜杠):{b2!r}/{src2!r}") + for bad in ("gitea.zheromiku.top", "ftp://gitea.zheromiku.top", + "https://u:p@gitea.zheromiku.top", "https://a b/"): + os.environ["CI_PUBLIC_BASE_URL"] = bad + b3, src3 = _link_base() + if b3 != "http://internal:3000" or "回退" not in src3: + problems.append(f"非法 CI_PUBLIC_BASE_URL({bad!r}) 没明确回退:{b3!r}/{src3!r}") + os.environ["CI_PUBLIC_BASE_URL"] = "https://gitea.zheromiku.top" + if _commit_url("a/b", "c" * 40, "https://gitea.zheromiku.top") != \ + f"https://gitea.zheromiku.top/a/b/commit/{'c' * 40}": + problems.append("提交链接拼错") + for bad_repo, bad_sha in (("onlyone", "c" * 40), ("a/b", "c" * 12), ("a/b", "")): + if _commit_url(bad_repo, bad_sha, "https://gitea.zheromiku.top"): + problems.append(f"仓库/短 SHA 不合法却仍拼出链接:{bad_repo!r}/{bad_sha[:12]!r}") + finally: + for k2, v2 in env_backup.items(): + if v2 is None: + os.environ.pop(k2, None) + else: + os.environ[k2] = v2 if problems: print("❌ 上报核对自检失败:", file=sys.stderr) for p in problems: @@ -228,9 +259,47 @@ def _scope_for_notify(): return [], "", f"{e.__class__.__name__}" -def _compare_url(repo: str) -> str: +def _link_base() -> tuple: + """通知里的链接用哪个基址:**CI_PUBLIC_BASE_URL 优先**,没配/非法就退回 GITHUB_SERVER_URL。 + + 只接受 `http(s)://host[:port][/path]` 的绝对地址、不接受带凭据(userinfo)的写法。 + 返回 (base, source);source 会写进上报与 CI 日志,方便事后核对"这条链接是谁给的基址"。 + + ⚠️ 这是**唯一的**链接基址来源:不做"把消息文本里的 URL 全部替换一遍"那种事 —— + 那样会把提交标题、日志摘要里的 URL 也改掉(那些是事实,不许动)。 + """ + pub = (os.environ.get("CI_PUBLIC_BASE_URL") or "").strip() + fallback = (os.environ.get("GITHUB_SERVER_URL") or "").strip().rstrip("/") + if pub: + bad = None + if not re.match(r"^https?://", pub): + bad = "不是 http(s) 绝对地址" + elif " " in pub or " " in pub: + bad = "含空白字符" + else: + m = re.match(r"^[a-z]+://([^/]+)", pub, re.I) + if not m or "@" in m.group(1): + bad = "主机部分不合法或带凭据(userinfo)" + if bad: + return fallback, f"CI_PUBLIC_BASE_URL 非法({bad})⇒ 回退 GITHUB_SERVER_URL" + return pub.rstrip("/"), "CI_PUBLIC_BASE_URL" + return fallback, "GITHUB_SERVER_URL(未配 CI_PUBLIC_BASE_URL)" + + +def _repo_ok(repo: str) -> bool: + """链接里的仓库必须是真的 owner/repo(两段、都非空),否则宁可不给链接。""" + parts = [x for x in str(repo or "").split("/")] + return len(parts) == 2 and all(x.strip() for x in parts) + + +def _commit_url(repo: str, sha: str, base: str) -> str: + if not _repo_ok(repo) or not base or not re.match(r"^[0-9a-f]{40}$", str(sha or "")): + return "" + return f"{base}/{repo}/commit/{sha}" + + +def _compare_url(repo: str, base: str) -> str: """变更列表链接:只有拿到真实的 before/after 才给(不然就是编一个打不开的地址)。""" - base = (os.environ.get("GITHUB_SERVER_URL") or "").rstrip("/") ev = (os.environ.get("GITHUB_EVENT_PATH") or "").strip() if not base or not repo or not ev or not os.path.exists(ev): return "" @@ -351,6 +420,15 @@ def main(argv: list[str]) -> int: if problems: print("❌ 汇总不可信/不完整 ⇒ 不能报绿(作业这次不算通过)", file=sys.stderr) + _lb_base, _lb_source = _link_base() + repo_full = (rep.get("repository") or os.environ.get("GITHUB_REPOSITORY") or "").strip() + if _lb_source != "CI_PUBLIC_BASE_URL": + print(f"提示:通知链接基址 = {_lb_base or '(未取得)'}({_lb_source})", flush=True) + else: + print(f"通知链接基址 = {_lb_base}(来自 CI_PUBLIC_BASE_URL)", flush=True) + if not _repo_ok(repo_full): + print(f"⚠️ 仓库名不是 owner/repo({repo_full[:40]!r})⇒ 通知里不给 run/提交/变更 链接", + file=sys.stderr) _scope_commits, _scope_source, _scope_err = _scope_for_notify() _scope_payload = {"commits": _scope_commits, "source": _scope_source, "count": len(_scope_commits)} @@ -384,9 +462,8 @@ def main(argv: list[str]) -> int: "event": rep.get("event"), "run_mode": rep.get("run_mode"), "run_id": os.environ.get("GITHUB_RUN_ID", ""), - "run_url": (f"{os.environ.get('GITHUB_SERVER_URL', '')}/" - f"{rep.get('repository', '')}/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')}" - if os.environ.get("GITHUB_RUN_ID") else ""), + "run_url": (f"{_lb_base}/{repo_full}/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')}" + if (os.environ.get("GITHUB_RUN_ID") and _lb_base and _repo_ok(repo_full)) else ""), "checks": checks, "manifest": rep.get("manifest"), "problems": (rep.get("problems") or []) + problems, @@ -403,7 +480,10 @@ def main(argv: list[str]) -> int: "commits": _scope_payload["commits"], # 准确范围里的提交标题(≤10 条) "range_source": _scope_payload["source"], # 范围是怎么算出来的(可复盘) "commit_count": (_scope_payload["count"] or payload_count), # 条数由 Git 算 - "compare_url": _compare_url(rep.get("repository", "")), + "compare_url": _compare_url(repo_full, _lb_base), + "commit_url": _commit_url(repo_full, rep.get("commit_sha") or "", _lb_base), + "link_base": _lb_base, # 链接基址(可复盘) + "link_base_source": _lb_source, # 基址从哪来:CI_PUBLIC_BASE_URL / GITHUB_SERVER_URL 回退 "run_duration_s": "", # 整次运行耗时:本 workflow 没留起点 ⇒ 不编 "duration_kind": "gate", "first_failure": _first_failure(checks), @@ -438,4 +518,4 @@ def main(argv: list[str]) -> int: if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) \ No newline at end of file + raise SystemExit(main(sys.argv[1:])) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 5d4c183..7ddcb47 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -177,6 +177,11 @@ jobs: env: BRIDGE_URL: ${{ vars.CI_BRIDGE_URL }} BRIDGE_TOKEN: ${{ secrets.CI_BRIDGE_TOKEN }} + # 通知里的链接基址(**非秘密**,直接写在 workflow 里便于审查与复盘): + # CI 运行 / 提交 / 变更 三类链接统一用它拼;提交与变更里的仓库、SHA、run_id 仍是真实值。 + # **不覆盖 GITHUB_SERVER_URL**(检出与所有内部调用继续用它,检出地址一个字节没变)。 + # 删掉这一行 ⇒ ci_report.py 明确回退到 GITHUB_SERVER_URL,并在上报里写 link_base_source。 + CI_PUBLIC_BASE_URL: https://gitea.zheromiku.top run: | set +e; set +o pipefail B="${BRIDGE_URL:-}"