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:-}"