diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a9e0a399..362ddc17 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -387,7 +387,7 @@ jobs: # 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头, # 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一 # 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信 - # https 基址后再取。 + # https 基址后再取。只追加 pin 的三个 40 位 hex,不 source pin 文件。 SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')" scheme="${GITHUB_SERVER_URL%%://*}" export GIT_CONFIG_COUNT=4 @@ -400,15 +400,43 @@ jobs: export GIT_CONFIG_KEY_3="core.hooksPath" export GIT_CONFIG_VALUE_3="/dev/null" git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm - # 凭据环境须保持到 pins 子模块取完(insteadOf + token 头覆盖嵌套 SSH URL), - # 之后再 unset。 - git -C rdm submodule update --init --recursive + mapfile -t pins < <(python3 - <<'PY' + import pathlib + got = {} + for line in pathlib.Path("tests/integration/lifecycle/rdm.pin").read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + if key not in ("sha", "prev_sha", "legacy_sha") or len(value) != 40: + raise SystemExit("rdm.pin rejected") + if any(c not in "0123456789abcdef" for c in value): + raise SystemExit("rdm.pin rejected") + got[key] = value + if set(got) != {"sha", "prev_sha", "legacy_sha"}: + raise SystemExit("rdm.pin rejected") + print(got["prev_sha"]) + print(got["legacy_sha"]) + print(got["sha"]) + PY + ) + [ "${#pins[@]}" -eq 3 ] + git -C rdm fetch --depth=1 origin "${pins[@]}" + for rev in "${pins[@]}"; do + git -C rdm checkout --detach "$rev" + git -C rdm submodule update --init --recursive + done echo "RDM commit: $(git -C rdm rev-parse HEAD)" unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \ GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \ GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3 unset SUBMODULE_TOKEN - make -C rdm build + : > rdm/pin-build-digests.txt + for rev in "${pins[@]}"; do + git -C rdm checkout --detach "$rev" + make -C rdm build + sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt + done - name: Install Node 22 run: | set -euo pipefail diff --git a/tests/integration/lifecycle/lifecycle_suite.py b/tests/integration/lifecycle/lifecycle_suite.py index 9e24e947..ece1e23a 100644 --- a/tests/integration/lifecycle/lifecycle_suite.py +++ b/tests/integration/lifecycle/lifecycle_suite.py @@ -1,18 +1,77 @@ """真实 Dev/RDM 组合门禁。由 make test-lifecycle 调用。 缺 PostgreSQL、RDM 源码或制品时直接失败,不静默跳过。 -登录依赖账户 CLI 公共化(账户方案 U7/U5),单独标 NOT_RUN, +登录与 rdm.ping 依赖账户 CLI 公共化,单独标 NOT_RUN, 不因此把已经跑过的真实组合场景记成失败。 """ +import hashlib import os import subprocess import sys from pathlib import Path +from urllib.parse import urlparse ROOT = Path(__file__).resolve().parents[3] PIN = Path(__file__).resolve().parent / "rdm.pin" +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def print_inventory(src, dev, rdm): + print("组合清单") + head = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "HEAD"], + capture_output=True, text=True) + print("dev_sha", head.stdout.strip() if head.returncode == 0 else "NOT_RUN") + modules = subprocess.run( + ["git", "-C", str(ROOT), "submodule", "status", "--recursive"], + capture_output=True, text=True) + if modules.returncode == 0: + for line in modules.stdout.splitlines(): + print("submodule", " ".join(line.split()[:2])) + else: + print("submodule NOT_RUN") + print("pin") + for line in PIN.read_text().splitlines(): + if line.strip() and not line.startswith("#"): + print(" ", line.strip()) + print("dev_bin", dev, sha256_file(dev)) + print("rdm_bin", rdm, sha256_file(rdm)) + digests = Path(src) / "pin-build-digests.txt" + if digests.is_file(): + for line in digests.read_text().splitlines(): + print("pin_build", line) + else: + print("pin_build NOT_RUN:本进程没有预构建三个 pin") + print("pg_version", pg_version()) + print("登录 NOT_RUN:RDM 二进制没有 user/apikey,工作台登录依赖账户方案 U7/U5") + print("rdm.ping NOT_RUN:需要已登录会话,本轮不公共化账户 CLI") + + +def pg_version(): + parsed = urlparse(os.environ.get("AGENT_PG_TEST_DSN", "")) + if not parsed.hostname: + return "NOT_RUN" + env = os.environ.copy() + env.update({ + "PGHOST": parsed.hostname, + "PGPORT": str(parsed.port or 5432), + "PGUSER": parsed.username or "", + "PGPASSWORD": parsed.password or "", + "PGDATABASE": parsed.path.lstrip("/"), + }) + proc = subprocess.run(["psql", "-tAc", "SHOW server_version"], + env=env, capture_output=True, text=True) + if proc.returncode != 0: + return "NOT_RUN" + return proc.stdout.strip() or "NOT_RUN" + + def main(): missing = [] if not os.environ.get("AGENT_PG_TEST_DSN"): @@ -30,18 +89,12 @@ def main(): print("test-lifecycle 失败。缺:%s" % ", ".join(missing)) return 1 - print("组合清单") - print("pin", PIN.read_text().strip()) - print("rdm_src", src) - print("dev_bin", dev) - print("rdm_bin", rdm) print("将运行 tests.test_svc_realcombo:SQLite 身份、RDM 二进制生命周期、PostgreSQL 身份环境交付") print("将运行 tests.integration.lifecycle.test_lifecycle_real:pin 上的真实候选与切换") if os.environ.get("SVC_RDM_ORIGIN"): print("将运行全轨部署:SVC_RDM_ORIGIN 已设置") else: print("全轨部署 NOT_RUN:未设置 SVC_RDM_ORIGIN(由 svc-realcombo 作业覆盖)") - print("登录 NOT_RUN:RDM 二进制没有 user/apikey,工作台登录依赖账户方案 U7/U5") env = os.environ.copy() env["SVC_TEST_REAL"] = "1" env["SVC_TEST_PG"] = "1" @@ -52,10 +105,11 @@ def main(): "tests.test_svc_realcombo", "tests.integration.lifecycle.test_lifecycle_real", "-q"], cwd=str(ROOT), env=env) + print_inventory(src, dev, rdm) if proc.returncode != 0: print("test-lifecycle 失败:真实组合用例未通过") return proc.returncode - print("test-lifecycle 可跑场景通过;登录与未设 origin 的全轨部署保持 NOT_RUN") + print("test-lifecycle 可跑场景通过;登录与 rdm.ping 保持 NOT_RUN") return 0 diff --git a/tests/test_ci_credentials.py b/tests/test_ci_credentials.py index d32ec708..6f6fb350 100644 --- a/tests/test_ci_credentials.py +++ b/tests/test_ci_credentials.py @@ -25,6 +25,14 @@ def checkout_script(): return textwrap.dedent("\n".join(lines[start:end])) +def rdm_clone_script(): + lines = WORKFLOW.read_text().splitlines() + start = next(i for i, line in enumerate(lines) if "Clone Agent-System-RDM" in line) + start = next(i for i in range(start, len(lines)) if lines[i].strip() == "run: |") + 1 + end = next(i for i in range(start, len(lines)) if lines[i].startswith(" - name:")) + return textwrap.dedent("\n".join(lines[start:end])) + + class CheckoutTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() @@ -236,6 +244,49 @@ class CheckoutTests(unittest.TestCase): self.assertNotEqual(result.returncode, 0) self.assertFalse(any("untrusted" in path for path, auth in self.requests)) + def test_rdm_pin_fetch_is_three_shas_on_the_allowlist(self): + source = self.base / "Agent-System-RDM" + source.mkdir() + self.git(source, "init", "-q", "-b", "main") + shas = [] + for index in range(3): + (source / "Makefile").write_text( + "build:\n\tmkdir -p bin\n\tprintf 'rev%d\\n' > bin/agent-system-rdm\n" % index) + self.git(source, "add", ".") + self.git(source, "-c", "user.name=Fixture", + "-c", "user.email=fixture@example.invalid", + "commit", "-q", "-m", "rev %d" % index) + shas.append(self.git(source, "rev-parse", "HEAD")) + target = self.published / "weichen" / "Agent-System-RDM.git" + target.parent.mkdir(parents=True, exist_ok=True) + self.git(self.base, "clone", "-q", "--bare", str(source), str(target)) + self.git(target, "config", "uploadpack.allowReachableSHA1InWant", "true") + self.git(target, "config", "uploadpack.allowAnySHA1InWant", "true") + work = self.base / "rdm-job" + pin = work / "tests" / "integration" / "lifecycle" + pin.mkdir(parents=True) + (pin / "rdm.pin").write_text( + "sha=%s\nprev_sha=%s\nlegacy_sha=%s\n" % (shas[2], shas[1], shas[0])) + script = rdm_clone_script() + self.assertNotRegex(script, r"(?m)^\s*source\s") + self.requests.clear() + result = subprocess.run( + ["bash", "-c", script], cwd=work, + env=self.env | {"SUBMODULE_TOKEN": SUB_TOKEN, "GITHUB_SERVER_URL": self.url}, + capture_output=True, text=True, timeout=60) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn(SUB_TOKEN, result.stdout + result.stderr) + self.assertTrue(self.requests) + for path, auth in self.requests: + self.assertRegex(path, r"^/weichen/Agent-System-RDM\.git/") + self.assertEqual(auth, "token " + SUB_TOKEN) + for sha in shas: + self.assertEqual(self.git(work / "rdm", "cat-file", "-t", sha), "commit") + self.assertEqual(self.git(work / "rdm", "rev-parse", "HEAD"), shas[2]) + digests = (work / "rdm" / "pin-build-digests.txt").read_text() + for sha in shas: + self.assertIn(sha, digests) + if __name__ == "__main__": unittest.main()